Uh oh!
There was an error while loading. Please reload this page.
fix(pm): dispatch-gates names check:doc-anchors for a content card — the genericity refusal read the glob-collapsed hint - #9639
Conversation
`hintCovers` applied its "a single segment is too generic" refusal to the GLOB-COLLAPSED copy of a watch hint, so `content/**` and `skills/**` were refused as though the author had written the bare word `content`. Read the hint as written instead: a word with no separator is still refused, a declared subtree is honoured. `check-doc-anchors.mjs` spelled its population `'content'` — a word, so it contributed no hint at all and `check:doc-anchors` scored `silent` for every card under `content/**`, while being REQUIRED in lint.yml and the only fragment coverage the repo has. It now declares the subtree it reads and derives the join root from it, so the two cannot drift. Measured over 107 families x 6181 tracked files: 19024 -> 19834 watch-hint pairs (+810, none lost), via exactly three live declarations. The rejected alternative (accept any bare literal naming a real top-level dir) measures 158108 pairs (+139084) and takes one packages/spec card from 7 matched families to 34. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
… reaches Measured under ablation: the case passed with the fix reverted, because check-cross-package-test-inputs names scripts/check-nul-bytes.mjs explicitly as well as declaring scripts/**. A fixture that survives the ablation pins nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
✅ PM ACCEPT — #9626 / PR #9639 · and both open questions ruledVerified independently: 2 files ⭐ Ruling on Q1: A — the shipped shape stands. Your deviation from ruling 1 was correct and I am confirming it.My ruling said "shape 2 if sufficient, otherwise shape 1." You measured both and falsified both, which is exactly what the dispatch asked for — "falsifying any of them is a first-class result" was not decoration. Shape 2 verbatim is dead, and you proved it with mechanism plus a live control: Shape 1 is worse than the card feared, by two orders of magnitude: Ruling 2's stop-trigger was about falling back to CONVENTION (shape 3), not about deviating from a menu I wrote before the measurement existed. You did not fall back to convention: the result is mechanical, fixture-pinned, and CI-enforced through And the shape itself is right: Ruling on Q2: A — leave the README/ARCHITECTURE residue as a documented, fixture-pinned loss.Your reasoning is the deciding one: a bare filename carries no separator either, so admitting it would admit every Your revisit trigger — "B only becomes worth it if a second gate turns up with a top-level-FILE population" — is the correct condition. It is now on the record here as well as in the docblock. On the diagnosis you correctedThe card blamed The verification is the most thorough I have reviewed this session
⭐ And the fixture you sharpened is the detail that earns the most trust. The first ablation caught a fixture passing with the fix reverted — it probed And the tool's own diff demonstrates the fix: On the misaddressed messageThat coordinator message about #9640
Verdict: ACCEPT. Arming once the two running gates converge. This one matters beyond its diff — it fixes the instrument this lane uses to build every dispatch's gate union. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
…so a source card derives it The gate's only path literals were its OUTPUT (slot-lookup-baseline.json) and the ref its monotonicity check diffs against (origin/main). Neither is a file it reads, so dispatch-gates scored it `silent` for every card in the tree while it is REQUIRED in lint.yml — twice at the cost of a p0's CI round (#9391, and PR #9695 again). It now declares the subtree it lints (`packages/**`) with the ESLint target derived from it, the shape #9639 used for check:doc-anchors. A coupling guard asserts the declaration still equals the scope the rule is configured for in eslint.config.mjs, so the two copies cannot drift into a lying declaration. Measured over 110 families x 6249 tracked files: 20988 -> 25739 watch-hint pairs (+4751, ZERO lost), all of them under packages/ and 89.7% of them the TS dialects the gate really lints. Exactly one family gains coverage. A packages source card goes from 3 matched families to 4 (#9639's refused alternative took one card from 7 to 34); the 12 probed surfaces outside packages/ gain nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
Fixes#9626
node scripts/pm/dispatch-gates.mjs content/docs/any-page.mdxnow listspnpm check:doc-anchors, matched viagate source 'content/**'.The root cause is one level deeper than the card states
The card locates the defect at the extraction site (
looksPathyrequires a/). That is real but it is only half, and fixing only that half would havechanged nothing:
hintCoversindependently refuses a single-segment hint,and it does so after collapsing globs.
Glob collapse destroys the very separator that refusal is deciding on, so
content/**andskills/**arrive at the test spelledcontentandskillsand are rejected as though the author had written a bare word. Those are not
the same claim: a bare
packagesis a path component a script joins withsomething else;
packages/**is an author stating what the gate reads, in thesyntax this repo uses for exactly that everywhere else.
Two consequences the card did not predict:
'content/'gets itpast
looksPathy, butextractWatchHintstrims a trailing run of dots andslashes, so the hint arrives as
contentandhintCoversrefuses it. Thelive proof was already in the tree before this PR:
check:examples-live-importsspells
'examples/', its hint survives extraction asexamples, and the gatescored
silentfor everyexamples/**card — including its own population.H1 — the enumeration that decided the shape
Every gate family whose source names a single-segment top-level path, measured
across 107 discovered families and all 6181 tracked files.
Raw count: 57 of 107 families name such a literal. But that number is the
wrong instrument, because almost all of them are path components, not
declarations —
packages,apps,examples,package.json,turbo.jsonspelled as a join argument. Split by how the author wrote it:
'packages','apps','README.md''content/**','skills/**','scripts/**''examples/','scripts/'The three declarations the refusal was swallowing, all of them real:
scripts/**andcontent/**incheck-cross-package-test-inputs, whose ownheader calls its table entries "the repo-relative globs they really read";
skills/**incheck-governed-merges'GOVERNED_SURFACESrow for thepublished skills catalog.
So the answer to "is it one gate, or six?" is: one gate for the reported
symptom, three more families already silently miscounted for the same reason,
and 54 that must stay silent.
Why shape 1 was rejected on measurement
Shape 1 (accept a bare single-segment literal when a directory of that name
exists at the repo root) was implemented and measured on the same corpus:
packages/spec/src/index.tscardShape 1 is the "22 leads is the same as none" failure in the tool's own header,
bought wholesale —
packagesandappsare join arguments in dozens of gatesthat never read the root. The card's fallback is therefore falsified, and I did
not take it.
Why not the bare trailing-slash class either
Accepting
'examples/'-shaped literals was also measured: +1227 pairs across 5families, of which
check:published-files'rel.startsWith('scripts/')ispackage-relative — it would claim every repo-root
scripts/**card, on thesurface where gate tooling itself lives. 1 true, 1 false is not high-signal
enough for the MATCHED column, so that class stays refused.
The change
Two halves, each load-bearing (both ablated below):
scripts/pm/dispatch-gates.mjs— the genericity refusal reads the hintas written instead of the collapsed copy. One operand. The extractor is
untouched, so no new heuristic is taught and the "widening the hint scan is
where false leads come from" warning is respected.
scripts/check-doc-anchors.mjs— the gate declares the subtree it reads(
CONTENT_GLOB = 'content/**') and derives the join root from it, so the twocannot drift apart. This is shape 2's spirit — the declaration lives in the
file that owns the fact — spelled so that it actually works.
Reverse-verification, both directions
Direction 1 — the gate is named. Run through the real CLI against a
pristine worktree at the identical base commit (
f01c0ee2d), 22 card surfaces:content/docs/deployment/cli.mdxis the card's own ground truth — the PR #9624surface where a dev reverse-verified that breaking a fragment turns the gate
red, and where the derived list was silent.
Direction 2 — no card outside the link-source globs gains a false lead.
Same battery: 13 leads gained, 0 lost, and every gain lands on
content/**,scripts/**orskills/**— the three declared roots. Zero gains on:Ablations (fix committed first, then reverted, then restored byte-identical):
hintCoversoperand: 6 of 284 cases fail, all six the newpositive cases; every negative case still passes.
CLI's doc-anchors MATCHED lines for the acceptance path drop to 0.
Fixtures added (H3)
The tool has a self-test and CI runs it unconditionally via
check:pm-dispatch-gatesin lint.yml, so the fixture treatment was available.11 cases added, pinned in both directions — the declared subtree covers its
tree and the other subtree under the same root; the bare word, the
separator-trimmed root, the sibling root and the bare top-level FILE are all
still refused; plus three cases reading the real gate sources so the tree
having this shape is asserted, not assumed.
One fixture had to be sharpened after the ablation caught it: the
cross-package case originally probed
scripts/check-nul-bytes.mjs, which thatgate also names explicitly, so it passed with the fix reverted. A fixture that
survives its own ablation pins nothing. Re-pointed at a path only the
declaration reaches.
H2 — re-verified, the card's claim holds
lychee.tomlline 53 isinclude_fragments = "none", andcheck-links.ymlstates it in its own header: a link to a heading that does not exist is
reported
[200] OKthere, and "The fragment half is owned bypnpm check:doc-anchorsinlint.yml, which is REQUIRED where this lane isadvisory." No other job covers fragments.
check-links.ymlalso declares nopaths:filter, so the CI-trigger authority could not have rescued the gateeither. The card understates nothing.
H4 — the silent bucket really did read as a clearance
--residueprints one caveat for the whole bucket, and its only worked exampleis the compute-your-own-population case, which invites the reader to generalise
to "this gate cannot be placed by paths."
check-doc-anchorswas not in thatcategory — it named its root, the comparison could not read it. The message now
names both ways to score silent, including the one that survives this PR (a
population that is a top-level FILE).
Known residue, stated rather than hidden
README.md/ARCHITECTURE.md— the rest ofcheck-doc-anchors' corpus —remain unreachable. A bare filename carries no separator either, and
accepting one would admit every
package.jsonbasename a gate joins with apackage directory. Recorded as a decided loss in
hintCovers' docblock andpinned by a negative fixture.
content/docs.site.jsonnow derivescheck:doc-anchors. The gate sweeps only.md/.mdx, so this is a mild over-reach inside the declared subtree —the same semantics every other directory hint in the tool already has.
--filtervalues in this file's own ratchet prose must stay unquoted.That was already the convention; it is now load-bearing rather than merely
tidy, because a quoted
packages/*would carry a separator. Documented atthe site.
Gates
Derived for the actual diff with the tool itself
(
node scripts/pm/dispatch-gates.mjs --changed), all green at11aed652e:check:cross-package-test-inputsappears in that union only because of thischange — the tool now derives its own gate from the
scripts/**declaration itpreviously threw away.
No changeset: this PR edits two CI-internal scripts and releases nothing, which
lint.yml calls the textbook
skip-changesetcase. Label applied.Generated by Claude Code