From aa7bf78930f5c5fc41d574fe1620fb86ce515545 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 15 Aug 2026 10:57:32 -0700 Subject: [PATCH 1/2] Declare the Publisher and Validator Workflows at Intent Fidelity (#736) Follow-up to #732, which settled `.github/workflows/publish-release.yml` and `.github/workflows/validate-task.yml` as `accepted` gaps. That call was wrong and this reverses it. ## What was wrong `accepted` means "a path collision, the repo's own file", and its effect is to silence the finding and stop the audit measuring either file ever again. The fleet's intent for these two is the opposite: carry the hub's copy as closely as each repository's targets allow, so a diff against the canonical stays small and readable. The reasoning behind #732 tested both files against `interface` fidelity, found correctly that no named contract fits (job keys and triggers are genuinely per-repo), and then concluded that nothing could be declared. That skipped `intent`, which is exactly the tier for this: presence asserted, equivalence judged by hand against the `intentRef`, content never hashed, and a staleness advisory raised when the hub's canonical moves past a carrier's copy. The `appliesTo` claim was also wrong for the publisher. Re-measured: every repository lacking one carries `releaseTrigger: none` (AudioCleaner, HolidayLights, EspDinIoT, DevKitCIoT), and all 18 carriers carry a publishing trigger. That is an exact selector, and it states something true, namely that a repository which publishes a release has a publisher. ## What this does ```json { "path": ".github/workflows/validate-task.yml", "fidelity": "intent", "intentRef": "WORKFLOW.md#d1---pr-fast-feedback-smoke", "appliesTo": "*" }, { "path": ".github/workflows/publish-release.yml", "fidelity": "intent", "intentRef": "WORKFLOW.md#d4---release--publish", "appliesTo": ["two-phase", "dispatch-only", "publish-on-merge"] } ``` Both ledger rows are deleted, since a declared file is no longer a gap and `fidelity_honesty.py` drops such a row from the generated report by construction. The validator is declared fleet-wide **by decision rather than by measurement**: no selector separates its carriers from the repositories that inline validation into `test-pull-request.yml`, and the maintainer's call is that the reusable task is the fleet standard. The skill text already reads that way, naming `publish-release.yml` in the orchestration layer that is "the standardization baseline". ## Measured impact, whole fleet One full audit run at `audit run 2026-08-15T17:35:45Z | hub 0713008`: | | absent (letter) | staleness advisory (drift) | | --- | --- | --- | | `publish-release.yml` | 0 | 17 | | `validate-task.yml` | 8 | 13 | - The publisher's 0 absences confirm the selector is exact. - The validator's 8 absences are the convergence this asks for: homeassistant-purpleair, KiCadLibrary, EspDinIoT, ESPHome-Config, HomeAssistant-Config, DevKitCIoT, Vantage-Config, HolidayLights. - **17 of the 30 advisories are a false class**, and they are not this change's fault so much as this change's discovery: the intent advisory compares dates with no action-pin normalization, and the hub's publisher last changed in a Dependabot pin bump (`1892d6f`), so every carrier trips at once on drift the fidelity model already calls governed and per-repo. Filed as #735 with three options. - The other 13 are against `0d18078`, which added `ruff format --check`. Real content, though a repository that adopted the same step independently still reports as trailing, which is why the tier is an advisory and not a verdict. ## PlexCleaner report `reports/plexcleaner/audit.md` is updated in place, since it is the report this settlement was written into. Its two new advisories are read and retired in Drift Finding 6: the validator copy already runs `ruff format --check` at a pinned version, and the publisher advisory is the pin-bump class above. Verdict is unchanged at operational. Verified: `spec/validate.py`, `spec/audit.py --selftest` (which asserts no declared path leaks into the hub-only set), `scripts/repo_gate.py`, the whole-tree prose gate, markdownlint, and editorconfig-checker all pass. --- reports/divergences.md | 2 -- reports/plexcleaner/audit.md | 13 +++++++------ spec/divergences.json | 4 +--- spec/files.json | 2 ++ 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/reports/divergences.md b/reports/divergences.md index 65e77015..291e9cab 100644 --- a/reports/divergences.md +++ b/reports/divergences.md @@ -23,8 +23,6 @@ Generated by `python3 spec/fidelity_honesty.py --report` - do not hand-edit. Cur - **TODO.md** (manifest gap, carried by Blog, HomeAutomation-Config) - A repo's own running backlog, which STANDUP.md section 2 names as one of the four declared destinations for repo-specific content. It is advisory and required by no selector, so the manifest deliberately does not declare it and a repo's copy is its own content rather than a carry. - **.husky/pre-commit** (manifest gap, carried by AudioCleaner, LanguageTags, MediaTools, NxWitness, PhotoCleaner, PlexCleaner, Utilities) - A path collision rather than a carry. Each repo's hook runs that repo's own language formatters, and the hub's runs the doc gates, so the seven downstream copies share the path and none of the content: LanguageTags and Utilities run language formatting and style with no Docker, PlexCleaner runs CSharpier and dotnet format, and the hub runs prose_lint.py and repo_gate.py. Verified by reading the downstream copies on 2026-08-10. - **scripts/README.md** (manifest gap, carried by KiCadLibrary) - A path collision rather than a carry. KiCadLibrary's copy documents its own KiCad tooling (common.py, verify_library.py, build_library.py) beside the scripts it describes, and shares nothing with the hub's fleet-gate documentation. Verified by reading it on 2026-08-10. scripts/ is a generic path, so a repo with its own tooling directory matches this check without carrying anything of the hub's. -- **.github/workflows/publish-release.yml** (manifest gap, carried by Blog, ESPHome-Config, ESPHome-NonRoot, Financial-Modeling, HomeAssistant-Config, HomeAutomation-Config, KiCadLibrary, LanguageTags, MediaTools, NxWitness, PhotoCleaner, PlexCleaner, Utilities, VSCode-Server-DotNetCore, Vantage-Config, aiopurpleair, homeassistant-purpleair) - A path collision rather than a carry, settled 2026-08-15 by reading every cataloged repo's copy on its ground-truth branch (18 copies read, the hub's own included, so 17 downstream repos carry one and 4 carry none). Each repo's publisher is its own. The job keys are per-repo (publish in 15 of the 18, against create-release on homeassistant-purpleair, publish-pypi on aiopurpleair, and an eight-job orchestration on NxWitness), and the triggers run from dispatch-only through dispatch plus schedule plus a paths-filtered push. No interface contract the schema can express fits that spread, and no appliesTo selector separates the carriers from the four that carry none, since both source-only and csharp+console repos sit on either side. What the file must honor is declared elsewhere and needs no second entry: build-release-task.yml carries the interface contract for the release path, and WORKFLOW.md's D2.3, D4.1, and D7.1 are read by hand per AUDIT.md section 5. -- **.github/workflows/validate-task.yml** (manifest gap, carried by AudioCleaner, Blog, ESPHome-NonRoot, Financial-Modeling, HomeAutomation-Config, LanguageTags, MediaTools, NxWitness, PhotoCleaner, PlexCleaner, Utilities, VSCode-Server-DotNetCore, aiopurpleair) - As publish-release.yml, and settled in the same 2026-08-15 read (14 copies read, the hub's own included, so 13 downstream repos carry one and 8 carry none). Every carrier is on: workflow_call over a per-repo job list, in six distinct shapes across the 14 (lint alone on 3, unit-test plus lint on 5, validate alone on 2, lint plus test plus docs on 2, compile-test plus lint on 1, and unit-test plus lint plus codegen-drift on 1), so the only thing every copy shares is the trigger, which no contract field states. The 8 that carry none inline their validation instead, and no selector separates them, since source-only repos again sit on either side. The fixed part is the required check the aggregator reports, which test-pull-request.yml declares at interface fidelity and the ruleset binds by name. ## Untriaged - add a disposition to `spec/divergences.json` diff --git a/reports/plexcleaner/audit.md b/reports/plexcleaner/audit.md index a3a409ca..0d172e7c 100644 --- a/reports/plexcleaner/audit.md +++ b/reports/plexcleaner/audit.md @@ -5,7 +5,7 @@ - **Workflow model:** release - **Verdict:** operational - **Date:** 2026-08-15 -- **Run stamps:** `audit run 2026-08-15T15:13:26Z | hub d54862a` (the hub `develop` head, byte-identical to `main` at `0e84805`), re-run at `audit run 2026-08-15T15:30:07Z | hub d54862a` after the two gap dispositions below were settled +- **Run stamps:** `audit run 2026-08-15T15:13:26Z | hub d54862a` (the hub `develop` head, byte-identical to `main` at `0e84805`), and `audit run 2026-08-15T17:35:45Z | hub 0713008` after the two gap dispositions below were settled by declaring both files Supersedes the 2026-07-04 snapshot, which measured the repository before the `AGENTS.md` and `GOVERNANCE.md` split, before `repo-config/configure.sh` was retired, and before the README structure sweep, so it described drift that no longer exists. This report is measured rather than carried forward: every row below was re-read at `16f196e`, and each structural workflow divergence the old snapshot recorded was re-checked rather than assumed. Two of them are closed. @@ -17,7 +17,7 @@ The repository was resynced against the hub `main` at `0e84805` and promoted on ## Dimensions -The deterministic subset is `spec/audit.py` at the run stamps above. The first reports **0 defect, letter, or error findings** and two `hub-only` drift items, both of them the fleet-wide ledger question rather than this repository's. The second, after that question was settled, reports **clean**. The judgment rows are hand-read at `16f196e`. +The deterministic subset is `spec/audit.py` at the run stamps above, and both runs report **0 defect, letter, or error findings**. The first also raised two `hub-only` drift items, the fleet-wide ledger question rather than this repository's. The second, with both files now declared at `intent`, replaces them with two staleness advisories, and reading each against the hub retires both, see Drift Finding 6. The judgment rows are hand-read at `16f196e`. | Dimension | Letter | Intent | Verdict | Evidence (file:line) | | --- | --- | --- | --- | --- | @@ -26,7 +26,7 @@ The deterministic subset is `spec/audit.py` at the run stamps above. The first r | docker | pass | pass | pass | Registry layer cache keyed `buildcache-`, never `type=gha`, reading both branches and writing only the built one on push (`build-docker-task.yml:92-95`). The trimmed overview publishes from `Docker/README.md` on `main` only (`:105-113`). The image push is gated on `inputs.push` alone, independent of the release-create skip | | python | pass | pass | pass | Lint-only profile: `RegressionTests/pyproject.toml` carries `[tool.ruff]` and `[tool.mypy]` and no `[project]` table, so there is no lockfile to pin. The tools run through `uvx` at pinned versions, `uvx ruff@0.15.22 check .`, `format --check .`, and `uvx mypy@2.3.0 .` (`validate-task.yml:98-100`), which is `python.scripts.uvx` satisfied. `python.uvlock.pinned` and `python.coverage.codecov` are N/A for this subtree, as the registry records | | branch-model | pass | pass | pass | `repo-config/configure.sh check ptr727/PlexCleaner release` from the hub checkout at `d54862a` reports every ruleset rule and every general setting matching the carried payloads, with nothing to apply | -| carried-scope | pass | pass | pass | `repo-config/configure.sh` is retired from the tree, and no other hub-hosted path is carried. The two `investigate` hits are the fleet-wide question settled in this change rather than a repository finding, see Proposed Registry / Spec Updates | +| carried-scope | pass | pass | pass | `repo-config/configure.sh` is retired from the tree, and no other hub-hosted path is carried. The two `investigate` hits were the fleet-wide question settled in this change, and both files are now declared carries rather than gaps, so this repository's copies are measured instead of unclassified, see Proposed Registry / Spec Updates | | repo-setup | pass | pass | pass | Every name in `spec/secrets.json` is present in both stores with no forbidden name, per the mechanized run. `.github/dependabot.yml` declares `nuget` and `github-actions` against both `main` and `develop`, which covers every implied ecosystem (`docker` and `devcontainers` are not implied here). `host-tools.json` declares the .NET 10 SDK alone, with the media tools argued as runtime rather than host dependencies | | linter-parity | pass | pass | pass | One config per linter drives editor, CLI, and CI: csharpier and `dotnet format style --verify-no-changes` (`validate-task.yml:64-71`), markdownlint-cli2 and cspell (`:73-85`), ruff and mypy (`:91-100`), actionlint and editorconfig-checker (`:102-106`). The `.husky/pre-commit` hook runs the C# half natively and the Markdown half through already-present Docker images | | recurring-violations | drift | pass | drift | The tree passes the mechanized `eol` check and carries no charset violation in text written since the rules took effect, but a whole-tree run of the hub prose gate reports 772 findings and `repo_gate.py --check eol-coverage` reports one. Neither is swept in CI here, because this repository consumes no prose gate. See Drift Findings 1 and 2 | @@ -52,18 +52,19 @@ None. No applicable check fails both letter and intent. ## Drift Findings -1. **CI runs no prose gate, and the tree carries the backlog that follows.** The repository vendors no prose gate and consumes none, so nothing checks a new line against the fleet prose rules at any point (`validate-task.yml` runs no such step, and `.husky/pre-commit` runs markdownlint and cspell only). A whole-tree run of the hub's `scripts/prose_lint.py` reports 772 findings, most of them `comment-wrap` (342), `comment-case` (135), `semicolon` (120), and `dash` (82), concentrated in the C# sources and in text written before the rules existed. The fleet remedy is the diff-scoped gate rather than a tree-wide sweep, so consuming the hub's `prose-gate` action in `validate-task.yml`, as PhotoCleaner does, bounds the backlog at today's size and leaves the rest to the fix-as-edited policy. Two findings are substantive rather than stylistic and can be fixed on sight: `RegressionTests/RegressionTest.sh:75,361` carry the British `behaviour`, and `PlexCleaner/VideoProps.cs:138` duplicates `not`. -2. **`.gitattributes:29` pins `*.Dockerfile` to LF and no tracked file matches it.** `repo_gate.py --check eol-coverage` fails on exactly this. The repository consolidated on a single `Docker/Dockerfile`, which the `:28` pin already covers, so the `*.Dockerfile` line is either dropped or marked forward-declared the way the `uv.lock` block above it is. +1. **CI runs no prose gate, and the tree carries the backlog that follows.** The repository vendors no prose gate and consumes none, so nothing checks a new line against the fleet prose rules at any point (`validate-task.yml` runs no such step, and `.husky/pre-commit` runs markdownlint and cspell only). A whole-tree run of the hub's `scripts/prose_lint.py` reports 772 findings, most of them `comment-wrap` (342), `comment-case` (135), `semicolon` (120), and `dash` (82), concentrated in the C# sources and in text written before the rules existed. The fleet remedy is the diff-scoped gate rather than a tree-wide sweep, so consuming the hub's `prose-gate` action in `validate-task.yml`, as PhotoCleaner does, bounds the backlog at today's size and leaves the rest to the fix-as-edited policy. Filed as ptr727/PlexCleaner#925. Two findings are substantive rather than stylistic and can be fixed on sight: `RegressionTests/RegressionTest.sh:75,361` carry the British `behaviour`, and `PlexCleaner/VideoProps.cs:138` duplicates `not`. +2. **`.gitattributes:29` pins `*.Dockerfile` to LF and no tracked file matches it.** `repo_gate.py --check eol-coverage` fails on exactly this. The repository consolidated on a single `Docker/Dockerfile`, which the `:28` pin already covers, so the `*.Dockerfile` line is either dropped or marked forward-declared the way the `uv.lock` block above it is. Filed as ptr727/PlexCleaner#926. 3. **No `changes` paths-filter (D1.1, D1.4, D1.5).** Deliberate, and documented in the workflow header. It over-builds on a docs-only or workflow-only push and cannot under-build. 4. **The dispatch guard skips rather than failing fast (D2.3).** `publish-release.yml:33`. A dispatch from a feature branch is a green run that published nothing, which reads as a successful release. 5. **Two `GOVERNANCE.md` mentions name a path this tree no longer holds.** `GOVERNANCE.md:29` names `repo-config/configure.sh`, which is the hub-hosted pointer that rule requires rather than a stale mention, and is the false positive filed as #721 and fixed in #731. `GOVERNANCE.md:282` names `.vscode/settings.json`, which this repository genuinely deleted, so that one is a real stale mention for the file's next edit. +6. **Both `intent` staleness advisories on the newly declared workflows are false trails, read and retired here.** The advisory is date-based by design, a hint rather than proof, and `intent` equivalence is a human call. `validate-task.yml` last changed 2026-08-05 against a hub canonical that moved on 2026-08-14, but that hub change added `ruff format --check` and this copy already runs it, at a pinned version rather than `@latest` per its own convention (`validate-task.yml:99`). `publish-release.yml` last changed 2026-07-24 against a hub canonical that moved on 2026-08-08, and that hub change was a Dependabot action-pin bump, which is per-repo drift by definition. Neither copy trails the hub in substance. The publisher case is not this repository's at all: the advisory has no action-pin normalization, so a Dependabot bump in the hub marks every carrier at once, 17 of them in this run, filed as #735. ## Proposed Registry / Spec Updates - **Delete the branch-hygiene `driftNote`.** It reads "3 stale Dependabot nuget branches (PRs closed/superseded) and an unmerged feature/727-decouple-release-assets branch linger". The Dependabot branches are gone, and the live list now holds seven branches beyond `main` and `develop` rather than four, so the note describes neither the old state nor the new one. Branch hygiene is also not a baseline deviation any check scores, which is what a `driftNote` records, so it is deleted rather than re-snapshotted into prose that rots the same way. Applied in this change. - **The other two `driftNotes` stand.** `ARCHITECTURE.md` and `codecov.yml` are both carried beyond the baseline, and the csharp-plus-python shape note is accurate down to its details (the `RegressionTests/` subtree, no `uv.lock`, a `pyproject.toml` holding only ruff and mypy config, and the two N/A check ids). - **`status`, `types`, `publish`, `requiredSecrets`, `consumerModel`, `releaseTrigger`, and `configLayout` are unchanged and accurate.** -- **Settle both `investigate` gap dispositions as `accepted`.** Applied in this change, with the measurement written into the ledger reason. Neither `.github/workflows/publish-release.yml` nor `.github/workflows/validate-task.yml` can be declared. Reading every cataloged repository's copy on its ground-truth branch shows the publisher's job keys are per-repo (`publish` in 15 of the 18 copies read, against `create-release`, `publish-pypi`, and an eight-job orchestration elsewhere) and its triggers run from dispatch-only to dispatch plus schedule plus push, while `validate-task.yml` is uniformly `on: workflow_call` over a per-repo job list in six distinct shapes. The counts here include the hub's own copy, so 17 downstream repositories carry a publisher and 13 carry a validator, which is what the ledger's carrier lists name. No `interface` contract the schema can express fits either file, and no `appliesTo` selector separates the carriers from the non-carriers, since `source-only` repos sit on both sides of each. What governs them is already declared: `test-pull-request.yml` and `build-release-task.yml` carry the interface contracts, and WORKFLOW.md's D-guarantees are read by hand per AUDIT.md section 5. +- **Settle both `investigate` gap dispositions by declaring the files at `intent` fidelity.** Applied in this change, and it is a fleet decision rather than this repository's, taken by the maintainer on the measurement below. `spec/files.json` gains `.github/workflows/publish-release.yml` (`appliesTo` the three publishing `releaseTrigger` values) and `.github/workflows/validate-task.yml` (`appliesTo` `*`), and both ledger rows are deleted, since a declared file is no longer a gap. What the measurement settled is the fidelity rather than the question of carrying: reading every cataloged repository's copy on its ground-truth branch shows the publisher's job keys are per-repo (`publish` in 15 of the 18 copies read, against `create-release`, `publish-pypi`, and an eight-job orchestration elsewhere) and its triggers run from dispatch-only to dispatch plus schedule plus push, while `validate-task.yml` is uniformly `on: workflow_call` over a per-repo job list in six distinct shapes. No `interface` contract fits that spread, and `intent` is the tier that does: presence asserted, equivalence judged by hand against the `intentRef`, and a staleness advisory raised when the hub's copy moves past a repository's, which is what keeps a copy close to the canonical rather than merely present. The publisher's scope is exact, since every repository lacking one carries `releaseTrigger: none` and all 18 carriers carry a publishing trigger. The validator is declared fleet-wide by decision rather than by measurement, so the eight repositories that inline their validation into `test-pull-request.yml` now carry a presence finding, which is the convergence this asks for rather than noise. - **Set the `csharp` plus `python` row's Last audited to 2026-08-15** in `reports/conformance-matrix.md`. Applied in this change. ## Escalations diff --git a/spec/divergences.json b/spec/divergences.json index 3daf54f0..a39be82b 100644 --- a/spec/divergences.json +++ b/spec/divergences.json @@ -11,8 +11,6 @@ { "path": "TODO.md", "disposition": "accepted", "reason": "A repo's own running backlog, which STANDUP.md section 2 names as one of the four declared destinations for repo-specific content. It is advisory and required by no selector, so the manifest deliberately does not declare it and a repo's copy is its own content rather than a carry.", "tracking": null }, { "path": ".husky/pre-commit", "disposition": "accepted", "reason": "A path collision rather than a carry. Each repo's hook runs that repo's own language formatters, and the hub's runs the doc gates, so the seven downstream copies share the path and none of the content: LanguageTags and Utilities run language formatting and style with no Docker, PlexCleaner runs CSharpier and dotnet format, and the hub runs prose_lint.py and repo_gate.py. Verified by reading the downstream copies on 2026-08-10.", "tracking": null }, { "path": "scripts/README.md", "disposition": "accepted", "reason": "A path collision rather than a carry. KiCadLibrary's copy documents its own KiCad tooling (common.py, verify_library.py, build_library.py) beside the scripts it describes, and shares nothing with the hub's fleet-gate documentation. Verified by reading it on 2026-08-10. scripts/ is a generic path, so a repo with its own tooling directory matches this check without carrying anything of the hub's.", "tracking": null }, - { "path": "pyproject.toml", "disposition": "investigate", "reason": "The hub gained a config-only Scripts-profile pyproject.toml in #388. Decide whether to track it (intent, appliesTo python) after confirming the python repos carry an equivalent.", "tracking": null }, - { "path": ".github/workflows/publish-release.yml", "disposition": "accepted", "reason": "A path collision rather than a carry, settled 2026-08-15 by reading every cataloged repo's copy on its ground-truth branch (18 copies read, the hub's own included, so 17 downstream repos carry one and 4 carry none). Each repo's publisher is its own. The job keys are per-repo (publish in 15 of the 18, against create-release on homeassistant-purpleair, publish-pypi on aiopurpleair, and an eight-job orchestration on NxWitness), and the triggers run from dispatch-only through dispatch plus schedule plus a paths-filtered push. No interface contract the schema can express fits that spread, and no appliesTo selector separates the carriers from the four that carry none, since both source-only and csharp+console repos sit on either side. What the file must honor is declared elsewhere and needs no second entry: build-release-task.yml carries the interface contract for the release path, and WORKFLOW.md's D2.3, D4.1, and D7.1 are read by hand per AUDIT.md section 5.", "tracking": null }, - { "path": ".github/workflows/validate-task.yml", "disposition": "accepted", "reason": "As publish-release.yml, and settled in the same 2026-08-15 read (14 copies read, the hub's own included, so 13 downstream repos carry one and 8 carry none). Every carrier is on: workflow_call over a per-repo job list, in six distinct shapes across the 14 (lint alone on 3, unit-test plus lint on 5, validate alone on 2, lint plus test plus docs on 2, compile-test plus lint on 1, and unit-test plus lint plus codegen-drift on 1), so the only thing every copy shares is the trigger, which no contract field states. The 8 that carry none inline their validation instead, and no selector separates them, since source-only repos again sit on either side. The fixed part is the required check the aggregator reports, which test-pull-request.yml declares at interface fidelity and the ruleset binds by name.", "tracking": null } + { "path": "pyproject.toml", "disposition": "investigate", "reason": "The hub gained a config-only Scripts-profile pyproject.toml in #388. Decide whether to track it (intent, appliesTo python) after confirming the python repos carry an equivalent.", "tracking": null } ] } diff --git a/spec/files.json b/spec/files.json index 04933a04..46a61f67 100644 --- a/spec/files.json +++ b/spec/files.json @@ -28,6 +28,8 @@ { "path": "spec/secrets.json", "fidelity": "intent", "intentRef": "docs/repo-config-carry.md", "appliesTo": "*" }, { "path": ".github/dependabot.yml", "appliesTo": "*" }, { "path": ".github/workflows/test-pull-request.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["check-workflow-status"], "requiredCheckName": "Check pull request workflow status job" }, "intentRef": "GOVERNANCE.md#workflow-yaml-conventions", "appliesTo": "*" }, + { "path": ".github/workflows/validate-task.yml", "fidelity": "intent", "intentRef": "WORKFLOW.md#d1---pr-fast-feedback-smoke", "appliesTo": "*" }, + { "path": ".github/workflows/publish-release.yml", "fidelity": "intent", "intentRef": "WORKFLOW.md#d4---release--publish", "appliesTo": ["two-phase", "dispatch-only", "publish-on-merge"] }, { "path": ".github/workflows/merge-bot-pull-request.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["merge-dependabot", "disable-auto-merge-on-maintainer-push"] }, "intentRef": "WORKFLOW.md#d8---bots--automation", "appliesTo": "*" }, { "path": ".github/workflows/build-release-task.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["get-version", "validate-release", "github-release"], "artifactNameToken": "release-asset-", "requireTokensInJob": { "github-release": ["pattern:", "merge-multiple:"] }, "forbidTokensInJob": { "github-release": ["artifact-ids:"] }, "verbatimJobs": ["github-release"] }, "reference": "catalog/snippets/workflows/build-release-task.yml", "intentRef": "GOVERNANCE.md#release-model", "appliesTo": ["csharp", "console", "docker", "nuget", "pypi", "eda"] }, { "path": ".github/workflows/deploy-site.yml", "fidelity": "interface", "contract": { "requiredJobKeys": ["assert-ref", "validate", "deploy"], "requireTokensInJob": { "deploy": ["deploy-site-task.yml", "secrets: inherit"] } }, "reference": "catalog/snippets/workflows/deploy-site.yml", "intentRef": "WORKFLOW.md#d4---release--publish", "appliesTo": ["hugo"] }, From 7c67328ca98b813d111a4473aa850e551008bcaa Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 15 Aug 2026 12:04:19 -0700 Subject: [PATCH 2/2] Mark the Quoted Misspelling and Correct What AGENTS.md Carries (#740) Closes #737 Closes #738 Two wording defects in `.github/copilot-instructions.md`, both raised by Copilot against the carried copy on ptr727/ESPHome-Config#94. They ship together because they are the same failure: a carried file whose faithful copy makes every carrier re-litigate the same finding, which is exactly what a per-repository Disproved Claims ledger cannot absorb. ## #737, the unmarked quotation `GOVENANCE.md` is quoted as the spelling a review round actually emitted, and it is the evidence for the sentence it sits in: a partial round can name a path no diff carries. Nothing marked it as quoted, and it sits in the same sentence as a correctly spelled `GOVERNANCE.md`, so it reads as an obvious typo. Correcting the spelling would delete the evidence and invert the claim, since the sentence would then say a round named a path that does exist. Each Markdown occurrence now reads "the reviewer's own spelling and a path no diff carries", which is the marker the issue proposed and leaves the misspelling literal: - `.github/copilot-instructions.md`, the `Counts unequal` bullet - `scripts/README.md` - `TODO.md` `scripts/pr_review.py:778` and `scripts/tests/test_pr_review.py:1320` are deliberately unchanged, per the issue's own note that a Python string literal reads less like a path. The `pr_review.py` docstring also calls it "that typo" two sentences later, so the disambiguation is already there in the prose that matters. ## #738, what AGENTS.md actually carries The sentence read "`AGENTS.md` is the thin router and carries three byte-locked sections of its own, with no repository-specific ones". True of the sections, wrong about the file, and wrong in a way that inverts the sentence's purpose: it exists so a reviewer knows what is safe to flag, and it reads as "none of `AGENTS.md` is editable" when every `AGENTS.md` preamble is the repository's own prose. Reworded as the issue proposed, naming the three sections and stating that everything above the first is the repository's own and editable. Verified the three names against `spec/files.json`, which declares exactly `Fleet Bootstrap`, `Context and Delegation Discipline`, and `Where the Rules Live` as the verbatim sections, so the sentence is accurate in every carrier rather than only here. ## Downstream ESPHome-Config already applied the #738 reword locally, which `intent` fidelity permits, and recorded #737 as its first Disproved Claims entry whose "Delete when" names this change. Both retire on that repository's next resync. Verified: whole-tree prose gate, markdownlint on the three files, `repo_gate.py`, `spec/validate.py`, `build_dist.py --check`, editorconfig-checker, and the `test_pr_review` and `test_prose_lint` modules all pass. All three files keep their CRLF endings. --- .github/copilot-instructions.md | 4 ++-- TODO.md | 2 +- scripts/README.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9457c64b..05367564 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -18,7 +18,7 @@ Summarized for VS Code's generators. The full rules, rationale, and examples are ## Reviewing Carried Fleet Content -Several of this repository's governance files are carried from a shared template and kept in sync across a fleet of sibling repositories, among them `AGENTS.md`, `CODESTYLE.md`, `WORKFLOW.md`, this file, and the `repo-config/` rulesets. Most of `GOVERNANCE.md` is universal fleet law: every section that states a rule, as opposed to the two that describe this repository's own directory tree and devcontainer, is byte-locked and verified by an automated byte-for-byte match against the template canonical, not by line-by-line review. `AGENTS.md` is the thin router and carries three byte-locked sections of its own, with no repository-specific ones. +Several of this repository's governance files are carried from a shared template and kept in sync across a fleet of sibling repositories, among them `AGENTS.md`, `CODESTYLE.md`, `WORKFLOW.md`, this file, and the `repo-config/` rulesets. Most of `GOVERNANCE.md` is universal fleet law: every section that states a rule, as opposed to the two that describe this repository's own directory tree and devcontainer, is byte-locked and verified by an automated byte-for-byte match against the template canonical, not by line-by-line review. `AGENTS.md` is the thin router and carries three byte-locked sections of its own, `Fleet Bootstrap`, `Context and Delegation Discipline`, and `Where the Rules Live`. Everything above the first of those headings is this repository's own prose, saying what this repository is and which local doc owns which subject, and it is editable like any other repository content. Three constraints follow when reviewing that content. @@ -154,7 +154,7 @@ Copilot reviewed 2 out of 3 changed files in this pull request and generated no The sentence tail after the first spelling reports how many comments the round raised and appears in four wordings. It is not coverage, so it is not part of what has to be recognized, and the counts are. Read them into three verdicts and two exemptions: - **Counts equal** - the round read the whole diff. This is the clean pass. -- **Counts unequal** - files in the diff have no review at all. Do **not** treat a re-request as the remedy: measured over four pull requests and seven rounds on this repository, every partial round stayed partial at the identical ratio and no round ever recovered, so re-requesting spends a round and changes nothing. Splitting works where it applies and does not apply to a promotion, whose head is `develop`. **The file table in the body does not tell you which file went unread**, and it looks as though it should, which is why it is written down here: measured over 348 review bodies on this repository and 121 on another in this fleet, that table names the whole changed set on partial and fully covered rounds alike, so a table naming every changed file is what a full round carries too and contradicts nothing. One round of the seven is the exception, stating 16 of 17 and naming 16, omitting `GOVERNANCE.md`, and `status` names an omitted file only in that shape, where the table is short by exactly what the counts leave unread and names nothing outside the diff. Treat that as a lead to check rather than a verdict, one round here naming `GOVENANCE.md`, a path no diff carries. Report the state and hand the merge decision to the maintainer. +- **Counts unequal** - files in the diff have no review at all. Do **not** treat a re-request as the remedy: measured over four pull requests and seven rounds on this repository, every partial round stayed partial at the identical ratio and no round ever recovered, so re-requesting spends a round and changes nothing. Splitting works where it applies and does not apply to a promotion, whose head is `develop`. **The file table in the body does not tell you which file went unread**, and it looks as though it should, which is why it is written down here: measured over 348 review bodies on this repository and 121 on another in this fleet, that table names the whole changed set on partial and fully covered rounds alike, so a table naming every changed file is what a full round carries too and contradicts nothing. One round of the seven is the exception, stating 16 of 17 and naming 16, omitting `GOVERNANCE.md`, and `status` names an omitted file only in that shape, where the table is short by exactly what the counts leave unread and names nothing outside the diff. Treat that as a lead to check rather than a verdict, one round here naming `GOVENANCE.md`, the reviewer's own spelling and a path no diff carries. Report the state and hand the merge decision to the maintainer. - **Coverage-shaped and unreadable** - the remedy is to fix the reader, not to read past it. The vetted spellings live in `scripts/pr_review.py` and here, and they stay in step because a case reads them out of this file. - **Exempt: a body stating no coverage at all.** 28 of those 332 bodies are an overview and a change list and nothing more. That shape is current, interleaves with the counted one throughout, and one pull request carries both across its two rounds, so treating it as a failure cries wolf on about one review in twelve and teaches an agent to work around the gate. It reads as `coverage=unstated`, never as a pass and never as a failure. - **Exempt: a refusal.** It carries no coverage line by design, and the refusal rule above has already classified it. Read it here as well and every refusal grows a spurious second failure on top of the one that names its remedy. diff --git a/TODO.md b/TODO.md index c838bd41..61a03ba6 100644 --- a/TODO.md +++ b/TODO.md @@ -330,7 +330,7 @@ One pull request, after a survey, deciding whether anything stands between this - **Checked** - `develop` at `674a27a` on 2026-08-08, measured over 348 Copilot review bodies on the newest 120 pull requests here and 121 on the fleet's Blog repository, each read against the pull request's own changed-file list rather than against its counts alone. - **Settled** - The reviewer does name a file list, and the caveat saying otherwise was wrong. It is a `| File | Description |` table carried by 91 of the 348 bodies, and every table row in the corpus belongs to one of those tables. - **Settled** - The table names the unread file on exactly one round of the seven, which states 16 of 17 and names 16, omitting `GOVERNANCE.md`. That round is also the only evidence on record that the unread file is a real file rather than an artifact of counting. - - **Settled** - It cannot be read as coverage anywhere else. It names the whole changed set on partial and fully covered rounds alike, including all seven partials on Blog, while one round here states 61 of 62 and names 50, another states 33 of 33 and names 32, and a third names `GOVENANCE.md`, a path no diff carries. A reading identical under both outcomes discriminates neither. + - **Settled** - It cannot be read as coverage anywhere else. It names the whole changed set on partial and fully covered rounds alike, including all seven partials on Blog, while one round here states 61 of 62 and names 50, another states 33 of 33 and names 32, and a third names `GOVENANCE.md`, the reviewer's own spelling and a path no diff carries. A reading identical under both outcomes discriminates neither. - **Settled** - Three of the four partials here carry their table on the round before a push, describing the diff that push replaced, so the comparison is head-scoped like the counts and reports no table rather than a stale list of unreviewed files. - **Open** - Whether a partial round is worth escalating to GitHub at all. One named file on one round is a starting point rather than the pattern an escalation needs. - **Settled** - It is durable rather than flaky. Four pull requests and seven rounds (#476, #479, #592, and the #609 promotion), and **every later round repeated the identical ratio**. A re-request has never cleared one, so the remedy the digest first stated was wrong and now says so. diff --git a/scripts/README.md b/scripts/README.md index 4f136161..fc185c10 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -157,7 +157,7 @@ The reading fails closed, so a coverage-shaped line that parses to no counts is The line is matched at its **start** rather than anywhere in the body, since both spellings are structural: across those bodies every coverage statement opens its line, 272 with the reviewer's own name and 32 as the `Review details` bullet, and none sits mid-sentence. A body-wide match reports the pull request that adds this check as a partial round, which is the false positive the suppressed matcher and the refusal matcher have each had once already, and fenced blocks are dropped for the same reason, 131 of the bodies carrying one and this change putting both spellings into the diff a review of it quotes. The cost is named rather than hidden: a wording that moves the statement off the line start reads as no statement rather than as one this cannot parse. The reading is **head-scoped**, unlike a suppressed finding and like a refusal, because a partial round describes one commit's diff and the push that changes that diff raises a round reading the whole of the new one, and where one head carries two rounds through a re-request the worst of them reports, since the one naming files it did not read is the one to answer. A case reads the vetted spellings out of the runbook and hands them to this script's own parser, so the pair stays in step in both directions. -A partial round is reported and handed over rather than retried into. Measured over four pull requests and seven rounds, **every partial stayed partial at the identical ratio and no round ever recovered**, and the diff size does not predict it: fully covered pull requests here run to 33 files and 2,219 changed lines, well past every partial. The stated denominator matches the API's own changed-file count on 103 of 104 pull requests, so the reviewer counts the file and does not read it. Which file that is usually cannot be recovered, and the file table many bodies carry is why it is worth saying "usually" rather than "never": measured over 348 review bodies here and 121 on another repository in this fleet, that table names the whole changed set on partial and fully covered rounds alike, so a table naming every file is exactly what a fully covered round carries too and separates a miscount from a file nobody read exactly never. **One round of the seven is the exception**, stating 16 of 17 and naming 16, omitting `GOVERNANCE.md`, and it is the only evidence on record that the unread file is a real file rather than an artifact of counting. `status` therefore prints what the table says beside the counts and takes no verdict from it. It names the omitted file only where the table is short by exactly what the counts leave unread **and** names nothing outside the diff, one round here naming `GOVENANCE.md`, a path no diff carries, which is enough of a reminder that the table is prose the reviewer writes rather than a list from the API. The comparison is head-scoped like the counts are, so a table from before a push describes the diff that push replaced and reports as no table rather than as a list of unreviewed files, which is the shape three of the four partials here carry. `wait` and `status` therefore report `42` as a state for the maintainer rather than one to re-request into. +A partial round is reported and handed over rather than retried into. Measured over four pull requests and seven rounds, **every partial stayed partial at the identical ratio and no round ever recovered**, and the diff size does not predict it: fully covered pull requests here run to 33 files and 2,219 changed lines, well past every partial. The stated denominator matches the API's own changed-file count on 103 of 104 pull requests, so the reviewer counts the file and does not read it. Which file that is usually cannot be recovered, and the file table many bodies carry is why it is worth saying "usually" rather than "never": measured over 348 review bodies here and 121 on another repository in this fleet, that table names the whole changed set on partial and fully covered rounds alike, so a table naming every file is exactly what a fully covered round carries too and separates a miscount from a file nobody read exactly never. **One round of the seven is the exception**, stating 16 of 17 and naming 16, omitting `GOVERNANCE.md`, and it is the only evidence on record that the unread file is a real file rather than an artifact of counting. `status` therefore prints what the table says beside the counts and takes no verdict from it. It names the omitted file only where the table is short by exactly what the counts leave unread **and** names nothing outside the diff, one round here naming `GOVENANCE.md`, the reviewer's own spelling and a path no diff carries, which is enough of a reminder that the table is prose the reviewer writes rather than a list from the API. The comparison is head-scoped like the counts are, so a table from before a push describes the diff that push replaced and reports as no table rather than as a list of unreviewed files, which is the shape three of the four partials here carry. `wait` and `status` therefore report `42` as a state for the maintainer rather than one to re-request into. `status` and `wait` both exit `43` where the reviewer sent a **shape this script has no reader for**, and that outcome outranks every other reading here, because a reader that does not understand the output cannot be believed about what it found in it. Every rule in this script keys on a structural marker, so a marker that changes spelling is a section the reader stops finding and reports as absent. All three failures on record have that exact shape: the suppressed heading reworded and the count went to zero, the suppressed section moved inside another wrapper and the count went to zero again, and the coverage line was never parsed at all. Each reported a clean pass over a review it had misread, and each was found by the maintainer afterwards rather than by the gate. The digest carries `shapes=UNRECOGNIZED`, lists each shape it could not place, and names the remedy in two parts: **file an issue on the hub repository that hosts this script**, quoting the body the shape came from, and leave the merge decision to the maintainer, since an unrecognized shape does not say the pull request is bad, only that nothing here can vouch for the review of it.