Uh oh!
There was an error while loading. Please reload this page.
docs(ci): point the governed-merges step comment at the register, not a hand copy - #10005
Conversation
… a hand copy The `Governed-merges audit self-test` step comment restated the five governed surfaces (`docs/adr/**`, `.claude/**`, `skills/**`, `AGENTS.md`, `CLAUDE.md`) inline. Nothing read that copy, and the register it copies has grown three times in two days; the two hand copies a seat actually acts on went stale twice before `check:pm-governed-prose` pinned them. That pin asserts over backtick code spans inside an anchored region, which a bare YAML comment cannot present, so extending it here would mean recognising path-shaped literals in free text. Deleting the copy is the smaller surface and removes the drift structurally rather than detecting it: the comment now points at `GOVERNED_SURFACES` in `scripts/pm/check-governed-merges.mjs`, the way the sibling step comment already points at its own script, and says why the list is not restated so the next author does not helpfully re-add it. Comment-only: the parsed workflow is structurally identical to `origin/main` (same jobs, same `Lint & Repo Gates` name, same 70 steps, same `run:` lines). Part of #9840 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
PM review — ACCEPT. Ruling 1's "why it misses" turned out more useful than the copy itself. Arming.Verified at ⭐ Ruling 1 — not stale, and the miss is DECLARED rather than accidentalI told you to check #9841 first and warned that a hasty card against a moving repo is a claim about a past commit. It holds — and the reason is the better half:
Confirmed from this seat — the script's own header, lines 32-33: "over the region's backtick code spans only — never over bare prose." ⇒ the prose-pinning gate cannot see this copy by construction, and says so in its header. That is a designed boundary, not an oversight, and knowing which it is changes the fix: registering the region would have made containment red on correct text unless the paths were backticked first, while the over-claim half would still judge nothing. ⭐ H3 — DELETE, and the reasoning generalises
That is the right test for delete-vs-pin: not "is this list correct?" but "does the sentence need the list to make its point?" And the contrast you drew is what makes it a judgement rather than a preference — AGENTS.md Prime Directive #14 keeps its copy because at-a-glance readability is the point there. Same list, opposite disposition, for a stated reason. The sibling step comment directly above already names its script instead of restating it, so the file's own convention agrees. A pointer makes the drift structurally impossible; a pin only makes it detected. Preferring the first when the reader is served by it is the better outcome, and H3 existed precisely so you would not default to pinning because a gate happened to exist. ⭐ H1 — 6 total / 2 pinned / 3 free, and two of the free ones MUST stay free
This is the third time today a dev has drawn this line correctly, and it is worth naming as a standing principle: a quotation, a fixture, and a historical record are evidence — pinning them to the live value would falsify them. PR #9909 made the same call about ADR-0076 ("correcting it would falsify a correct historical record"), and PR #9990 about a self-test fixture corpus ("changing evidence to match prose"). A gate that pinned the maintainer's dated quote to today's table would rewrite what someone said. After this PR: unpinned, editable, non-quoted copies = zero. That is the number that matters, and "a third" was right about that population while undercounting the total. H4 — extension priced and REFUSED, correctly
Reported rather than built, as ruling 4 asked. A recognizer whose anchors sit in the most-edited file in the repo, wired into a required context with no paths filter, is a fleet-wide outage waiting for an unrelated refactor. Declining it with the price stated is the answer #9747's family calls for. ⭐ The structural proof — the right instrument for "comment-only" in a workflow
On a file where a stray edit is a fleet-wide outage, "I only touched a comment" is not evidence. A parsed-AST comparison is. Same discipline as PR #9944's And the declared narrowing is exemplary: Reverse verification with a non-red predictionDirection stated first — IDENTICAL, not red — because watch hints come from gate script sources with comments masked, so a path named in a YAML comment cannot become one. Byte-identical, prediction held, restore verified clean. Predicting "nothing moves" and then proving nothing moved is harder to fake than predicting a failure. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9840
The
Governed-merges audit self-teststep comment in.github/workflows/lint.ymlrestated the five governed surfaces (
docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md) inline, by hand. Nothing read that copy. Triage graded thecard direction 2 — delete the enumeration and point at the register — and that is
what this PR does.
Reconciliation against the prose pin (PR #9841, from #9525) — it does NOT cover this
Checked first, because a card filed against a moving repo is a claim about a past
commit.
scripts/pm/check-governed-prose.mjsis onmainand itsPROSE_SURFACESregister has exactly two rows:
AGENTS.mdand.claude/skills/pm-dispatch/SKILL.md..github/workflows/lint.ymlis not among them, so the gate never opens this file. Thecard is live, not stale.
The more useful half is why it misses, and the answer is not an oversight. Both
assertion halves operate over backtick code spans inside an anchored region; this
copy was bare, unquoted, comma-separated YAML prose. Registering it would have made
containment red on correct text (five spans it cannot see) unless the paths were
backticked first, and the over-claim half would still judge nothing. The gate's own
header states that boundary rather than implying it. So the pin's blind spot here is
declared, and the fix is to remove the copy rather than to widen the recogniser.
Why delete rather than pin
The comment's reader is someone scanning
lint.ymlto understand why this step runs theself-test only while the live sweep is report-only. That argument does not depend on
which five paths the sweep enumerates — the names were incidental colour, unlike
AGENTS.mdPrime Directive #14, where at-a-glance readability was the point. Thesibling step comment directly above already names its script (
scripts/pm/check-half-states.mjs)instead of restating what it does, so pointing is the established shape in this block.
Pinning instead (a third
PROSE_SURFACESrow) would have cost: backticking the five,plus two literal anchors inside a workflow file that churns heavily, in an
unconditional gate with no paths filter — an unrelated restructuring of this step
would turn
Lint & Repo Gatesred repo-wide. Detected drift is strictly worse thanimpossible drift when the copy has no reader to serve. The replacement comment also says
why the list is not there, so the next author does not helpfully re-add it.
Census of the governed-surface list across the tree
The card says "a third" copy. Measured over the whole tree, that is right about the
free-and-fixable population and undercounts the total statement count:
scripts/pm/check-governed-merges.mjs:274-280GOVERNED_SURFACESAGENTS.md:182(Prime Directive #14)PROSE_SURFACESrow 1.claude/skills/pm-dispatch/SKILL.md:496-497(ACCEPT path-fork)PROSE_SURFACESrow 2.github/workflows/lint.yml:287scripts/pm/check-governed-merges.mjs:78-81scripts/pm/check-governed-prose.mjs:252-2536 total / 2 pinned / 3 free, of which two must stay free: rewriting a dated verbatim
ruling to satisfy a gate is rewriting the ruling, and the self-test fixture is
independent on purpose —
verdict()takes the register as an argument precisely so thetest can vary it. After this PR the count of unpinned, editable, non-quoted copies is
zero.
Everything else that names these paths together is already a pointer, not a copy:
AGENTS.md:344,.claude/skills/pm-dispatch/SKILL.md:194and both PM lane files deferto Prime Directive #14 / the ACCEPT fork by name.
scripts/check-required-contexts.mjs:419names similar paths for its own scan set, and
scripts/pm/dispatch-gates.mjs:710cites a single register row; neither is an enumeration.
Latent, not live
The deleted copy named all five surfaces, correctly spelled, in register order — it
matched the register on the day it was removed. So the defect graded here is latent
(it rots on the next surface change), not an already-wrong statement. The register has
grown three times in two days, which is what makes a latent copy worth removing rather
than leaving.
Verification — comment-only, mechanically proven
1a8f380849.The parsed workflow is structurally identical to
origin/main: same jobs, sameLint & Repo Gatesjob name (the required context), same 70 steps inlint:, samerun:lines. No verdict, exit code or gate population changes.Gate union re-derived at
1a8f380849withnode scripts/pm/dispatch-gates.mjs(no patharguments — the script takes its own change set from the merge base):
check:node-versioncheck-node-version: OK (29 setup-node step(s) across 26 workflow(s), all on Node 22).check:required-contextscheck:shard-attestation✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).check:type-check-coveragecheck-type-check-coverage: OK — 64/77 workspace packages type-checked (plus the root)check:workflow-status-functionscheck-workflow-status-functions: OK (scanned 26 workflow file(s), 45 job(s) …)node scripts/check-shard-attestation.mjs✓ check-shard-attestation: 2 aggregate gate(s) …check:nul-bytescheck-nul-bytes: OK (scanned 6340 text file(s) … no raw ASCII control bytes).check:pm-governed-prose✓ check-governed-prose: 2 instruction surface(s) name all 5 registered governed surfaces … and claim no others.check:pm-governed-merges✓ check-governed-merges --self-test: 77 assertionsOne declared narrowing:
check:type-check-debtwas not run to completion. Its--self-testleg passes; its--re-measureleg refuses on an environmentprecondition — "55 workspace dependenc(ies) of the ledgered packages have no built type
entry point on disk … Build the closure first, exactly as lint.yml does before this
step" — in a fresh worktree with nothing built. That leg reads
dist/*.d.tsand theledgers, never this file. The only two facts the script derives from
lint.ymllive inobserved()(scripts/check-type-check-coverage.mjs:2240-2255) and are unchanged bythis diff, measured directly:
and the sibling leg that actually evaluates
observed()—check:type-check-coverage—is green above. CI builds the closure before this step and runs the farm regardless.
Reverse verification (no population change). Predicted direction: identical output,
because watch hints are extracted from gate script sources with comments masked, so a
path named in a YAML comment cannot become one. Restored
origin/main'slint.ymlintothe tree (the fix was committed first, so the restore point is a real commit), re-ran
node scripts/pm/dispatch-gates.mjs --tier scripts/pm/check-governed-merges.mjs— thefile this comment newly names — and diffed against the same command on this branch:
byte-identical, and
git status --porcelainclean aftergit checkout HEAD --.No changeset: a workflow comment publishes nothing.
skip-changesetapplied.⛔ Draft on purpose — do not flip ready, queue or arm auto-merge; the maintainer arms
after review.
Generated by Claude Code