From 419ec78fa4302cc89c8b9bb76532523032d3c10c Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 5 Aug 2026 08:05:05 -0700 Subject: [PATCH 1/2] Settle Markdown capitalization and fix two TODO prose defects Answers the live half of #565, the six suppressed Copilot findings the digest defect in #562 hid, which reached no thread and so were never answered anywhere. ## The two prose defects Both are in `TODO.md` and were raised on #546 round 4. The downstream-surface entry joined two independent clauses with a comma, now two sentences. The reading-a-local-clone entry read "answers instead what that clone last saw", missing its connector, now "answers a different question, which is what that clone last saw". ## The capitalization convention The reviewer asked for the proper noun on one bullet of #561. That bullet is now internally consistent, so the live question was the file-wide mix rather than the line: `GOVERNANCE.md` alone carried 17 lowercase against 7 capitalized, and the same mix is vendored fleet-wide, so settling it per file settles it nowhere. `CODESTYLE.md` "Markdown and Spelling" gains item 5. Prose capitalizes the format's name; lowercase is for the strings a machine reads, meaning a tool or package name, a settings key, a heading anchor, an identifier, and a file extension. It lands in that file because every repo carries it, so the convention arrives with the file rather than being re-decided per repo. The sweep then corrected 40 prose occurrences across 18 files, in documents, code comments, docstrings, and two audit output strings. No identifier changed, and the tree now carries one spelling of the format's name. ## For the reviewer The sweep touched four `verbatim` regions, so every downstream repo is byte-mismatched until re-vendored: three `GOVERNANCE.md` sections by one word each, plus two comment lines in `.markdownlint-cli2.jsonc`, which is verbatim and whole. `CODESTYLE.md` is `intent`, so its new item reaches the fleet as a rule to adopt. `TODO.md`'s re-vendor entry records all of it and now names five files rather than three. The three findings #565 lists as already resolved were re-verified against the tree and need no change here. ## Verification prose_lint's gating rules (charset, dupword, spelling) clean tree-wide; the 157 prose_lint tests, test_repo_gate, test_pr_review, and `audit.py --selftest` all pass; repo_gate clean on eol and sha-pin; `spec/validate.py` clean; markdownlint-cli2 0 issues in 44 files; cspell clean; editorconfig-checker clean. Every changed file kept its own line endings. The `--diff` run reports five warn-only findings (four comment-wrap, one dash) on the three lines this diff touched in `spec/audit.py`, `spec/validate.py`, and `spec/fidelity-model.md`. All predate this change and belong to the #519 backlog: those files carry 101 and 8 respectively, and correcting one line of each would leave the file no more conformant while burying a one-word sweep. Related: #565 Co-authored-by: Claude Opus 5 (1M context) --- .github/copilot-instructions.md | 2 +- .markdownlint-cli2.jsonc | 4 ++-- AUDIT.md | 2 +- CODESTYLE.md | 7 ++++--- GOVERNANCE.md | 6 +++--- OPERATIONS.md | 2 +- STANDUP.md | 2 +- TODO.md | 8 ++++---- scripts/README.md | 12 ++++++------ scripts/pr_review.py | 2 +- scripts/prose_lint.py | 6 +++--- scripts/test_pr_review.py | 6 +++--- scripts/test_prose_lint.py | 10 +++++----- spec/audit.py | 10 +++++----- spec/fidelity-model.md | 4 ++-- spec/readme-structure.md | 2 +- spec/section-model.md | 2 +- spec/validate.py | 4 ++-- 18 files changed, 46 insertions(+), 45 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5a9434b7d..cf856d7cb 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -37,7 +37,7 @@ Auto-review on push is configured (via the branch ruleset's `copilot_code_review **A review with no inline comments is still a completed review, not a failure, and not a reason to ask the maintainer to re-trigger.** Copilot very often posts a single formal review (GraphQL `state: COMMENTED`) whose body ends with "...reviewed N of N changed files ... and generated no comments" and adds **zero** inline threads. That review carries the head `commit.oid` and fully satisfies the loop, and it is the clean-pass success case. Never read "no inline comments" as "the review didn't run," and never re-request or escalate to the maintainer because comments are absent. -**Read the low-confidence findings, which are not inline threads.** A review body can carry a collapsed `
` block of findings Copilot withheld from the inline threads, and those findings appear nowhere in `reviewThreads`, so a loop that polls threads alone never sees them and reports a clean pass. **Match the block on more than one phrasing.** Its heading has appeared both as `Suppressed comments (N)` and as "Comments suppressed due to low confidence", so a filter keyed on either one alone silently reports zero suppressed findings on a review that has them, the same false clean this rule exists to prevent, one level up in the detection. **The section moves as well as it is worded, so match the heading wherever it sits.** It has appeared as its own `
` wrapper with a matching ``, as a bare heading in the body, and as a markdown heading nested inside the `Review details` wrapper, whose `` names the wrapper and not the section. A filter reading a wrapper's `` reports zero on the nested shape, and the count it needs is the heading's own `(N)` rather than the wrapper's. They have been right repeatedly, including a rule stated more broadly than its check enforced and a check that skipped fenced blocks in every rule but one. Read the body of every review, investigate each suppressed finding on the same footing as an inline one, and answer it in the PR conversation, since a suppressed finding has no thread to reply on or resolve. +**Read the low-confidence findings, which are not inline threads.** A review body can carry a collapsed `
` block of findings Copilot withheld from the inline threads, and those findings appear nowhere in `reviewThreads`, so a loop that polls threads alone never sees them and reports a clean pass. **Match the block on more than one phrasing.** Its heading has appeared both as `Suppressed comments (N)` and as "Comments suppressed due to low confidence", so a filter keyed on either one alone silently reports zero suppressed findings on a review that has them, the same false clean this rule exists to prevent, one level up in the detection. **The section moves as well as it is worded, so match the heading wherever it sits.** It has appeared as its own `
` wrapper with a matching ``, as a bare heading in the body, and as a Markdown heading nested inside the `Review details` wrapper, whose `` names the wrapper and not the section. A filter reading a wrapper's `` reports zero on the nested shape, and the count it needs is the heading's own `(N)` rather than the wrapper's. They have been right repeatedly, including a rule stated more broadly than its check enforced and a check that skipped fenced blocks in every rule but one. Read the body of every review, investigate each suppressed finding on the same footing as an inline one, and answer it in the PR conversation, since a suppressed finding has no thread to reply on or resolve. ```sh # `test` with an alternation, not `contains` on one phrasing: the heading wording has changed. diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index 40f5d7624..89ff90dee 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -3,9 +3,9 @@ // Prose paragraphs and data-heavy tables or URLs are intentionally long. // Reflowing at 80 columns hurts readability and churns diffs. "MD013": false, - // MD033 (inline HTML) stays enabled so native markdown wins. + // MD033 (inline HTML) stays enabled so native Markdown wins. // HTML comments, used as reference-link dividers, pass it. - // The details and summary elements are allowed for GitHub collapsibles, which have no markdown equivalent. + // The details and summary elements are allowed for GitHub collapsibles, which have no Markdown equivalent. // Every other element still flags. "MD033": { "allowed_elements": ["details", "summary"] }, // Require fenced code blocks over the legacy 4-space-indented style. diff --git a/AUDIT.md b/AUDIT.md index 22aadfbc4..18cff0300 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -12,7 +12,7 @@ The verdict vocabulary is [`WORKFLOW.md`][workflow]'s: **operational / not opera This audit is not occasional. Run it whenever you **create, adopt, or materially change** a fleet repo, and on demand for any known repo: - **Onboarding a repo is complete only when it either passes this audit** (operational on every applicable check) **or carries a committed `reports//audit.md` plus a tracking issue** enumerating every residual delta. A repo that is partially set up but never audited is itself a **defect**, the exact state this process prevents. The create-to-conformance counterpart is [`STANDUP.md`][standup]. Because both read the same manifests, a repo stood up by that file passes this audit by construction. -- **Touching a repo** (any conformance-affecting change) ends by re-running the applicable checks and **reconciling the registry entry to reality**: `status`, `types`, `releaseTrigger`, `workflowModel`, `driftNotes`. The registry records reality, not intent. [`spec/validate.py`][validate] proves the catalog is self-consistent, not that it matches the live repo. Closing that gap is this audit's job. The deterministic subset (settings, rulesets, secret names, file presence, per-scope markdown section presence, workflow interface conformance, verbatim content, branch facts) is mechanized in [`spec/audit.py`][audit-runner]: owner-initiated, run on demand when onboarding a repo, on suspected drift, or before fleet-wide changes. A required section missing from a carried markdown file is a **drift finding**, not a letter, because a heading rename reads as missing and equivalence is judged by hand. A carried `interface` workflow (spec/fidelity-model.md) is checked by name and wiring (required jobs, the ruleset-bound check name, the artifact-name handoff, and the forbidden `artifact-ids:` fork), all at **drift**, since the body is owned and a rename is a hint to verify. A carried `verbatim` unit, whether a whole file (`.markdownlint-cli2.jsonc`) or a canonical workflow job region (the `github-release` job), is content-hashed against the hub's canonical after line-ending normalization. A mismatch is classified **stale** (matches a past hub revision, re-vendor) or **modified** (matches none, the repo changed fixed content), both at **drift**, since equivalence is intent-governed and a byte diff is a hint to review. +- **Touching a repo** (any conformance-affecting change) ends by re-running the applicable checks and **reconciling the registry entry to reality**: `status`, `types`, `releaseTrigger`, `workflowModel`, `driftNotes`. The registry records reality, not intent. [`spec/validate.py`][validate] proves the catalog is self-consistent, not that it matches the live repo. Closing that gap is this audit's job. The deterministic subset (settings, rulesets, secret names, file presence, per-scope Markdown section presence, workflow interface conformance, verbatim content, branch facts) is mechanized in [`spec/audit.py`][audit-runner]: owner-initiated, run on demand when onboarding a repo, on suspected drift, or before fleet-wide changes. A required section missing from a carried Markdown file is a **drift finding**, not a letter, because a heading rename reads as missing and equivalence is judged by hand. A carried `interface` workflow (spec/fidelity-model.md) is checked by name and wiring (required jobs, the ruleset-bound check name, the artifact-name handoff, and the forbidden `artifact-ids:` fork), all at **drift**, since the body is owned and a rename is a hint to verify. A carried `verbatim` unit, whether a whole file (`.markdownlint-cli2.jsonc`) or a canonical workflow job region (the `github-release` job), is content-hashed against the hub's canonical after line-ending normalization. A mismatch is classified **stale** (matches a past hub revision, re-vendor) or **modified** (matches none, the repo changed fixed content), both at **drift**, since equivalence is intent-governed and a byte diff is a hint to review. ## 1. Scope and Ground-Truth Branch diff --git a/CODESTYLE.md b/CODESTYLE.md index dfdfaa9fb..92b0950ac 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -2,7 +2,7 @@ This is the single code-style guide for the fleet. The **General** section applies to every language. Each **language section** (.NET, Python) is self-contained: a repo follows only the section(s) for the languages it ships and ignores the rest. A repo keeps the whole file rather than trimming it. An unused-language section costs nothing, the same whole-file model as [`.editorconfig`][root], whose inert `[*.cs]` block a non-.NET repo keeps. -Cross-cutting *process* rules (PR titles, branching, US English, markdown style, comments philosophy, workflow YAML, PR review etiquette, and the verification discipline that defines the pre-push lint gate) live in [GOVERNANCE.md][governance] and are not repeated here. +Cross-cutting *process* rules (PR titles, branching, US English, Markdown style, comments philosophy, workflow YAML, PR review etiquette, and the verification discipline that defines the pre-push lint gate) live in [GOVERNANCE.md][governance] and are not repeated here. ## General @@ -33,10 +33,11 @@ Each language defines a **clean-compile** verification: the combination of build These apply repo-wide, in every directory: -1. **Markdown linting**: All `.md` files must be lint-clean (error and warning free) via the VS Code `markdownlint` extension. [`.markdownlint-cli2.jsonc`][markdownlint-cli2] at the repo root is the single source of truth, and the davidanson `markdownlint` extension and a command-line `markdownlint-cli2` run both read it, so the IDE and CLI stay in lock-step. Rules it deliberately disables (e.g. `MD013` line-length) are **intentional**, so do not "fix" them. `MD033` inline HTML stays **enabled**: HTML comments are permitted (markdownlint does not flag them), `details` and `summary` are allowed because a GitHub collapsible has no markdown equivalent, every other element is flagged, and anything with a native markdown equivalent uses the markdown. Fix violations at the source rather than disabling rules. +1. **Markdown linting**: All `.md` files must be lint-clean (error and warning free) via the VS Code `markdownlint` extension. [`.markdownlint-cli2.jsonc`][markdownlint-cli2] at the repo root is the single source of truth, and the davidanson `markdownlint` extension and a command-line `markdownlint-cli2` run both read it, so the IDE and CLI stay in lock-step. Rules it deliberately disables (e.g. `MD013` line-length) are **intentional**, so do not "fix" them. `MD033` inline HTML stays **enabled**: HTML comments are permitted (markdownlint does not flag them), `details` and `summary` are allowed because a GitHub collapsible has no Markdown equivalent, every other element is flagged, and anything with a native Markdown equivalent uses the markdown. Fix violations at the source rather than disabling rules. 2. **Spelling**: All spelling must be clean via the CSpell VS Code integration, and words must be correctly spelled in **US English** (the repo-wide convention, per [GOVERNANCE.md][governance]). The shared `cspell.json` sets `"language": "en-US"` so British spellings are flagged, where a bare `"en"` accepts both US and British and silently passes the wrong spelling. Project-specific terms go in the shared `cspell.json` `words` list, the single source of truth the extension, CLI, and CI all read. The `.code-workspace` must **not** carry its own `cspell.words`/`cSpell.words` block, and when externalizing words into `cspell.json`, delete any word list left in the workspace (a leftover one duplicates the list and silently drifts). -3. **Spelling CI scope**: The enforced CI spell-check gate covers **`README.md` and `HISTORY.md` only**, because these are the files every repo visitor sees, so they must be clean. It is deliberately **not** all `**/*.md`: repos carry many markdown files full of technical terms, and gating every one of them would mean endlessly padding `cspell.json` just to keep CI green. Broad, live spell-checking across any file (source, markdown, text) is the **cspell editor extension's** job, so typos still surface to whoever is editing. A repo owner **may** widen their own CI file list, but README + HISTORY are the default; keep the CI workflow, the `Lint: Spelling` VS Code task, and the GOVERNANCE.md cspell one-liner on the same file list. The list is explicit (not a glob), so a repo that ships no `HISTORY.md` (e.g. one with no changelog) must drop it from all three surfaces and gate on `README.md` alone, since cspell errors on a listed file that does not exist. Markdown *linting* (item 1) stays repo-wide `**/*.md`, which does not choke on technical terms. +3. **Spelling CI scope**: The enforced CI spell-check gate covers **`README.md` and `HISTORY.md` only**, because these are the files every repo visitor sees, so they must be clean. It is deliberately **not** all `**/*.md`: repos carry many Markdown files full of technical terms, and gating every one of them would mean endlessly padding `cspell.json` just to keep CI green. Broad, live spell-checking across any file (source, Markdown, text) is the **cspell editor extension's** job, so typos still surface to whoever is editing. A repo owner **may** widen their own CI file list, but README + HISTORY are the default; keep the CI workflow, the `Lint: Spelling` VS Code task, and the GOVERNANCE.md cspell one-liner on the same file list. The list is explicit (not a glob), so a repo that ships no `HISTORY.md` (e.g. one with no changelog) must drop it from all three surfaces and gate on `README.md` alone, since cspell errors on a listed file that does not exist. Markdown *linting* (item 1) stays repo-wide `**/*.md`, which does not choke on technical terms. 4. **`HISTORY.md` mirrors the README opening**: `HISTORY.md` is the maintainer-curated changelog and opens as the README's twin, carrying the same `# ` (without the README's ToC-omit comment) and the same intro paragraph copied verbatim, then a `## Release History` section. The mirrored opening keeps the project identity consistent for a reader who lands on the changelog directly. The audit checks that the title and intro match the README, with HTML comments stripped. +5. **"Markdown" is the format's name**: The format is a proper noun, so prose capitalizes it, meaning a Markdown file, a Markdown link, and the Markdown a surface renders. Lowercase is for the strings a machine reads and for nothing else: a tool or package name (`markdownlint`, `markdownlint-cli2`, `yzhang.markdown-all-in-one`), a settings key (`markdown.extension.toc.levels`), a heading anchor (`#markdown-and-spelling`), an identifier in code, and a file extension. What this settles is the mix rather than either spelling, because a file carrying both gives the next author no default to follow and a reviewer a finding to raise on whichever one it wrote last. The rule lives here because every repo carries this file, so the convention arrives with it rather than being re-decided per repo. ## .NET diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 73e0c7b63..7bb7c42f8 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -144,7 +144,7 @@ Clarify devcontainer setup steps in README ### Markdown -- **Reference-style links in human-facing docs.** Every markdown file uses reference-style links only, **except** the four agent-instruction files that keep inline links: `AGENTS.md`, `GOVERNANCE.md`, `OPERATIONS.md`, and `.github/copilot-instructions.md`. Those are read one section at a time rather than end to end, so an inline target resolves where it is read while a definition at the bottom of the file is never reached. The exception is that closed list rather than a category to argue from, so every other markdown file follows the rule whatever its audience. Under the rule: every URI (internal path, anchor, external URL, or shield image) is defined at the **bottom of the file**, split into groups by type under an HTML-comment header (e.g. `<!-- Shields -->`, `<!-- Workflow -->`, `<!-- Repo -->`, `<!-- External -->`) with each group's definitions alphabetized by reference name. **Reference names are contextual and encode the target and its group**: `foo-shield` for a shield image, `foo-link` for an external URL, and a bare `foo` for a local path or anchor (e.g. `[license-shield]`, `[releases-link]`, `[repo-config]`), never numeric (`[1]`) or opaque. No inline `[text](uri)` targets in prose. **A URL inside a fenced code block stays inline**, because reference links do not resolve in code blocks, so do not extract it, and exclude fenced code from any link-integrity check (bracket literals like `["a", "b"]` otherwise read as undefined references). **Removing a link also removes its reference definition**, since an orphaned definition fails the no-unused-defs rule. The one exception is the Table of Contents, whose entries stay inline anchor links (see Table of Contents below). +- **Reference-style links in human-facing docs.** Every Markdown file uses reference-style links only, **except** the four agent-instruction files that keep inline links: `AGENTS.md`, `GOVERNANCE.md`, `OPERATIONS.md`, and `.github/copilot-instructions.md`. Those are read one section at a time rather than end to end, so an inline target resolves where it is read while a definition at the bottom of the file is never reached. The exception is that closed list rather than a category to argue from, so every other Markdown file follows the rule whatever its audience. Under the rule: every URI (internal path, anchor, external URL, or shield image) is defined at the **bottom of the file**, split into groups by type under an HTML-comment header (e.g. `<!-- Shields -->`, `<!-- Workflow -->`, `<!-- Repo -->`, `<!-- External -->`) with each group's definitions alphabetized by reference name. **Reference names are contextual and encode the target and its group**: `foo-shield` for a shield image, `foo-link` for an external URL, and a bare `foo` for a local path or anchor (e.g. `[license-shield]`, `[releases-link]`, `[repo-config]`), never numeric (`[1]`) or opaque. No inline `[text](uri)` targets in prose. **A URL inside a fenced code block stays inline**, because reference links do not resolve in code blocks, so do not extract it, and exclude fenced code from any link-integrity check (bracket literals like `["a", "b"]` otherwise read as undefined references). **Removing a link also removes its reference definition**, since an orphaned definition fails the no-unused-defs rule. The one exception is the Table of Contents, whose entries stay inline anchor links (see Table of Contents below). - **Table of Contents.** Generate it with the Markdown All in One extension, which fills and auto-updates the list on save, so leave the `## Table of Contents` heading for the extension to populate and never hand-author or hand-edit the entries. Exclude a heading with an inline `<!-- omit from toc -->` marker on it (the badge/build header block and the `## Table of Contents` heading itself carry it). The workspace sets which heading levels appear. - One logical paragraph per line, with no hard-wrap line-length limit. For an intentional hard line break within a block (stacked badges, status, or license lines), end the line with a trailing backslash (`\`). This explicit form is preferred over trailing whitespace and is not treated as a paragraph split. - Headings follow the title-case-with-short-bind-words rule from the PR-title section. @@ -314,7 +314,7 @@ Anti-pattern: don't keep flipping the code on the same style point. Flip the rul ## Communicating with the User -- **Reference every pull request as a clickable link.** When you mention a PR on a surface that renders markdown (chat, a summary, a report), render it as a markdown link to the PR (`[#123](https://github.com/OWNER/REPO/pull/123)`), never a bare `#123`. The same applies to issues and commits. **The form follows the surface.** Some surfaces link neither a markdown link nor a bare URL, an interactive prompt's question and option text among them, and pasting a full URL into one of those does not rescue it, since the reader gets a string to copy, which is the outcome this rule exists to prevent. There the reference is a bare `#123`, and the clickable link goes in the message that accompanies the prompt. The test is whether the reader can click it where it is read, not whether it was written in the syntax that works elsewhere. +- **Reference every pull request as a clickable link.** When you mention a PR on a surface that renders Markdown (chat, a summary, a report), render it as a Markdown link to the PR (`[#123](https://github.com/OWNER/REPO/pull/123)`), never a bare `#123`. The same applies to issues and commits. **The form follows the surface.** Some surfaces link neither a Markdown link nor a bare URL, an interactive prompt's question and option text among them, and pasting a full URL into one of those does not rescue it, since the reader gets a string to copy, which is the outcome this rule exists to prevent. There the reference is a bare `#123`, and the clickable link goes in the message that accompanies the prompt. The test is whether the reader can click it where it is read, not whether it was written in the syntax that works elsewhere. - **Ask for input as a numbered list.** When you need the user to decide or answer, present the questions, and any options, as a numbered list so they can reply per number. A single inline question is fine, and two or more are always numbered. - **Raise work blocked on the user as a direct interactive prompt.** When progress needs a decision, an authorization, or an answer only the user can give, ask for it through the interface's own prompt mechanism, at the point the work stops. Never leave it as prose in a summary: a handoff buried in a paragraph is a handoff that did not happen, because a summary reads as a report of finished work and the one line still waiting on the user is the easiest in it to skim past. The blocked item is the message, not a closing remark on a message about something else. **The options offered are the actions themselves**, and the one that unblocks the work names the action it authorizes ("squash and merge it"), so selecting it is the go-ahead rather than a note to act on later. Offering only ways to wait is the same failure in interactive clothing, since a prompt whose every choice is inaction reports the block rather than clearing it, and where the agent may not perform the authorized action itself, the option says who does it. This supersedes the numbered-list rule above wherever an interactive prompt is available, and the numbered list is the fallback where none is. @@ -400,7 +400,7 @@ Contributors commit to this repo with signed commits. The SSH-signing setup live Every repo's GitHub repository details (the About panel) follow a fixed convention so the fleet stays consistent and self-describing. -- **Description** matches the README's first non-empty line after the `#` H1 heading, as plain text, stripping markdown links (`[text](url)` and `[text][ref]` become `text`) since a description is not rendered. The README is the source of truth: set the description from it (`gh api -X PATCH repos/<owner>/<repo> -f description=...`), never the reverse. When the current description is *more specific* than the README (a chip revision or variant the README omits), surface the drift to the maintainer rather than silently discarding the detail, and the fix is to sharpen the README so the description follows it. Keep the line at most **100 characters**, Docker Hub's short-description cap and the tightest surface it feeds. For a repo that publishes a Docker image, the **Docker Hub short description** mirrors the same README intro line, so one canonical sentence carries to the README, the About panel, and Docker Hub alike. +- **Description** matches the README's first non-empty line after the `#` H1 heading, as plain text, stripping Markdown links (`[text](url)` and `[text][ref]` become `text`) since a description is not rendered. The README is the source of truth: set the description from it (`gh api -X PATCH repos/<owner>/<repo> -f description=...`), never the reverse. When the current description is *more specific* than the README (a chip revision or variant the README omits), surface the drift to the maintainer rather than silently discarding the detail, and the fix is to sharpen the README so the description follows it. Keep the line at most **100 characters**, Docker Hub's short-description cap and the tightest surface it feeds. For a repo that publishes a Docker image, the **Docker Hub short description** mirrors the same README intro line, so one canonical sentence carries to the README, the About panel, and Docker Hub alike. - **Topics** are optional, and any that are present match the repo's actual content. Do not invent topics to fill the field. - **Include in the home page**: Releases on, Deployments off, Packages off. These toggles are UI-only, since the REST and GraphQL APIs neither read nor write them, so they are set by hand and cannot be audited through `gh`. diff --git a/OPERATIONS.md b/OPERATIONS.md index 30e98be32..900c0e3f6 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -87,7 +87,7 @@ docker run --rm --pull=always -v "$PWD":/workdir --workdir /workdir davidanson/m docker run --rm --pull=always -v "$PWD":/workdir --workdir /workdir ghcr.io/streetsidesoftware/cspell:latest --no-progress README.md HISTORY.md ``` -Both commands are the canonical invocations from [GOVERNANCE.md](./GOVERNANCE.md). markdownlint reads every markdown file, while cspell reads `README.md` and `HISTORY.md` only. That narrower spelling scope is deliberate, since gating every markdown file would mean padding `cspell.json` with technical terms without end, and broad live spell-check is the editor extension's job. Widening it here produces noise that no gate acts on. +Both commands are the canonical invocations from [GOVERNANCE.md](./GOVERNANCE.md). markdownlint reads every Markdown file, while cspell reads `README.md` and `HISTORY.md` only. That narrower spelling scope is deliberate, since gating every Markdown file would mean padding `cspell.json` with technical terms without end, and broad live spell-check is the editor extension's job. Widening it here produces noise that no gate acts on. The `editorconfig-checker` action is setup-only. Using it alone silently skips the check, so CI invokes the checker itself rather than relying on the action. diff --git a/STANDUP.md b/STANDUP.md index 0b5a2d753..209c16ad4 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -92,7 +92,7 @@ Copy every [`spec/files.json`][files] entry whose `appliesTo` matches the repo's - `OPERATIONS.md`: how the repo is run, under the headings `Runbooks`, `Backup and Recovery`, `Logs and Debugging`, `Tool Usage`, and `Configuration Layout`. - `TODO.md`: the repo's running backlog, per [`spec/readme-structure.md`][readme-structure]. It keeps open work out of the README's section order, where it does not belong and changes on a different cadence from everything around it. -**`OPERATIONS.md` is required on every repo**, not optional, so it appears in the baseline above with `appliesTo: "*"`. It is presence-checked only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own and a repo with little to say still carries the file as a stub, meaning those five headings with no content under them, for which this repo's own `OPERATIONS.md` is the worked example. Do not read the `operational` workflow model into the requirement, because that selector describes where config lives rather than whether the repo has runbooks, and a repo that publishes to a package registry or deploys a site has operational surface under either model. It is the operational analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half, so real runbooks (a deploy procedure, a rollback, a retention policy, a credential rotation) go there rather than into a carried file. It is agent-instruction content, so it takes the inline-link exception the markdown rules name rather than the reference-style default. `ARCHITECTURE.md` and `TODO.md` stay advisory and are required by no selector, so a repo with nothing to say in one carries no file rather than an empty one. +**`OPERATIONS.md` is required on every repo**, not optional, so it appears in the baseline above with `appliesTo: "*"`. It is presence-checked only, the same footing as `README.md` and `HISTORY.md`, so its content is entirely the repo's own and a repo with little to say still carries the file as a stub, meaning those five headings with no content under them, for which this repo's own `OPERATIONS.md` is the worked example. Do not read the `operational` workflow model into the requirement, because that selector describes where config lives rather than whether the repo has runbooks, and a repo that publishes to a package registry or deploys a site has operational surface under either model. It is the operational analogue of `ARCHITECTURE.md`, and it is where an `AGENTS.md` split puts the repo-specific half, so real runbooks (a deploy procedure, a rollback, a retention policy, a credential rotation) go there rather than into a carried file. It is agent-instruction content, so it takes the inline-link exception the Markdown rules name rather than the reference-style default. `ARCHITECTURE.md` and `TODO.md` stay advisory and are required by no selector, so a repo with nothing to say in one carries no file rather than an empty one. Choose the destination while scaffolding rather than after. Repo-specific content left in a carried file is drift, which the audit lists as an undeclared section to reconcile, and reconciling it later means moving prose that downstream readers have already started trusting in the wrong place. diff --git a/TODO.md b/TODO.md index a4ffcde72..c2369fa04 100644 --- a/TODO.md +++ b/TODO.md @@ -30,19 +30,19 @@ The gates in [`scripts/`][scripts] are hub-only and are the fleet's main defense Everything here changes a file the fleet holds a copy of, so each entry costs a hub edit plus a sweep, and the sweep is the part that gets skipped. -- Re-vendor the changed `verbatim` content across the fleet, which is one sweep covering three files. `repo-config/configure.sh` is carried `verbatim` with `appliesTo: "*"` and the hub swept it to one sentence per line. In `AGENTS.md`, "Context and Delegation Discipline" carries the wait rule's failure clause and "Where the Rules Live" carries a row for the new section named below. In `GOVERNANCE.md`, "Verification Discipline" carries the rule that a launched process is not a result, "PR Review Etiquette" carries the five outcomes that close a finding, and "Representative Data in Agent-Authored Text" is an entirely new carried section that no downstream repo holds, which the audit reports as a missing section rather than as drift. Every repo already holding a copy of a changed section is byte-mismatched against the hub until it takes the new one, which the audit reports as stale rather than modified. This sweep is also the follow-through [#489][issue-489] and [#379][issue-379] were waiting on, and the `.editorconfig` line in [#353][issue-353] rides the same visit to each repo. Regenerate [reports/divergences.md][divergences-report] before using it as the work list, since the committed copy is dated 2026-07-22 and therefore predates the router split, which shows in it naming "Repository Boundaries and Write Safety", "Git and Commit Rules" and "Verification Discipline" as `AGENTS.md` sections when all three now live in `GOVERNANCE.md`. A stale ledger is the same hazard as a stale exemption, in that it hands out a work list measured against a tree that no longer exists. +- Re-vendor the changed `verbatim` content across the fleet, which is one sweep covering five files. `repo-config/configure.sh` is carried `verbatim` with `appliesTo: "*"` and the hub swept it to one sentence per line. In `AGENTS.md`, "Context and Delegation Discipline" carries the wait rule's failure clause and "Where the Rules Live" carries a row for the new section named below. In `GOVERNANCE.md`, "Verification Discipline" carries the rule that a launched process is not a result, "PR Review Etiquette" carries the five outcomes that close a finding, and "Representative Data in Agent-Authored Text" is an entirely new carried section that no downstream repo holds, which the audit reports as a missing section rather than as drift. Three further `GOVERNANCE.md` sections differ by a single word each, "Documentation Style Conventions", "Communicating with the User" and "Repository Details", where the format's name was capitalized to the convention `CODESTYLE.md` "Markdown and Spelling" now states, so they are byte-mismatched for a reason a reader of the diff would otherwise call cosmetic. Two comment lines in `.markdownlint-cli2.jsonc` took the same capitalization, and that file is `verbatim` and `whole`, so every downstream copy is byte-mismatched on a config nothing else changed about. `CODESTYLE.md` carries the new item and is the fifth file, at `intent` rather than `verbatim` fidelity, so it reaches the fleet as a rule each repo adopts in its own copy rather than as bytes to match, and the same mixed spelling is waiting in every downstream tree. Every repo already holding a copy of a changed section is byte-mismatched against the hub until it takes the new one, which the audit reports as stale rather than modified. This sweep is also the follow-through [#489][issue-489] and [#379][issue-379] were waiting on, and the `.editorconfig` line in [#353][issue-353] rides the same visit to each repo. Regenerate [reports/divergences.md][divergences-report] before using it as the work list, since the committed copy is dated 2026-07-22 and therefore predates the router split, which shows in it naming "Repository Boundaries and Write Safety", "Git and Commit Rules" and "Verification Discipline" as `AGENTS.md` sections when all three now live in `GOVERNANCE.md`. A stale ledger is the same hazard as a stale exemption, in that it hands out a work list measured against a tree that no longer exists. - Drop the unused `GITHUB_TOKEN` grants from the merge-bot workflow, per [#521][issue-521], which the tree confirms is still open, since all four jobs still carry `contents: write` and `pull-requests: write` while every write in the file authenticates with the App token. The finding is least privilege on the highest-blast-radius shape in the fleet, a `pull_request_target` workflow holding an App private key, where the grant is not exploitable today only because no step consumes it. Three things to settle: whether to drop the job-level blocks or set a workflow-level `permissions: {}`, whether the audit should compare permissions at all given the file is `interface` fidelity with only a `requiredJobKeys` contract, and the re-vendor, since every repo carries the file. Note that [`spec/files.json`][files] now declares this workflow at `appliesTo: "*"`, which closes the separate gap [#456][issue-456] raised, that D8.1 graded a file the file spec never required. - State the rule that an agent never assumes a Docker image is present locally, however recently it pulled one, because a background prune can remove an image between two commands of the same session and the run then fails on something that worked minutes earlier. The prescription is the always-pull flag by default, and an explicit `docker pull <image>:<tag>` before use where the flag does not apply. The four documented lint invocations already carry `--pull=always`, in [`GOVERNANCE.md`][governance] "Running the Linters Locally", mirrored in [`OPERATIONS.md`][operations] and both VS Code task snippets, so what is missing is the rule rather than the one-liners: an agent composing an ad-hoc `docker run` drops the flag precisely because it believes the image is already cached. Two things to settle when writing it. Where it lives, since that section is scoped to the four lint tools while the rule covers any container an agent starts, which argues for placing it with the Docker or verification guidance instead, and whether it is carried to the fleet, since every repo runs the same images from the same instructions. Note the limit before writing it, so the flag does not read as the whole answer: `docker run` against a registry tag re-pulls an absent image on its own, so the cases that actually break are a locally built tag with no registry to pull from, and any command that branches on the image being present (`docker image inspect`, `docker images`) rather than simply running it. - Decide where a carried file may name hub-only machinery, since `GOVERNANCE.md` "PR Review Etiquette" points at `scripts/pr_review.py` and the fleet carries the section but not the script. A downstream reader follows that pointer to a path their repo does not have. Either the script joins the carried set, or the rule states the behavior and drops the tool name the way the coordination-reference rule already requires for the template repo itself. - Revisit [#305][issue-305], which asks for a propagation path for the rule docs and is the oldest open issue here, because most of the machinery it asked for has since been built and the issue has not been re-scoped to match. The fidelity model, the `spec/files.json` manifest, [`spec/divergences.json`][divergences] with its generated [reports/divergences.md][divergences-report], and [`AUDIT.md`][audit-doc] section 10 together give the canonical-versus-adapted split and the audit path the issue proposed. What is genuinely still missing is the push half, since every one of those detects drift while the sweep that fixes it is manual, which is exactly the entry above. Either re-scope the issue to the push half or close it against the entry above and the audit-automation entry below. -- Reduce the surface that is copied downstream at all, since every carried file is a copy that breaks when it is not the current one. `repo-config/configure.sh` is the worked example from this session: a defect in its check mode was fixed once in the hub and immediately turned into a re-vendor owed by every repository carrying it, and until each takes the fix they run different logic while reporting the same command. A repository that misses the sweep does not fail loudly, it quietly audits itself with an older script. The alternative is for the hub to host the tool and for each downstream repository to call it rather than hold a copy, so one edit reaches the fleet and no stale copy exists to detect, re-vendor, or classify. This is the same argument as the reusable-workflow entry below and should be settled with it rather than separately, since both are about consuming hub code at runtime instead of vendoring it. Three things to settle. What the fleet actually needs vendored, which is the content a repository is audited against and could not run without, against what only needs to be reachable. How a consumed tool is pinned, because consuming hub code at runtime is a floating dependency unless it names a commit, and the action-pinning rule already requires exactly that. And what a repository does when the hub is unreachable, since a vendored copy still works offline and a called one does not, which is the real cost being traded away. +- Reduce the surface that is copied downstream at all, since every carried file is a copy that breaks when it is not the current one. `repo-config/configure.sh` is the worked example from this session: a defect in its check mode was fixed once in the hub and immediately turned into a re-vendor owed by every repository carrying it, and until each takes the fix they run different logic while reporting the same command. A repository that misses the sweep does not fail loudly. It quietly audits itself with an older script. The alternative is for the hub to host the tool and for each downstream repository to call it rather than hold a copy, so one edit reaches the fleet and no stale copy exists to detect, re-vendor, or classify. This is the same argument as the reusable-workflow entry below and should be settled with it rather than separately, since both are about consuming hub code at runtime instead of vendoring it. Three things to settle. What the fleet actually needs vendored, which is the content a repository is audited against and could not run without, against what only needs to be reachable. How a consumed tool is pinned, because consuming hub code at runtime is a floating dependency unless it names a commit, and the action-pinning rule already requires exactly that. And what a repository does when the hub is unreachable, since a vendored copy still works offline and a called one does not, which is the real cost being traded away. - Investigate replacing copy-pasted workflow content with cross-repo reuse, now that this repo is public. A public repository's composite actions and reusable workflows can be consumed by any other repository regardless of owner type, so the organization account this pattern was assumed to require is not needed, and the constraint that shaped the current vendor-everything model no longer holds. The catalog under [`catalog/snippets/workflows/`][workflows] is copied into each repo today, which means a fix to a shared job is a sweep across the fleet rather than one edit here, and it is the mechanism by which a defect in a snippet seeds itself into every repo that adopted it. Scope the investigation to which jobs are genuinely identical across repos against which only look similar, since a reusable workflow that needs a long input list to cover per-repo variation is worse than the copy it replaces. Settle the ref policy in the same pass, because consuming hub code at CI time is a floating dependency unless it is pinned, and [`GOVERNANCE.md`][governance] "Action pinning" requires a commit SHA for every action with one documented exception. Note that `uses:` does not accept expressions, so a per-branch ref cannot be selected in the workflow file and any branch-dependent behavior belongs inside the consumed action instead. ## Audit and Spec Model - Teach the audit to see content sitting in the wrong file, per [#523][issue-523], which the tree confirms is untouched, since [`spec/audit.py`][audit] still checks file presence, declared-section presence, verbatim hashes, and workflow interface conformance and nothing that reads a heading against a destination. The case that found it is a repo whose `.github/copilot-instructions.md` carried 311 lines under nine headings that [`spec/section-model.md`][section-model] assigns to `ARCHITECTURE.md` and `OPERATIONS.md`, reported as a missing-file letter while the misplacement that caused it was invisible. The shape that fits the engine is structural, collecting the level-two headings of an `intent` file, subtracting the ones [`spec/files.json`][files] declares for it, and comparing the remainder against the headings other destinations declare. The similarity-based version is explicitly rejected by `section-model.md`, and a detector built on it produces findings whose remedy is to delete content. Four things to settle, all in the issue: whether an undeclared heading is a finding at all given a repo may add locally, whether the destination mapping becomes declared data rather than prose, whether it reaches the advisory `ARCHITECTURE.md`, and how many repos are affected, which is measured before the check is designed rather than after it starts reporting. - State what a `driftNote` may contain at the moment one is written, not only what happens to it once the deviation clears, and widen the check that guards it. [`AUDIT.md`][audit-doc] section 8 says a note records a current deviation and is deleted once resolved, which is a rule about a note's end of life and leaves an agent authoring one with no rule about its content. PhotoCleaner is the worked example, in [#537][pr-537]: asked to self-audit and reconcile its notes, the agent wrote a completion record ("baseline onboarding completed"), a historical fact (the date a past version first published), and a description of the repo conforming to the README spec. All three are prose about the past or about compliance rather than a current deviation, and none of them is what the field is for. Read that as an incomplete instruction rather than an agent ignoring one, since nothing it was given forbids any of the three. Two things to settle. The rule wants stating positively at authoring time, that a note names a deviation that is true right now and would still be true if nobody read it, so history, completion, and conformance are each out by construction, and a repo doing exactly what the spec requires earns no note at all. And the detector needs the same widening, because [`spec/audit.py`][audit] currently flags a stale note by matching words that assert *outstanding* work ("pending", "not yet", "missing", "behind"), which is the opposite polarity to all three of PhotoCleaner's notes, and it only runs that check when the repo audits clean, which PhotoCleaner did not until the round that found this. A guard that fires only on one polarity, and only in the state where the problem has already gone, cannot see the failure it exists to catch. Note the limit before building it, since it is the same limit the section model already records: judging whether a sentence describes a current deviation is a semantic call, so the honest target is the shapes that are mechanically recognizable, a past-tense completion verb and an explicit date among them, with the rest left to the rule. -- Declare the canonical short description in [`registry/repos.json`][repos] instead of deriving it by parsing the README, which makes every check and every push deterministic. Today [`spec/audit.py`][audit] extracts the intro line after the H1 and compares the GitHub About panel and the Docker Hub short description against it, so the source of truth lives inside prose. That forces anything wanting the value to parse markdown, and PhotoCleaner#32 shows the cost, since a workflow step that reads the intro at publish time needs nine guards against headings, block quotes, all four list markers, ordered lists, HTML, tables, code, links and the length cap, and every one of them fails the release rather than the tagline. Adding a `description` field turns the README intro into a third mirror rather than the source, so the audit compares all three against one declared value, the Docker workflow pushes a literal with no parser, and `repo-config/configure.sh` can set the GitHub About panel from the same field it already sets every other repository setting from. The 100-character cap stays, since Docker Hub's short description is still the tightest surface. Add the field to `registry/repos.schema.json` as well, which sets `additionalProperties: false`, and make it optional at first so the audit falls back to the README intro while repos adopt it one at a time. The disposition is recorded on [#509][issue-509], and the ask on the Docker repos is only that the parsing step is not propagated further while the field is pending. +- Declare the canonical short description in [`registry/repos.json`][repos] instead of deriving it by parsing the README, which makes every check and every push deterministic. Today [`spec/audit.py`][audit] extracts the intro line after the H1 and compares the GitHub About panel and the Docker Hub short description against it, so the source of truth lives inside prose. That forces anything wanting the value to parse Markdown, and PhotoCleaner#32 shows the cost, since a workflow step that reads the intro at publish time needs nine guards against headings, block quotes, all four list markers, ordered lists, HTML, tables, code, links and the length cap, and every one of them fails the release rather than the tagline. Adding a `description` field turns the README intro into a third mirror rather than the source, so the audit compares all three against one declared value, the Docker workflow pushes a literal with no parser, and `repo-config/configure.sh` can set the GitHub About panel from the same field it already sets every other repository setting from. The 100-character cap stays, since Docker Hub's short description is still the tightest surface. Add the field to `registry/repos.schema.json` as well, which sets `additionalProperties: false`, and make it optional at first so the audit falls back to the README intro while repos adopt it one at a time. The disposition is recorded on [#509][issue-509], and the ask on the Docker repos is only that the parsing step is not propagated further while the field is pending. - Rework [`spec/readme-structure.md`][readme-structure] to match the hand-crafted PlexCleaner README, which is the shape the maintainer wants, and make the result auditable rather than advisory. Four concrete divergences are already identified, measured against PlexCleaner `README.md`, this repo's `README.md`, and the current spec. First, the distribution bullet is labeled by deliverable: PlexCleaner ships executables and calls the channel **Binary Releases**, while the spec fixes the label as **Versioned Releases** for every repo, so the label belongs in a per-channel table rather than as one string. Second, the license shield sits in the top **Build Status** block here and at the very bottom of PlexCleaner, inside a closing `## License` section that reads `Licensed under the [MIT License]` followed by the shield, immediately before the link definitions. Third, the Release Notes section closes with `See Release History for complete release notes and older versions.` in PlexCleaner against `See Release History for the full history.` here, and the PlexCleaner form is the wanted one. Note that PlexCleaner writes that link inline, which the reference-style rule forbids, so adopt the wording and keep the reference form. Fourth, the channel bullets and their shields vary by deliverable, meaning GitHub binaries, Docker Hub, NuGet, and PyPI each carry a different bullet label and a different shield set, which is what a per-type table has to encode for the `readme-structure` audit dimension to check a repo against its own declared types. - Decide whether the canonical README section order follows PlexCleaner, which is a separate question from the four divergences above and affects every repo plus the `readme-structure` audit. PlexCleaner places **Questions or Issues** immediately after the Table of Contents, where the spec orders it ninth, and it carries sections the spec names nowhere, including Performance Considerations, Runtime Metrics, Custom Plugins, Testing, Development Tooling, Feature Ideas, and Sample Media Files. Under the recurrence rule in [`spec/section-model.md`][section-model] those last ones are correctly repo-specific and stay undeclared, so the open question is only the position of the sections the spec already names. - Declare locally-required secrets the way GitHub-stored ones are already declared, and make a gitignored `secrets/` directory the fleet standard that holds them. [`spec/secrets.json`][secrets] covers only the Actions and Dependabot stores, so a repo that deploys somewhere has no declared way to say what it needs at runtime, and the required set is discoverable only by reading the deploy. The pattern already runs in the fleet in two shapes: HomeAutomation-Config keeps a gitignored secrets directory of env files and Docker secret files, and ESPHome-Config keeps a gitignored `secrets.yaml` beside a committed `_secrets.yaml`. The committed file carries the required names with dummy values, so the shape of the requirement is in git while the values never are, which is the same split the GitHub side already gets from `requiredSecrets[]`. Blog needs it immediately, since it deploys on the proxmox host through HomeAutomation-Config's Docker Compose stack and carries the copy destinations and the internal URI. The hub carries neither the directory nor a `.gitignore` entry for one today, so adopting it here comes first. Note that the **GitHub** side has the same missing axis, surfaced by the `hugo` type: a deploy's credentials are per-environment GitHub Environment secrets and variables, `stores` is a closed enum of `actions` and `dependabot`, and `spec/audit.py` seeds `required_by_store` with those two keys and indexes it unguarded, so adding an `environments` value to the enum raises `KeyError` for every repo whose `publish[]` maps to that mechanism. The type landed with an optional `environments` block added to [`spec/secrets.schema.json`][secrets-schema] and described in `spec/secrets.json`'s note, so a repo that needs it may declare its per-environment names and the shape is legal rather than an invention. The hub's own `spec/secrets.json` carries no such block, having no environments of its own, and no tool reads one where it exists. That is honest but it is not a gate, so a clean audit says nothing about whether an environment is configured. Solving it properly means a store vocabulary that does not crash on an unknown key and a per-environment secrets and variables fetch, and it is the same axis as the local half above rather than a separate problem. @@ -61,7 +61,7 @@ Everything here changes a file the fleet holds a copy of, so each entry costs a ## Host and Environment - State the rule that an agent works only in its own checkout, in its own directory, on its own feature branch, and never writes to a repository because it happened to be on disk. A clone left in a scratch directory is convenient exactly when it is most dangerous, since the operations that cross the boundary are the ordinary ones rather than the reckless ones. A blanket `git add -A` sweeps another agent's uncommitted work into your commit, a `git reset --hard` deletes it outright, and a branch switch carries it along into a change it has nothing to do with. This session produced the first of those: a blanket add swept a second agent's in-progress registration work into an unrelated commit and pushed it, and the work survived only because it was rescued and restored by hand afterwards. Care does not make a shared working tree safe, because every one of those commands is correct in isolation and wrong only because of who else is in the directory. Two things to settle when writing it. Whether the rule is stated as one checkout per agent or one checkout per task, since a single agent moving between two repositories in one directory hits the same hazard as two agents sharing one. And what an agent does when it finds a foreign checkout anyway, where the answer is to leave it alone and clone its own rather than to work carefully in it. -- Extend the same rule to **reading**, which is the more common case and the easier one to think is harmless. A clone on disk is not the branch it names. It is whatever that clone was last fetched to, so reading it to answer what a repository carries answers instead what that clone last saw. The failure is silent, because the read succeeds and returns a plausible answer. This session produced two instances. A detection pass over local clones reported one repository as still drifted on a file whose fix had already merged, because that clone's refs predated the merge, and the correction came from a fetch rather than from the read. A second pass then reported a repository as missing a file it does carry, because the local checkout sat on a branch behind the one being measured. State the working practice with the rule: read the live ref through the API where a claim will be acted on, or fetch immediately before reading, and name the ref and commit in any finding derived from a local read so a reader can tell what was actually measured. Note the exception worth keeping, which is that a local clone is the right tool for anything needing history or a build, since those are what an API read cannot give. +- Extend the same rule to **reading**, which is the more common case and the easier one to think is harmless. A clone on disk is not the branch it names. It is whatever that clone was last fetched to, so reading it to answer what a repository carries answers a different question, which is what that clone last saw. The failure is silent, because the read succeeds and returns a plausible answer. This session produced two instances. A detection pass over local clones reported one repository as still drifted on a file whose fix had already merged, because that clone's refs predated the merge, and the correction came from a fetch rather than from the read. A second pass then reported a repository as missing a file it does carry, because the local checkout sat on a branch behind the one being measured. State the working practice with the rule: read the live ref through the API where a claim will be acted on, or fetch immediately before reading, and name the ref and commit in any finding derived from a local read so a reader can tell what was actually measured. Note the exception worth keeping, which is that a local clone is the right tool for anything needing history or a build, since those are what an API read cannot give. - Finish the [#365][issue-365] rollout, which has two halves and one of them is invisible from here. Four machines are still unchecked, WSL2 Ubuntu, the MacBook Air, and both ThinkPads, plus any headless or cron environment that runs Claude Code with the token. A ticked row means the `CLAUDE.md` text and not the hook, since only running `host-setup/agent-safety/install.sh` deploys both layers, and the proxmox host proved that distinction the hard way by carrying the documentary half alone for eight days on the very machine where the incident originated. Note also the follow-up left open on this host and never applied, that the superseded `## Safety rules` section from [#364][issue-364] still sits above the canonical `agent-safety v1` block in `~/.claude/CLAUDE.md`, so the two overlap. Removing the superseded section is a judgment call on a per-machine file, which is why it was surfaced rather than applied. - Fill in the remaining cells of the [#483][issue-483] host tooling matrix, which is the reference that makes the per-machine chore above repeatable, so the two close together rather than either alone. Linux, Windows and WSL2 are filled in and verified. **macOS** needs someone on that platform, the **Proxmox** question is whether that host is a shell for agent work or also runs containers, which decides whether `docker` is required there at all, and the engine-inside-the-distro variant of the WSL2 `docker` cell is still unverified since that host only exercised the Docker Desktop integration. Honor the issue's own rule when filling a cell, that an unverified install command is worse than a blank, because a blank prompts a question while a wrong command produces a broken host and a false sense that setup succeeded. diff --git a/scripts/README.md b/scripts/README.md index dd555d238..42baca7da 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -36,25 +36,25 @@ The default rule set covers comment shape (`comment-wrap` and `comment-case`) al A wide scan skips the trees this repo generates rather than authors, currently `reports/`, which [`spec/audit.py`][audit] writes. A finding there is the audit engine's phrasing rather than an author's, so no edit to that tree can fix it, and leaving them in made the repo's own number mostly generated output. Naming such a path directly still reads it (`prose_lint.py reports`), so nothing becomes uncheckable. -In markdown an HTML comment carrying no sentence punctuation is treated as a structural marker rather than commentary, so it takes neither a capital nor a sentence split. The reference-link group headers, the ToC-omit directive, and the `agent-safety` install markers are each matched verbatim by a tool, so rewriting one to satisfy the rule breaks whatever reads it. A markdown comment that does punctuate a sentence is prose and is judged as prose. +In Markdown an HTML comment carrying no sentence punctuation is treated as a structural marker rather than commentary, so it takes neither a capital nor a sentence split. The reference-link group headers, the ToC-omit directive, and the `agent-safety` install markers are each matched verbatim by a tool, so rewriting one to satisfy the rule breaks whatever reads it. A Markdown comment that does punctuate a sentence is prose and is judged as prose. -The `spelling` rule covers the US English convention where cspell does not reach. That gate reads README and HISTORY only, deliberately, because gating every markdown file would mean endlessly padding `cspell.json` with technical terms, so a British spelling anywhere else in the tree had nothing checking it. The banned words are generated from stems rather than listed one by one, since an inflected spelling is as wrong as its base and a hand-listed family drifts as soon as one form is added without the others. Two words are deliberately absent: `analyses` is the US plural of `analysis` as much as it is a British verb form, and `cancelled` is a GitHub Actions job status rather than prose. +The `spelling` rule covers the US English convention where cspell does not reach. That gate reads README and HISTORY only, deliberately, because gating every Markdown file would mean endlessly padding `cspell.json` with technical terms, so a British spelling anywhere else in the tree had nothing checking it. The banned words are generated from stems rather than listed one by one, since an inflected spelling is as wrong as its base and a hand-listed family drifts as soon as one form is added without the others. Two words are deliberately absent: `analyses` is the US plural of `analysis` as much as it is a British verb form, and `cancelled` is a GitHub Actions job status rather than prose. -**Outside markdown `spelling` and `dupword` read the comments, not the source lines**, reusing the extraction the `comment-wrap` rule already does. An identifier, a string literal, or a lookup table is code, and judging it as prose would make this script report its own table of banned words. Each comment on a line is judged on its own rather than joined with its neighbors, because two comments are two sentences and joining them reads the second's opening word as a repeat of the first's last. +**Outside Markdown `spelling` and `dupword` read the comments, not the source lines**, reusing the extraction the `comment-wrap` rule already does. An identifier, a string literal, or a lookup table is code, and judging it as prose would make this script report its own table of banned words. Each comment on a line is judged on its own rather than joined with its neighbors, because two comments are two sentences and joining them reads the second's opening word as a repeat of the first's last. `dupword` gates CI, so its scope decides what a correct file is allowed to contain. A repeated token outside a comment is usually correct authoring rather than a typo: `class="gallery gallery-cols-1"` is the ordinary way two CSS class names share a prefix, and `rel`, `srcset`, `sizes` and the `data-*` attributes all take value lists of the same shape. There is no edit that satisfies the rule without changing the rendered page, so a blocking gate that reads those lines rejects correct work. The cost of the narrower scope is stated plainly rather than hidden: a duplicated word in HTML body text, or in a YAML or JSON string value, is no longer caught. Narrowing to the comment is preferred over exempting an attribute, since an exemption list covers only the attributes its author thought of. **Scope** is every text file git tracks, binaries skipped by a NUL-byte check, with no extension allowlist: an allowlist covers what its author thought of and silently stops covering whatever is added next, which is the same reason the line-endings rule already requires `git ls-files` over a raw `find`. `--list-files` prints the discovered set for auditing. -A double-quoted span in markdown is treated as a quotation and not scanned for prose rules, so a rule that states its own counter-example does not report the document that documents it. Outside markdown a double quote is structural, so the prose inside it still counts. +A double-quoted span in Markdown is treated as a quotation and not scanned for prose rules, so a rule that states its own counter-example does not report the document that documents it. Outside Markdown a double quote is structural, so the prose inside it still counts. The `semicolon` and `dash` rules ban a construction rather than a detectable subset of it, so each flags by default and the exceptions are the ones the rule names: a semicolon inside a list that already carries commas, and for the dash a compound word, a leading list marker, a range, and the `- **Label** - explanation` separator that opens a governed bullet. -**The semicolon rule reads the list where it lives.** The comma qualifies the list as a whole rather than one separator's position, so an enumeration whose commas fall in a later item keeps every semicolon it carries. Reading it positionally split one series in two, flagging the openers of the same list it then exempted the tail of, which would have restructured the enumerated guarantees the exemption exists to protect. A markdown table row is judged one cell at a time, since a row is a record of fields and a comma in one column cannot excuse a semicolon in another, and a bullet's `**Label**:` is dropped before the line is read, because it opens the bullet rather than announcing a list, the same construct the label dash is exempted for. What this misses is a sentence that reads as a list without being one: a colon early in a long line still excuses a splice later on it, which reading the diff catches. +**The semicolon rule reads the list where it lives.** The comma qualifies the list as a whole rather than one separator's position, so an enumeration whose commas fall in a later item keeps every semicolon it carries. Reading it positionally split one series in two, flagging the openers of the same list it then exempted the tail of, which would have restructured the enumerated guarantees the exemption exists to protect. A Markdown table row is judged one cell at a time, since a row is a record of fields and a comma in one column cannot excuse a semicolon in another, and a bullet's `**Label**:` is dropped before the line is read, because it opens the bullet rather than announcing a list, the same construct the label dash is exempted for. What this misses is a sentence that reads as a list without being one: a colon early in a long line still excuses a splice later on it, which reading the diff catches. **Both are markdown-only for now.** A shell script carries 78 statement separators that are not prose at all, so telling a comment from code is a precondition for reaching source files. Until then a semicolon or dash in a code comment is missed, which reading the diff by eye still catches. -The `comment-wrap` rule covers comments in every syntax the fleet's project types carry, not only the hash ones: `//` and `/* */` for C#, C, C++ and JSONC, `/* */` alone for CSS, `<!-- -->` for XML, csproj and markdown, `<# #>` for PowerShell, `;` for INI, and `#` for Python, shell, YAML and TOML. +The `comment-wrap` rule covers comments in every syntax the fleet's project types carry, not only the hash ones: `//` and `/* */` for C#, C, C++ and JSONC, `/* */` alone for CSS, `<!-- -->` for XML, csproj and Markdown, `<# #>` for PowerShell, `;` for INI, and `#` for Python, shell, YAML and TOML. JSON is treated as JSONC, because that is what ships: VS Code tasks, launch, devcontainer and workspace files all carry comments under a plain `.json` name. A marker inside a string literal is not a comment, so each line is scanned with quoted spans blanked first, and Python uses `tokenize` so a trailing comment is seen exactly. A documentation comment (`///`, `/**`, a docstring) is left to CODESTYLE, which permits the paragraphs this rule forbids. diff --git a/scripts/pr_review.py b/scripts/pr_review.py index 1d2e319c6..6d8832417 100644 --- a/scripts/pr_review.py +++ b/scripts/pr_review.py @@ -226,7 +226,7 @@ def suppressed_blocks(body: str) -> list[str]: """Return the review body's low-confidence sections, each sliced from its own heading. The section has worn three shapes so far: its own `<details>` wrapper, a bare heading in the - body, and a markdown heading nested inside the `Review details` wrapper. The wrapper is the + body, and a Markdown heading nested inside the `Review details` wrapper. The wrapper is the part that keeps moving, so each region is scanned line by line for the heading rather than read for a wrapper's summary: a nested heading is not a summary, and stripping the wrappers to look for it outside deletes the very region it sits in. That is the pair that reported diff --git a/scripts/prose_lint.py b/scripts/prose_lint.py index a158b5bbf..43e2aa6ac 100644 --- a/scripts/prose_lint.py +++ b/scripts/prose_lint.py @@ -613,7 +613,7 @@ def strip_quoted(s: str) -> str: def list_spans(s: str) -> list[str]: """Split a line into the spans that each hold their own list. - A markdown table row is a record of fields rather than one sentence, so judging the row whole + A Markdown table row is a record of fields rather than one sentence, so judging the row whole let a comma in one column excuse a semicolon in another. """ cells = s.strip().strip('|').split('|') if s.lstrip().startswith('|') else [s] @@ -821,7 +821,7 @@ def comment_wrap_findings(path: Path, raw: str, lines: list[str]) -> list[tuple[ if not body or NOT_PROSE.search(body) or BARE_URI.match(body.strip()): prev_body = '' continue - # An unpunctuated markdown HTML comment is a structural marker, not commentary. + # An unpunctuated Markdown HTML comment is a structural marker, not commentary. # It is a label, so it takes neither a capital nor a sentence split. # A tool matches each one verbatim, so rewriting it breaks whatever reads it. # Group headers, the ToC-omit directive, and the agent-safety markers are the cases. @@ -860,7 +860,7 @@ def check_file(path: Path, rules: set[str]) -> list[tuple[int, str, str]]: lines = raw.split('\n') if {'comment-wrap', 'comment-case'} & rules: out.extend(f for f in comment_wrap_findings(path, raw, lines) if f[1] in rules) - # Outside markdown the prose lives in the comments, and both rules judge prose, not code. + # Outside Markdown the prose lives in the comments, and both rules judge prose, not code. # A source line holds identifiers and literals, and an attribute value may legally repeat. # Reading it rejects correct work, `class="gallery gallery-cols-1"` being the reported case. comments: dict[int, list[str]] = {} diff --git a/scripts/test_pr_review.py b/scripts/test_pr_review.py index ab798071f..419cef3cf 100644 --- a/scripts/test_pr_review.py +++ b/scripts/test_pr_review.py @@ -46,10 +46,10 @@ def collapsed(heading: str = 'Comments suppressed due to low confidence (1)', def nested(heading: str = '### Suppressed comments (2)', finding: str = '**a.py:12**\n* The retry count is off by one.') -> str: - """The section as a markdown heading nested inside the `Review details` wrapper. + """The section as a Markdown heading nested inside the `Review details` wrapper. The live shape as of 2026-08-05: the section is no longer its own `<details>` wrapper with a - matching `<summary>`, it is a markdown heading inside the wrapper that also carries the + matching `<summary>`, it is a Markdown heading inside the wrapper that also carries the round's file and effort metadata, which trails the findings rather than preceding them. """ return ('### Ready to approve\n\nThe change is narrow.\n\n' @@ -380,7 +380,7 @@ def test_prose_that_merely_discusses_the_phrase_is_not_a_block(self) -> None: def test_a_heading_nested_inside_the_review_details_wrapper_reports(self) -> None: """The shape that reported `suppressed=0` over a body carrying two findings. - The reviewer moved the section inside the `Review details` wrapper as a markdown heading, + The reviewer moved the section inside the `Review details` wrapper as a Markdown heading, so the wrapper's summary reads `Review details` and matches nothing, and the fallback that exists for a moved wrapper scans the body with every wrapper deleted, which deletes the region the heading now sits in. The two misses compound into a clean round over findings diff --git a/scripts/test_prose_lint.py b/scripts/test_prose_lint.py index 9822f302a..54fefcdf2 100644 --- a/scripts/test_prose_lint.py +++ b/scripts/test_prose_lint.py @@ -169,10 +169,10 @@ def test_the_tiers_do_not_overlap(self) -> None: class TestDupword(BaitCase): - """A doubled word, read from markdown prose and from the comments of every other syntax. + """A doubled word, read from Markdown prose and from the comments of every other syntax. The scope matters more here than for the other prose rules, because this one gates CI. Outside - markdown a repeated token is far more often correct code than a typo: `class="gallery + Markdown a repeated token is far more often correct code than a typo: `class="gallery gallery-cols-1"` is the ordinary HTML idiom, and no edit fixes it without changing the page. """ @@ -620,7 +620,7 @@ def test_an_unpunctuated_markdown_marker_is_a_label_not_a_sentence(self) -> None # A punctuated HTML comment is commentary, so it stays judged as prose. self.assertEqual(['comment-wrap'], self.flag('a.md', 'Prose.\n\n<!-- One thing. Another thing. -->\n')) - # Outside markdown the carve-out does not apply, since there the marker case does not arise. + # Outside Markdown the carve-out does not apply, since there the marker case does not arise. self.assertEqual(['comment-case'], self.flag('a.py', '# lowercase opening\n')) def test_a_block_opener_inside_a_line_comment_is_text(self) -> None: @@ -1116,7 +1116,7 @@ def test_a_binary_file_is_not_scanned(self) -> None: class TestSentenceSplit(BaitCase): - """One sentence per line, the markdown counterpart of the comment-wrap rule.""" + """One sentence per line, the Markdown counterpart of the comment-wrap rule.""" def test_a_sentence_continuing_onto_the_next_line_is_flagged(self) -> None: self.assertEqual(['sentence-split'], @@ -1152,7 +1152,7 @@ def test_the_rule_is_markdown_only(self) -> None: class TestSpelling(BaitCase): - """US English, read from markdown prose and from the comments of every other syntax. + """US English, read from Markdown prose and from the comments of every other syntax. The rule runs on whatever file it is handed, README and HISTORY included. It complements the cspell gate rather than dividing the tree with it: cspell reads those two files and this reads diff --git a/spec/audit.py b/spec/audit.py index 03c707d38..9d0f5335f 100644 --- a/spec/audit.py +++ b/spec/audit.py @@ -4,7 +4,7 @@ Compares each cataloged registry repo against the ground truth in this repo - general settings (repo-config/settings.json), branch rulesets (normalized diff vs the model's payloads), secret names (spec/secrets.json; values are never read), baseline/per-type file presence and per-scope -markdown section presence on the ground-truth branch (spec/files.json, spec/scope-model.md), and +Markdown section presence on the ground-truth branch (spec/files.json, spec/scope-model.md), and branch-model facts (main/develop existence, develop behind main). Owner-initiated: run it when onboarding a repo, when drift is suspected, or before fleet-wide changes. Read-only - it never modifies a target. @@ -209,7 +209,7 @@ def extract_section(text, heading): def heading_texts(markdown): - """Lowercased heading texts in a markdown document, for case-insensitive section-presence matching.""" + """Lowercased heading texts in a Markdown document, for case-insensitive section-presence matching.""" return {m.group(1).strip().lower() for line in markdown.splitlines() for m in (_HEADING.match(line),) if m} @@ -662,7 +662,7 @@ def audit_repo(entry, spec, branch=None): # --- File and section presence on the ground-truth branch --- # appliesTo is matched against the repo's full selector set (types + workflowModel + releaseTrigger + # consumerModel), so the release/operational develop ruleset is two data entries, not a code swap. - # Required sections union across same-path entries. A carried markdown file must contain each heading + # Required sections union across same-path entries. A carried Markdown file must contain each heading # scoped to this repo. A rename reads as missing and equivalence is judged by hand, so a missing section # is DRIFT (a hint to verify), never a LETTER. sel = repo_selectors(entry, spec["registry"].get("defaults", {})) @@ -783,7 +783,7 @@ def audit_repo(entry, spec, branch=None): findings.append(("LETTER", "readme: no intro line after the H1 - the README opens with the title then a one-line description, which doubles as the About description (spec/readme-structure.md)")) else: if strip_md_links(intro_line) != intro_line: - findings.append(("LETTER", "readme: the intro line carries markdown links - keep it link-free plain text, it doubles as the repo About description (spec/readme-structure.md)")) + findings.append(("LETTER", "readme: the intro line carries Markdown links - keep it link-free plain text, it doubles as the repo About description (spec/readme-structure.md)")) want = strip_md_links(intro_line).strip() if len(want) > 100: findings.append(("LETTER", f"readme: the intro line is {len(want)} characters, over the 100-char limit (Docker Hub's short-description cap, the tightest surface it feeds) - tighten it to one short sentence (spec/readme-structure.md)")) @@ -995,7 +995,7 @@ def _selftest(): ok = False print(" FAIL description: strip_md_links behavior") else: - print(" ok description: markdown links reduce to their text, plain text passes through") + print(" ok description: Markdown links reduce to their text, plain text passes through") # cspell duplication: a workspace cSpell word list is detected, and a mere cspell.json mention is not. ws_dup = '{ "settings": { "cSpell.words": ["foo"] } }' ws_ok = '{ "settings": { "editor.rulers": [100] }, "note": "words live in cspell.json" }' diff --git a/spec/fidelity-model.md b/spec/fidelity-model.md index 8af3786c0..c76acb49b 100644 --- a/spec/fidelity-model.md +++ b/spec/fidelity-model.md @@ -10,9 +10,9 @@ Carried content is a class with virtual functions. The **fixed** part is the int Each [`spec/files.json`][files] entry declares one `fidelity`, defaulting to `presence`. -- **presence** - the unit exists (a file, or a markdown section heading). The audit's baseline check. +- **presence** - the unit exists (a file, or a Markdown section heading). The audit's baseline check. - **intent** - carried faithfully but judged by meaning, not bytes. A downstream copy legitimately differs (a governed divergence or a paraphrase), and equivalence is a human call via `intentRef`. The audit asserts nothing beyond presence. -- **verbatim** - byte-identical to the hub's canonical after line-ending, action-pin, and job-needs normalization. The audit content-hashes the downstream copy against canonical. It applies to a whole file, a workflow job region (a job selected by key), or a markdown section region (a `## heading` block selected by name). The section granularity lets one file be **intent overall while a few of its sections are verbatim** - a universal rule block stays byte-identical fleet-wide even though the rest of the document is a repo-adapted paraphrase, so a stale section or a missing rule is caught while its heading still passes the presence check. +- **verbatim** - byte-identical to the hub's canonical after line-ending, action-pin, and job-needs normalization. The audit content-hashes the downstream copy against canonical. It applies to a whole file, a workflow job region (a job selected by key), or a Markdown section region (a `## heading` block selected by name). The section granularity lets one file be **intent overall while a few of its sections are verbatim** - a universal rule block stays byte-identical fleet-wide even though the rest of the document is a repo-adapted paraphrase, so a stale section or a missing rule is caught while its heading still passes the presence check. - **interface** - an overridable body that must honor a named contract. The audit checks the contract by name and wiring, never the body. Fidelity is a declared field defaulting to `presence`, never inferred from `whole`/`placeholders`. `.editorconfig` and `.markdownlint-cli2.jsonc` are both whole with no placeholders yet sit at opposite fidelity, because the discriminator is governance, not field shape. diff --git a/spec/readme-structure.md b/spec/readme-structure.md index 3cc7f2146..43ad9ea47 100644 --- a/spec/readme-structure.md +++ b/spec/readme-structure.md @@ -4,7 +4,7 @@ The preferred `README.md` shape for a fleet project. The audit's `readme-structu ## Sections and Order -1. **Title (`# <Name>`)** - the H1 **is the repository name** (a hyphenated name may render its hyphens as spaces: `Financial-Modeling` -> `Financial Modeling`), then a one-line description as the next paragraph. That description is a **single sentence, link-free plain text, at most 100 characters**, and it is the one canonical short description. It doubles as the GitHub About description (GOVERNANCE.md "Repository Details") and, for a repo that publishes a Docker image, the Docker Hub short description. Both render no markdown, and Docker Hub caps the short description near 100 characters, the tightest surface, which sets the limit. The audit checks the H1 name, the length, the link-free form, and the mirrors. +1. **Title (`# <Name>`)** - the H1 **is the repository name** (a hyphenated name may render its hyphens as spaces: `Financial-Modeling` -> `Financial Modeling`), then a one-line description as the next paragraph. That description is a **single sentence, link-free plain text, at most 100 characters**, and it is the one canonical short description. It doubles as the GitHub About description (GOVERNANCE.md "Repository Details") and, for a repo that publishes a Docker image, the Docker Hub short description. Both render no Markdown, and Docker Hub caps the short description near 100 characters, the tightest surface, which sets the limit. The audit checks the H1 name, the length, the link-free form, and the mirrors. 2. **Build and Distribution (`##`)** - a bullet per distribution channel the project actually ships, each linking where it lives: **Source Code** (the GitHub repo), **Versioned Releases** (GitHub Releases), **Docker Images** (Docker Hub), **NuGet Packages** (NuGet.org), **PyPI Packages** (PyPI.org). List only the channels the project uses. It carries three sub-sections: - **Build Status (`###`)** - the CI/build status shields (release build, Docker build, last commit, last build). - **Releases (`###`)** - the version shields (GitHub release, GitHub pre-release, Docker latest/develop, NuGet, PyPI), one per channel the project publishes. diff --git a/spec/section-model.md b/spec/section-model.md index 63615f5f0..e416df8bb 100644 --- a/spec/section-model.md +++ b/spec/section-model.md @@ -76,7 +76,7 @@ A repo's own content is not carried, and the hub declares where it goes rather t `CODESTYLE.md` is carried by every repo already. `ARCHITECTURE.md` stays **declared but advisory**: every code repo has an architecture, and how much of it earns a separate document is contextual, so mandating it would produce empty files where the design needs none. Declared and required are separate questions, and only a universal need answers both. -`OPERATIONS.md` is agent-instruction content, so it takes the inline-link exception the markdown rules name, not the reference-style default. `ARCHITECTURE.md` is not on that closed list and follows the reference-style rule. +`OPERATIONS.md` is agent-instruction content, so it takes the inline-link exception the Markdown rules name, not the reference-style default. `ARCHITECTURE.md` is not on that closed list and follows the reference-style rule. ## Migrating a repo onto the split diff --git a/spec/validate.py b/spec/validate.py index 2afa79713..5495f08be 100644 --- a/spec/validate.py +++ b/spec/validate.py @@ -302,13 +302,13 @@ def check_selector(where, applies_to): errors.append(f"files.json: {path} section object missing a non-empty string 'name': {elt!r}") check_selector(f"{path} section '{elt.get('name', '?')}'", elt.get("appliesTo", "*")) # A section may carry its own fidelity (intent default, or verbatim for a universal rule block - # checked byte-for-byte). verbatim is meaningful only on a markdown file, where the heading + # checked byte-for-byte). verbatim is meaningful only on a Markdown file, where the heading # delimits the region. The hub's own file is the canonical, so no reference is needed. sfid = elt.get("fidelity", "intent") if sfid not in ("intent", "verbatim"): errors.append(f"files.json: {path} section '{elt.get('name', '?')}' fidelity '{sfid}' invalid (expected intent or verbatim)") elif sfid == "verbatim" and not path.endswith(".md"): - errors.append(f"files.json: {path} section '{elt.get('name', '?')}' is verbatim but {path} is not markdown (heading regions apply to .md only)") + errors.append(f"files.json: {path} section '{elt.get('name', '?')}' is verbatim but {path} is not Markdown (heading regions apply to .md only)") elif not isinstance(elt, str): errors.append(f"files.json: {path} section entry {elt!r} must be a string or object") From 213cd24e37e63773765f8f8d2c5844b899855ff2 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen <ptr727@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:13:58 -0700 Subject: [PATCH 2/2] Capitalize the two shapes the sweep's pattern skipped Both findings from the first review round are the same defect in the sweep rather than two: its pattern treated a following `-` or `.` as the mark of an identifier, so a hyphenated prose compound and a sentence-final use were read as code and left lowercase. `markdown-only` in `scripts/README.md` and `scripts/prose_lint.py`, and "uses the markdown." in `CODESTYLE.md` item 1, are the two the reviewer named. Re-scanning the tree for the same shape found three more the reviewer did not reach: `spec/audit.py`, `spec/fidelity_honesty.py`, and one assert string in `spec/project-types.json`. Item 5 now states the boundary, that a hyphenated compound in prose capitalizes, and says why, since the hyphen is exactly what a mechanical sweep misreads. Gates re-run clean: prose_lint gating rules, the script self-tests, `audit.py --selftest`, `repo_gate.py`, `spec/validate.py`, and markdownlint-cli2. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> --- CODESTYLE.md | 4 ++-- scripts/README.md | 2 +- scripts/prose_lint.py | 2 +- spec/audit.py | 2 +- spec/fidelity_honesty.py | 2 +- spec/project-types.json | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CODESTYLE.md b/CODESTYLE.md index 92b0950ac..2414a1eaf 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -33,11 +33,11 @@ Each language defines a **clean-compile** verification: the combination of build These apply repo-wide, in every directory: -1. **Markdown linting**: All `.md` files must be lint-clean (error and warning free) via the VS Code `markdownlint` extension. [`.markdownlint-cli2.jsonc`][markdownlint-cli2] at the repo root is the single source of truth, and the davidanson `markdownlint` extension and a command-line `markdownlint-cli2` run both read it, so the IDE and CLI stay in lock-step. Rules it deliberately disables (e.g. `MD013` line-length) are **intentional**, so do not "fix" them. `MD033` inline HTML stays **enabled**: HTML comments are permitted (markdownlint does not flag them), `details` and `summary` are allowed because a GitHub collapsible has no Markdown equivalent, every other element is flagged, and anything with a native Markdown equivalent uses the markdown. Fix violations at the source rather than disabling rules. +1. **Markdown linting**: All `.md` files must be lint-clean (error and warning free) via the VS Code `markdownlint` extension. [`.markdownlint-cli2.jsonc`][markdownlint-cli2] at the repo root is the single source of truth, and the davidanson `markdownlint` extension and a command-line `markdownlint-cli2` run both read it, so the IDE and CLI stay in lock-step. Rules it deliberately disables (e.g. `MD013` line-length) are **intentional**, so do not "fix" them. `MD033` inline HTML stays **enabled**: HTML comments are permitted (markdownlint does not flag them), `details` and `summary` are allowed because a GitHub collapsible has no Markdown equivalent, every other element is flagged, and anything with a native Markdown equivalent uses the Markdown. Fix violations at the source rather than disabling rules. 2. **Spelling**: All spelling must be clean via the CSpell VS Code integration, and words must be correctly spelled in **US English** (the repo-wide convention, per [GOVERNANCE.md][governance]). The shared `cspell.json` sets `"language": "en-US"` so British spellings are flagged, where a bare `"en"` accepts both US and British and silently passes the wrong spelling. Project-specific terms go in the shared `cspell.json` `words` list, the single source of truth the extension, CLI, and CI all read. The `.code-workspace` must **not** carry its own `cspell.words`/`cSpell.words` block, and when externalizing words into `cspell.json`, delete any word list left in the workspace (a leftover one duplicates the list and silently drifts). 3. **Spelling CI scope**: The enforced CI spell-check gate covers **`README.md` and `HISTORY.md` only**, because these are the files every repo visitor sees, so they must be clean. It is deliberately **not** all `**/*.md`: repos carry many Markdown files full of technical terms, and gating every one of them would mean endlessly padding `cspell.json` just to keep CI green. Broad, live spell-checking across any file (source, Markdown, text) is the **cspell editor extension's** job, so typos still surface to whoever is editing. A repo owner **may** widen their own CI file list, but README + HISTORY are the default; keep the CI workflow, the `Lint: Spelling` VS Code task, and the GOVERNANCE.md cspell one-liner on the same file list. The list is explicit (not a glob), so a repo that ships no `HISTORY.md` (e.g. one with no changelog) must drop it from all three surfaces and gate on `README.md` alone, since cspell errors on a listed file that does not exist. Markdown *linting* (item 1) stays repo-wide `**/*.md`, which does not choke on technical terms. 4. **`HISTORY.md` mirrors the README opening**: `HISTORY.md` is the maintainer-curated changelog and opens as the README's twin, carrying the same `# <Title>` (without the README's ToC-omit comment) and the same intro paragraph copied verbatim, then a `## Release History` section. The mirrored opening keeps the project identity consistent for a reader who lands on the changelog directly. The audit checks that the title and intro match the README, with HTML comments stripped. -5. **"Markdown" is the format's name**: The format is a proper noun, so prose capitalizes it, meaning a Markdown file, a Markdown link, and the Markdown a surface renders. Lowercase is for the strings a machine reads and for nothing else: a tool or package name (`markdownlint`, `markdownlint-cli2`, `yzhang.markdown-all-in-one`), a settings key (`markdown.extension.toc.levels`), a heading anchor (`#markdown-and-spelling`), an identifier in code, and a file extension. What this settles is the mix rather than either spelling, because a file carrying both gives the next author no default to follow and a reviewer a finding to raise on whichever one it wrote last. The rule lives here because every repo carries this file, so the convention arrives with it rather than being re-decided per repo. +5. **"Markdown" is the format's name**: The format is a proper noun, so prose capitalizes it, meaning a Markdown file, a Markdown link, and the Markdown a surface renders. Lowercase is for the strings a machine reads and for nothing else: a tool or package name (`markdownlint`, `markdownlint-cli2`, `yzhang.markdown-all-in-one`), a settings key (`markdown.extension.toc.levels`), a heading anchor (`#markdown-and-spelling`), an identifier in code, and a file extension. A hyphenated compound in prose is prose, so it capitalizes too (Markdown-only), which is the boundary a mechanical sweep gets wrong, since it reads the hyphen as the mark of an identifier. What this settles is the mix rather than either spelling, because a file carrying both gives the next author no default to follow and a reviewer a finding to raise on whichever one it wrote last. The rule lives here because every repo carries this file, so the convention arrives with it rather than being re-decided per repo. ## .NET diff --git a/scripts/README.md b/scripts/README.md index 42baca7da..7cc447f8c 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -52,7 +52,7 @@ The `semicolon` and `dash` rules ban a construction rather than a detectable sub **The semicolon rule reads the list where it lives.** The comma qualifies the list as a whole rather than one separator's position, so an enumeration whose commas fall in a later item keeps every semicolon it carries. Reading it positionally split one series in two, flagging the openers of the same list it then exempted the tail of, which would have restructured the enumerated guarantees the exemption exists to protect. A Markdown table row is judged one cell at a time, since a row is a record of fields and a comma in one column cannot excuse a semicolon in another, and a bullet's `**Label**:` is dropped before the line is read, because it opens the bullet rather than announcing a list, the same construct the label dash is exempted for. What this misses is a sentence that reads as a list without being one: a colon early in a long line still excuses a splice later on it, which reading the diff catches. -**Both are markdown-only for now.** A shell script carries 78 statement separators that are not prose at all, so telling a comment from code is a precondition for reaching source files. Until then a semicolon or dash in a code comment is missed, which reading the diff by eye still catches. +**Both are Markdown-only for now.** A shell script carries 78 statement separators that are not prose at all, so telling a comment from code is a precondition for reaching source files. Until then a semicolon or dash in a code comment is missed, which reading the diff by eye still catches. The `comment-wrap` rule covers comments in every syntax the fleet's project types carry, not only the hash ones: `//` and `/* */` for C#, C, C++ and JSONC, `/* */` alone for CSS, `<!-- -->` for XML, csproj and Markdown, `<# #>` for PowerShell, `;` for INI, and `#` for Python, shell, YAML and TOML. diff --git a/scripts/prose_lint.py b/scripts/prose_lint.py index 43e2aa6ac..d6aac293e 100644 --- a/scripts/prose_lint.py +++ b/scripts/prose_lint.py @@ -885,7 +885,7 @@ def check_file(path: Path, rules: set[str]) -> list[tuple[int, str, str]]: txt = strip_inline_code(line) prose = strip_quoted(txt) if path.suffix == '.md' else txt - # Both prose rules are markdown-only until a comment can be told from code. + # Both prose rules are Markdown-only until a comment can be told from code. # A shell script carries 78 statement separators that are not prose at all. if path.suffix == '.md': if 'semicolon' in rules: diff --git a/spec/audit.py b/spec/audit.py index 9d0f5335f..d0dd2ffec 100644 --- a/spec/audit.py +++ b/spec/audit.py @@ -719,7 +719,7 @@ def audit_repo(entry, spec, branch=None): findings.append(("DRIFT", f"verbatim: could not read {path} content on {ground} to compare (no inline content returned); verify by hand")) else: findings.extend(check_verbatim(path, text, item.get("reference") or path)) - # Heading-based presence is only meaningful for markdown. A "section" named on a non-md file (e.g. a + # Heading-based presence is only meaningful for Markdown. A "section" named on a non-md file (e.g. a # tasks.json task group) is an intent marker judged per AUDIT.md, not a heading grep. needed = wanted_sections[path] verbatim_needed = verbatim_secs[path] diff --git a/spec/fidelity_honesty.py b/spec/fidelity_honesty.py index 8cd21af75..d880ae1cf 100644 --- a/spec/fidelity_honesty.py +++ b/spec/fidelity_honesty.py @@ -179,7 +179,7 @@ def _fmt(repos): def render_report(spreads, promote, gaps, ledger): - """Join the live passes against the curated ledger into the checked-in burn-down markdown. + """Join the live passes against the curated ledger into the checked-in burn-down Markdown. A recorded disposition still matching a live divergence is a burn-down row. A live divergence (a verbatim hand-modification, or a manifest gap) with no disposition reads UNTRIAGED. A disposition is diff --git a/spec/project-types.json b/spec/project-types.json index 72df57f7f..bb08e32e4 100644 --- a/spec/project-types.json +++ b/spec/project-types.json @@ -114,7 +114,7 @@ "checks": [ { "id": "hugo.build.strict", "verdict": "letter", "assert": "The site build fails on a generator warning rather than rendering around it (hugo --gc --minify --panicOnWarning), and the pull request gate and the deploy run the same build command rather than two variants.", "workflowRef": "WORKFLOW.md#d1---pr-fast-feedback-smoke" }, { "id": "hugo.urls.parity", "verdict": "letter", "assert": "A URL contract gate compares the built tree against a committed list of the URLs that must render and the URLs that must redirect, and asserts a minimum length on each list before comparing it, since a truncated list makes every assertion below it pass vacuously. This is the type's check of record, standing in for the unit tests a site does not have.", "workflowRef": "WORKFLOW.md#6-per-project-type-test-walkthroughs" }, - { "id": "hugo.output.uncommitted", "verdict": "letter", "assert": "The rendered output is produced in CI only: its roots are gitignored and untracked, and they are excluded from the prose, spelling, and markdown gates along with any vendored third-party tree. A committed render is drift rather than a deliverable.", "intentRef": "GOVERNANCE.md#documentation-style-conventions" }, + { "id": "hugo.output.uncommitted", "verdict": "letter", "assert": "The rendered output is produced in CI only: its roots are gitignored and untracked, and they are excluded from the prose, spelling, and Markdown gates along with any vendored third-party tree. A committed render is drift rather than a deliverable.", "intentRef": "GOVERNANCE.md#documentation-style-conventions" }, { "id": "hugo.generator.pinned", "verdict": "letter", "assert": "The generator is pinned by exact version and by a checksum of the downloaded artifact, verified before install, never installed from a floating action or a latest tag, since the site is reproducible only if the generator is. The pin is declared once, and where two workflows need it something asserts the two copies agree.", "workflowRef": "WORKFLOW.md#d9---style--static-see-section-2" }, { "id": "hugo.vendored.provenance", "verdict": "letter", "assert": "A vendored third-party tree records the upstream repository and the exact ref or commit it was taken from, or is carried by a mechanism that pins it, so a bot or a tracker can move it. An unpinned copy with no recorded origin cannot be updated, diffed against upstream, or audited for a security fix.", "intentRef": "GOVERNANCE.md#release-model" }, { "id": "hugo.deploy.environment", "verdict": "letter", "assert": "The deploy job binds a GitHub Environment and takes every host-specific value and its credential from that environment, so the workflow file names no host, path, or address. A reusable callee re-asserts the environment name in a job of its own, because the environment binding resolves before any step runs and a workflow_call caller is not bound by the dispatch choice list a human sees.", "workflowRef": "WORKFLOW.md#d7---concurrency-permissions-safety" },