Repository navigation
Description
Noticed while deriving the local gate list for #9430 (a content/docs/deployment/cli.mdx edit, PR #9624).
What happens
node scripts/pm/dispatch-gates.mjs content/docs/deployment/cli.mdx places pnpm check:doc-anchors in the silent bucket:
- pnpm check:doc-anchors [lint.yml] names: scripts/check-doc-anchors.mjs
That gate is required in lint.yml and it is the one gate a docs edit that adds a link most needs — it resolves every internal #fragment against the real Fumadocs heading ids, and check-links.yml explicitly does not do the fragment half (include_fragments = "none"). On this card it mattered in practice: the diff added three fragment links, the derived list named the gate nowhere, and running it anyway was a judgment call rather than a derivation. Reverse-verified on that PR: breaking one of the new fragments turns the gate red, so it does read content/** links.
Why the derivation misses it
scripts/pm/dispatch-gates.mjs decides what counts as a path hint at the extraction site:
const looksPathy = s.includes('/') || /^\.(claude|changeset|github|gitattributes)\b/.test(s);A quoted literal with no / is not pathy. scripts/check-doc-anchors.mjs spells its population root as exactly that:
/** The Fumadocs content root — what `/` means in a site route, and what the ... */
const CONTENT_ROOT = 'content';So the gate's real input surface contributes no hint, the only surviving literal is the script's own filename, and the family scores silent for every card under content/**.
This is narrower than the caveat the script already prints about gates that compute their own population: check-doc-anchors does name its root as a string literal — the extractor just does not recognise a single-segment directory as a path.
Shapes, no recommendation
- Widen the extractor: treat a single-segment literal as a hint when a directory of that name exists at the repo root. Cheap; the header's own measurement history warns that widening the hint scan is where false leads come from, so it wants the same fixture treatment the existing rules got.
- Leave the extractor alone and let the gate declare its surface — e.g. spell the root
'content/'so it is pathy, in the file that owns it. - Do nothing mechanical and let docs cards carry the gate by convention, accepting that the derived list is silent on it.
Whichever way, the check would be that dispatch-gates.mjs content/docs/any-page.mdx lists check:doc-anchors, and that no card outside content/** and the other link-source globs gains a false lead.
Filed unassigned, no pm:queue — an observation, triage's call.
Generated by Claude Code
Generated by Claude Code
Activity
os-steve commentedon Aug 18, 2026
Claiming this card.
session_01XqDQYVU5smx29ts9pAErjaclaude/issue-9626-dispatch-gates-content-root../objectstack-9626Plan: measure H1 first (enumerate every gate that lands in the silent bucket because its population root is a single-segment top-level directory) — that measurement decides shape 1 vs shape 2 per the dispatch ruling.
Generated by Claude Code
Generated by Claude Code
os-steve commentedon Aug 18, 2026
{ "issue": 9626, "status": "done", "branch": "claude/issue-9626-dispatch-gates-content-root", "pr": "https://github.com/objectstack-ai/objectstack/pull/9639", "premise_still_valid": true, "summary": "The card's symptom is real and reproduced: check:doc-anchors scored `silent` for every content/** card. Its diagnosis of the mechanism is incomplete, and that changed the fix. The extraction site (looksPathy) is only half; hintCovers independently refuses a single-segment hint AFTER collapsing globs, so it rejected `content/**` and `skills/**` as though the author had written a bare word. Consequence: shape 2 verbatim ('content/') does NOT work, because extractWatchHints trims a trailing run of dots and slashes and the hint arrives as `content` anyway. Live proof already in the tree: check:examples-live-imports spells 'examples/' and scored silent for its own population. Shape 1 was implemented and measured, and is far worse than the card feared: +139084 matched pairs, one packages/spec card going 7 -> 34 families. So I took neither verbatim. The fix is shape 2's spirit made to work: hintCovers reads the hint AS WRITTEN (one operand, extractor untouched, no new heuristic), and check-doc-anchors declares the subtree it reads (CONTENT_GLOB = 'content/**') with the join root derived from it so the two cannot drift. DEVIATION FROM RULING 1, flagged deliberately: measurement falsified the stated fallback, so I did not fall to shape 1 — nor to shape 3, which stays rejected; the result is mechanical, self-tested and CI-enforced. SEPARATELY: a coordinator message arrived mid-task addressed to issue #9544 / branch claude/issue-9544-published-readme-symbols (published-readme-exports baseline, plugin-audit READMEs). That is another agent's claimed surface and none of it is mine, so I recorded it rather than acting on it — the #9544 dev may never have received it.", "tests": "All green at HEAD 11aed652e (the final commit; union re-run on that tree). Gate union DERIVED for the actual diff with the tool itself (node scripts/pm/dispatch-gates.mjs --changed), not recalled: `pnpm check:pm-dispatch-gates` -> 284 cases pass; `pnpm check:doc-anchors` -> 247 fragment links across 397 sources resolve; `pnpm check:cross-package-test-inputs` -> 33 self-test cases, 12 packages declared, turbo.json hashes every declared glob; `pnpm check:nul-bytes` -> 6176 text files, no raw control bytes. Plus a grep for control chars over both edited files: clean. Note check:cross-package-test-inputs entered that union only BECAUSE of this fix (via the scripts/** declaration the tool used to discard) — the tool's own diff demonstrates the change. ACCEPTANCE TEST (the card's own): `dispatch-gates.mjs content/docs/any-page.mdx` now prints `pnpm check:doc-anchors [lint.yml] matched via content/docs/any-page.mdx -> gate source 'content/**'`. REVERSE-VERIFICATION, both directions, run through the real CLI against a pristine worktree at the identical base commit f01c0ee2d over 22 card surfaces: 13 leads GAINED, 0 lost; every gain lands on content/**, scripts/** or skills/** (the three declared roots); zero gains on packages/*, apps/*, examples/*, docs/adr, .changeset, .github/workflows, .claude/skills, docker, package.json, turbo.json, pnpm-workspace.yaml, README.md, ARCHITECTURE.md. Direction 1 includes the card's ground truth content/docs/deployment/cli.mdx (PR #9624): base 9 -> fix 10 gates, the gained one being check:doc-anchors. ABLATIONS (fix committed first, reverted, then restored and verified byte-identical with cmp): reverting the hintCovers operand fails 6 of 284 cases, all six the new positives, every negative still passing; reverting the gate's declaration to the bare word fails 1 of 284 and drops the acceptance path's doc-anchors MATCHED lines to 0. One fixture had to be sharpened because the first ablation caught it passing with the fix reverted (it probed scripts/check-nul-bytes.mjs, which that gate also names explicitly) — re-pointed at a path only the declaration reaches, then re-ablated to 6/284.", "open_questions": [ { "question": "Ruling 1 said 'shape 2 if sufficient, otherwise shape 1'. Measurement falsified BOTH verbatim, so I shipped a refinement instead of escalating under ruling 2. Confirm that reading, or tell me to stop and re-open.", "options": [ "A. Accept the shipped shape: hintCovers reads the hint as written (+810 pairs, 0 lost, 3 families, all documented declarations) plus the gate declaring 'content/**'", "B. Treat 'neither verbatim shape is safe' as ruling 2's stop-and-report trigger and revert to a decision request", "C. Take shape 1 anyway" ], "recommendation": "A. Ruling 2's concern is falling back to CONVENTION (shape 3), which I did not do — the result is mechanical, fixture-pinned and CI-enforced via check:pm-dispatch-gates, and it satisfies both halves of ruling 3's acceptance test. C is measurably the 'louder version of the same uselessness' ruling 3 forbids: 34 gates on one packages/spec card. B costs a round to arrive at A." }, { "question": "check-doc-anchors' other population — README.md and ARCHITECTURE.md — stays unreachable, and that residue is structural rather than an oversight. Worth its own card?", "options": [ "A. Leave it: documented as a decided loss in hintCovers' docblock and pinned by a negative fixture", "B. File a finding so triage can weigh a declaration channel for top-level FILE populations" ], "recommendation": "A for now. A bare filename carries no separator either, and accepting one would admit every package.json / turbo.json / tsconfig.json basename a gate joins with a package directory — the same explosion shape 1 buys, one class over. B only becomes worth it if a second gate turns up with a top-level-file population." } ], "out_of_scope_findings": [ "filed as #9640: dispatch-gates' `export { maskComments }` sits inside an unterminated block comment, so the module does not actually re-export it (`typeof m.maskComments === 'undefined'`) while its docblock states the guarantee. Dormant — every live consumer imports from js-comment-mask.mjs directly. Not fixed inline: different defect class from this card, and the right shape (close the comment vs delete the re-export) is triage's call." ] }Generated by Claude Code
Generated by Claude Code
fix(gates): declare the workspace parents as globs, so the dispatch d…
fix(gates): declare the workspace parents as globs, so the dispatch d…