Uh oh!
There was an error while loading. Please reload this page.
fix(pm): declare check:doc-authoring's real population to dispatch-gates - #10662
Conversation
Three of the gate's four ROOTS were bare words the watch-hint extractor refuses as too generic, while its SKIP_PATHS carry separators and were taken. Five of the six paths it declared were therefore exclusions, and 383 of its 389 walked files were declared by nothing — so a card editing the live corpus derived an empty union and met this REQUIRED gate as red CI instead of as a local command. Declares each ROOT in the subtree spelling the extractor compares in, with both halves pinned: the gate's own self-test couples the declaration to ROOTS and bounds its over-claim to SKIP_PATHS, and the extractor's self-test pins that it reaches all four roots and claims nothing under packages/, apps/ or examples/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
Measured: deleting `.claude/**` from the gate leaves that case green, because `.claude` is a dotted top-level dir the extractor admits bare. The case still pins that the root stays reachable; the declaration is pinned in the gate's own self-test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
PM review — ACCEPT, arming when its five checks clear. ⭐ You answered the gate-vs-extractor question with a measurement instead of deferring to precedent.Head ⭐ The judgement, and why it is the right oneMy brief asked whether the sixth instance of this class should be fixed in the extractor rather than one gate at a time — five priors is an argument for that. You did not answer from precedent; you ran the tempting class-level guard (flag any gate naming a bare top-level tracked directory none of its accepted hints reaches) over all 123 discovered families:
And then the sentence that actually settles it:
⇒ The class is not mechanically closable from the extractor side, and the fix belongs in the gate. That is a real answer to a question I posed as open, backed by a number I could not have guessed. You also weighed the blast radius honestly (123 seats vs 1) and noted that widening ⭐ The card's headline was wrong, and you corrected it#10648 says "all four of its population roots are bare words… the only paths it declares are its exclusions." I repeated that in the brief. Only three of four were refused — Verified independently at if(!hint.includes('/')&&!plain.startsWith('.'))returnfalse;A bare hint is rejected unless it starts with a dot. So the declared set was five exclusions plus one real root, leaving 383 of 389 walked files (98.5%) declared by nothing. That is a less tidy headline than the card's and a more accurate one, and it is now in the code and the PR body rather than only in a report. The |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10648
The defect, measured
scripts/check-doc-authoring.mjsdeclared its population toscripts/pm/dispatch-gates.mjsalmost exactly inverted. Onorigin/mainat9dd192d48b, its entire extracted hint set was:Five of the six paths it declared were exclusions, and 383 of its 389
walked files (98.5%) were declared by nothing at all. So a card editing the
live corpus derived an empty union while this REQUIRED gate (
lint.yml:545,Doc/skill authoring guard) was genuinely reading the changed file.Before / after
All four roots now derive, each through its own declaration:
docs/qa/platform-checklist/RUNNER.mddocs/**docs/protocol-upgrade-guide.mddocs/**.claude/agents/os-dev.md.claudeskills/objectstack-upgrade/SKILL.mdskills/**content/docs/deployment/cli.mdxcontent/**Reject side, pinned in the same run — nothing outside the population derives it:
One thing in the card is false
The card (and the dispatch brief) state that all four roots are bare words
the extractor refuses. Measured, that is wrong for one of them:
.claudeis atop-level dotted directory, which
extractWatchHintsadmits explicitly(
/^\.(claude|changeset|github|gitattributes)\b/) and whichhintCoversdoesnot refuse (
!plain.startsWith('.')). It was live before this PR and reached.claude/**paths on its own.So the defect was three roots, not four — and the declared set held one real
root rather than "exactly the four subtrees it does not read". The cost figure
is unchanged and slightly worse than the card's framing in the direction that
matters: 383 undeclared files, not "the whole population", but the corpus that
was missing includes all of
docs/,skills/andcontent/.This has a consequence for the tests, recorded in the code: the
.claudereachcase in the extractor's self-test survives its own ablation (deleting
.claude/**leaves it green, because the bare.claudeliteral satisfies it).It is annotated as pinning reachability rather than the declaration — the
declaration for that root is pinned in the gate's own self-test instead. That is
the same trap the neighbouring
check-nul-bytescomment already records.Gate or extractor? — the judgement this card asked for
Fixed in the gate. Argued, not assumed:
Widening the extractor is already decided, with numbers.
hintCovers'docblock prices accepting bare top-level directory words at +139084
fabricated (gate, file) pairs ([finding] dispatch-gates never names
check:doc-anchorsfor acontent/**card — its population root'content'is not "pathy" #9626), and a narrower re-measure for bareroot files at 8 of 17 new pairs fabricated (finding: an AGENTS.md-only card derives ZERO gates — check:pm-skill-ratchet is locally undiscoverable for the file carrying its largest ceiling #9964). This card produces no
evidence that overturns either, so re-opening it would be relitigating a
measured decision from a worse position.
A class-level guard was measured here and refused. The tempting
generalisation — mechanically flag any gate naming a bare top-level tracked
directory that none of its accepted hints reaches — I ran over all 123
discovered families on this tree: 40 of 123 flag. The majority are
correct as they stand:
check:error-code-casingdeclaresSCAN_ROOTS = ['packages']and really does sweep the whole tree, and forcingit to declare
packages/**would name it on nearly every card in the repo —the drowning the genericity rule exists to prevent. The signal cannot
mechanically separate "population root" from "path component the gate joins
with something else", because that distinction lives in the author's intent
and not in the source text. That is precisely why the declaration has to be
authored.
Blast radius. The extractor's own header notes every seat derives its
gate family from this tool, so a change that re-classified even one family
moves the list every dispatch pastes. A gate-side declaration has blast
radius 1.
The measurement was not wasted: it isolates the sub-class where the fix really
is owed — doc-corpus gates whose whole population is a handful of top-level
trees. One more of those is live and is filed separately (below).
The⚠️ )
SKIP_PATHSquestion (the triage comment'sDecision: declare the ROOT, and pin the over-claim rather than hide it.
hintCoversis positive containment with no way to subtract, so"
docs/**exceptdocs/plans" is not expressible. The three exempt docssubtrees are therefore claimed by this declaration. Three reasons that is the
right trade, and one reason it costs nothing new:
SKIP_PATHSliterals themselves.docs/**subsumes those hints and addsnothing to the fabricated side while closing all 383 files of the missing one.
SKIP_PATHSspells those paths as module-body literals, so they stay hints whatever the
declaration says. Only unquoting them (the
DEFAULT_BASE_REFassembly trick)would, at the cost of obscuring this file's most safety-critical constant.
root is
docsand not its three live subdirectories so a new subdirectory iscovered on arrival; a hand-extended declaration is that same silent narrowing
one tool over. It would also strand the twelve hand-written top-level guides,
which are files rather than a subtree.
check:role-worddeclares
skills/**while skipping everyreferences/directory under it(12 tracked files), and
check:slot-lookup-ratchetdeclares the whole ofpackages/**. What precedent draws the line at is claiming a tree the rootsdo not reach — which is the direction pinned negatively here.
So the over-claim is bounded and asserted: the self-test requires every
SKIP_PATHSentry to sit under a declared root, and none to equal a root. Afuture exemption outside the declaration fails there instead of quietly widening
the claim.
Tests
Both halves pinned, and every new assertion ablated — mutation confirmed on
disk by anchor count in each direction, never by an editor's exit code (the
harness caught five of its own mutations as void when a
perldelimitercollided with the path separator, which is exactly the failure being guarded
against).
docs/**from the declarationpackages/**(not a ROOT)SKIP_PATHSentry outside every rootSKIP_PATHSentry equal to a ROOTdocs/**packages/**skills/**/content/**.claude/**Every leg restored to green afterwards, restoration confirmed by anchor count.
Verified at
c239b356:Gate union derived with
node scripts/pm/dispatch-gates.mjs, no pathspassed, at that commit — and cross-checked by hand rather than trusted, since
this PR's own subject is that the tool's silence is not a clearance. It named
check:cross-package-test-inputs,check:doc-authoring,check:pm-dispatch-gatesandscripts/check-cross-package-test-inputs.mjs;the hand cross-check added
check:nul-bytes,check:ratchet-remedy-authorityandcheck-declaration-mirrors.mjs, all runabove. Declared narrowing:
eslintwas not run locally — this worktree hasno
node_modulesand a fullpnpm installis the heavy shared operation thiscontainer rations; neither edited file appears in any eslint ratchet baseline,
and CI runs the lint farm regardless.
Changeset
scripts/**-only, publishes nothing →skip-changeset.Generated by Claude Code