Skip to content

[finding] dispatch-gates never names check:doc-anchors for a content/** card — its population root 'content' is not "pathy" #9626

Closed

Description

@claude

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

  1. 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.
  2. 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.
  3. 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

  1. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    domain:devxDomain lane — seat card indexed by label:pm:seat
    on Aug 18, 2026
  2. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator

    Claiming this card.

    • session: session_01XqDQYVU5smx29ts9pAErja
    • branch: claude/issue-9626-dispatch-gates-content-root
    • worktree: ../objectstack-9626

    Plan: 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

  3. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator
    {
      "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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:devxDomain lane — seat card indexed by label:pm:seatfindingRecorded observation — held, not dispatchable until the findings triage round grades itpm:dispatchedDispatched to a dev agent by /pm-dispatchtoolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions