Skip to content

[finding] dispatch-gates cannot name check:doc-formula-expressions for any card in its live corpus — same bare-root ROOTS line as #10648 #10664

Closed

Description

@claude

Found from the #10648 execution seat (which fixed the identical defect in
scripts/check-doc-authoring.mjs, PR #10662). Out of that card's scope and not
touched there. Seventh instance of the class: #9626 / #9979 / #10114 / #10314 /
#10516 / #10648.

The finding

packages/lint/scripts/check-doc-formula-expressions.mjs:170 declares its
population with the byte-identical line the sixth instance had:

const ROOTS = ['.claude', 'docs', 'skills', 'content'];

docs, skills and content carry no path separator, so
scripts/pm/dispatch-gates.mjs refuses them as too generic. (.claude is
admitted — it is a top-level dotted directory, which the extractor takes
explicitly. That detail was recorded wrong on #10648 and is corrected in
PR #10662.)

The result has the same inverted shape. Measured on origin/main at
9dd192d48b, the gate's whole extracted hint set is:

@objectstack/formula, .claude, .claude/worktrees, docs/audits,
docs/handoff, docs/plans, content/docs/references, packages/spec/src

Derivation, same tree:

docs/qa/platform-checklist/RUNNER.md   not derived
content/docs/deployment/cli.mdx        not derived
skills/objectstack-upgrade/SKILL.md    not derived
docs/plans/x.md                        DERIVED   <- an exclusion

So the live corpus derives nothing and the exempt subtrees derive the gate —
the inverse error, in a gate that is REQUIRED at .github/workflows/lint.yml:2916
(pnpm --filter @objectstack/lint run check:doc-formula-expressions).

Suggested fix

The pattern is documented and now has two worked instances to copy:
scripts/check-role-word.mjs (ROOT_DIR_WATCH_HINTS) and, closest to this
gate, scripts/check-doc-authoring.mjs after PR #10662 — which also carries the
reasoning for what to do about SKIP_PATHS (the hint language cannot subtract,
so the over-claim is bounded and asserted rather than removed).

⚠️ This gate is package-scoped (@objectstack/lint), so its extractor
resolution goes through that package's manifest rather than the root one. Worth
confirming the declaration is actually picked up after the change rather than
assuming — the whole class is about declarations that read as present and reach
nothing.

Why one at a time, and not the extractor

Asked and answered on #10648 with a measurement worth not repeating: a
class-level guard that flags any gate naming a bare top-level tracked directory
none of its hints reach fires on 40 of 123 families on this tree, and the
majority are correct as they stand (check:error-code-casing really does sweep
all of packages/). The declaration has to be authored because the distinction
between "population root" and "path component" is not in the source text. See
PR #10662's body for the full argument.

Refs: packages/lint/scripts/check-doc-formula-expressions.mjs · .github/workflows/lint.yml:2916 · #10648 · PR #10662


Generated by Claude Code


Generated by Claude Code

Activity

  1. added
    domain:devxDomain lane — seat card indexed by label:pm:seat
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    pm:queueReady for the PM dispatch loop
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    on Aug 21, 2026
  2. claude commented on Aug 21, 2026

    @claude
    ContributorAuthor

    Claim: /pm-dispatch domain:devx execution seat.
    Session: session_01DdCnBGcHeufjrq7drTD3wt
    Branch: claude/issue-10664-doc-formula-hints
    Worktree: objectstack-issue-10664
    Domain: domain:devx
    File surface: packages/lint/scripts/check-doc-formula-expressions.mjs (its declared population)

    ⭐ The class question is already settled — read this before re-opening it. PR #10662 (#10648, in the merge queue) measured the tempting extractor-side fix over all 123 discovered families: it fires on 40 of 123, and the majority are correct as they stand (check:error-code-casing declares SCAN_ROOTS=['packages'] and really does sweep the whole tree, so forcing it to declare packages/** would name it on nearly every card — the drowning the genericity rule exists to prevent). Its conclusion:

    The signal cannot mechanically separate 'population root' from 'path component the gate joins with something else' because that distinction is in the author's intent, not the source text; that is why the declaration must be authored.

    ⇒ This card is "fix this gate too", ⛔ not "the class is still open in the extractor". Widening hintCovers is separately decided with numbers (#9626: +139,084 fabricated pairs; #9964: 8 of 17 fabricated). Follow #10662's pattern — declare the real roots in the subtree spelling, pin both halves, and bound any over-claim rather than hiding it.

    ⛔ Governed surface out of scope (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, content/docs/releases/**) and the #9465 epic fence.
    ⚠️ No formatter of record — ⛔ do not run prettier --write (#10622).
    ⭐ Changeset: AGENTS.md:943; a diff that publishes nothing takes skip-changeset via the additive POST /labels (⛔ never a whole-set PUT — see #5533).


    Generated by Claude Code

  3. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 21, 2026
  4. added theissue type on Aug 21, 2026
  5. claude commented on Aug 21, 2026

    @claude
    ContributorAuthor
    {
      "issue": 10664,
      "status": "done",
      "branch": "claude/issue-10664-doc-formula-hints",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/10687",
      "premise_still_valid": true,
      "summary": "check-doc-formula-expressions.mjs now declares ROOT_WATCH_HINTS = ['.claude/**','docs/**','skills/**','content/**'] in the subtree spelling dispatch-gates compares in, following PR #10662's pattern (read before writing; it has NOT landed on origin/main 55809a09a5, so check-doc-authoring.mjs still carries the bug there and my dispatch-gates block is anchored well away from #10662's to avoid a conflict). Both halves pinned: 6 new cases in the gate's own --self-test derived from ROOTS rather than re-spelled, 11 new cases in dispatch-gates --self-test against the real extractor. Two corrections to the card, both measured: (1) it reports the whole live corpus as underived, but .claude (20 files, dotted-dir arm) and the entire SECOND surface packages/spec/src (972 files, SPEC_ROOT already carries a separator) were already reachable -- the real figure is 396 of 1388 files (28.5%) declared by nothing, not the ~98% of the sixth instance; (2) the .claude detail is correct as the card states, so only THREE of the four ROOTS were refused. Card's lint.yml:2916 is now lint.yml:2977 (line drift only). Self-report: the branch push used --force-with-lease unnecessarily; the output shows 55809a09a5..7d4e4cd923, a fast-forward, so no history was rewritten.",
      "tests": "All at final HEAD 7d4e4cd923, working tree clean. POPULATION re-derived and cross-checked against the gate's own output: docs 156 / skills 48 / content 192 / .claude 20 = 416 md-mdx files (gate prints '416 files / 1443 TS blocks') + packages/spec/src 972 (gate prints '972 packages/spec/src files') = 1388 walked; 992 derived a hint before, 396 (28.5%) declared by nothing. BEFORE: 'node scripts/pm/dispatch-gates.mjs --residue docs/qa/platform-checklist/RUNNER.md' -> 'check:doc-formula-expressions [lint.yml] names: @objectstack/formula, .claude, .claude/worktrees, ...' (silent bucket, exclusions only). AFTER: 'matched via docs/qa/platform-checklist/RUNNER.md => gate source docs/**'; skills/objectstack-upgrade/SKILL.md => 'skills/**'; content/docs/deployment/cli.mdx => 'content/**'. This also answers the card's package-scoped worry by measurement: the extractor really does resolve packages/lint/scripts/... through that package's manifest. REJECT SIDE, both directions: (a) outside the roots -- packages/core/src/index.ts, apps/console/src/main.tsx, examples/crm/objects/account.object.ts each yield ZERO 'matched via' rows for this gate and stay in the silent bucket (packages/ probed OUTSIDE packages/spec/src on purpose: a spec path would pass on SPEC_ROOT and pin nothing); (b) the SKIP_PATHS half is NOT assertable as the brief states -- see open_questions -- so it is pinned as a bounded, pre-existing residual instead: measured on origin/main BEFORE the change, all five SKIP_PATHS subtrees already derived the gate (docs/plans/x.md, docs/audits/x.md, content/docs/references/x.mdx, .claude/worktrees/x/y.md all DERIVED on base), and after the change docs/plans/x.md still matches via gate source 'docs/plans' -- the exclusion literal, not docs/**. A self-test case asserts it with the declaration filtered out of the hint set. ABLATIONS, 9, each mutation confirmed on disk by anchor count (1->0 for the anchor, >=1 for the injection) before any reading, each restored via 'git checkout <branch> -- <path>' from the commit (fix committed FIRST) and re-run green: A1 delete docs/** -> gate case 1 RED + 'reaches the live docs corpus' RED; A2 empty the declaration -> gate case 1 RED + 3 extractor cases RED while the .claude and packages/spec/src cases stay GREEN (this is the measured claim my source comment makes about those two cases, verified rather than asserted); A3 declare packages/** -> 'declares no root this gate does not walk' RED + 'claims nothing elsewhere under packages/' RED; A4 glob form into ROOTS -> case 3 RED; A5 exemption outside the roots -> case 4 RED; A6 exemption swallowing a root -> cases 4 AND 5 RED (one more than predicted: a bare 'skills' fails the startsWith(r + '/') test too -- explainable, reported rather than smoothed over); A7 SPEC_ROOT to a bare word -> case 6 RED + 'its second surface' RED; A8 drop docs/plans from SKIP_PATHS -> residual case RED; A9 exempt docs/qa -> 'the live corpus derived nothing without it' RED. GATE UNION derived with NO paths passed (script took the changeset from the merge base itself), 8 families, all EXIT=0 under scripts/pm/os-verify-lock.sh -- 'os-verify-lock: VERDICT command-exit 0 - held the lock 81s (1m21s) - waited 16s': check:cross-package-test-inputs, check:pm-dispatch-gates ('dispatch-gates self-test: 455 cases pass.'), check:slot-lookup ('ratchet holds: 107 unswept site(s) in 25 file(s), none new'), check:test-source-alias, check:type-source-resolution, check-affected-docs, check:doc-formula-expressions ('self-test: 30 cases passed', then '416 files / 1443 TS blocks judged clean' and '972 packages/spec/src files'), check:nul-bytes ('OK ... no raw ASCII control bytes'). Exit codes captured before any pipe (redirect-then-capture), and every verdict above is quoted from the gate's own printed line. HAND CROSS-CHECK, since an empty/short union is this card's own subject: grepped for real read/import edges into my two files -- only scripts/pm/check-dispatch-gates.mjs reads dispatch-gates.mjs (that IS check:pm-dispatch-gates, green); no line-ratchet ceiling names either file; the new cross-tree read I introduced (dispatch-gates.mjs reading packages/lint/scripts/...) is not a package-to-package edge, and check:cross-package-test-inputs passes anyway. ESLint run separately on both changed files (clean, VERDICT command-exit 0) because pnpm lint is unfiltered in CI and so outside the derived union by design.",
      "open_questions": [
        {
          "question": "The brief's reject-side bullet -- 'a path in one of its SKIP_PATHS must NOT derive it' -- is not satisfiable, and was already false before this change. It also contradicts the brief's own star bullet ('declare the root and pin the over-claim rather than hiding it') and #10662's settled reasoning. SKIP_PATHS spells its five exclusions as module-body string literals, so the extractor takes them as hints whatever ROOT_WATCH_HINTS says: measured on origin/main, all five exempt subtrees derived the gate BEFORE my change, and docs/plans/x.md still matches via 'docs/plans' rather than 'docs/**' after it. Making that bullet true would require unquoting the most safety-critical constant in the file.",
          "options": [
            "A: pin the achievable direction (a path outside the roots entirely derives nothing) and pin the SKIP_PATHS side as a bounded, pre-existing residual, asserted with the declaration filtered out of the hint set -- what I did",
            "B: unquote SKIP_PATHS (assemble the strings) so the exclusions stop being hints -- would satisfy the bullet literally, at the cost of obscuring the constant that decides what the gate does not read, and #10662 already refused this",
            "C: declare the live subtrees under docs instead of the root -- does not remove the residual either (SKIP_PATHS literals stay hints), strands the twelve hand-written top-level docs guides, and reintroduces the hand-extension failure mode the root spelling exists to prevent"
          ],
          "recommendation": "A, because it is the only option that is both true and checkable: the residual is pre-existing and now bounded by an assertion that fails if a future exemption lands outside the declared roots. B trades a real safety property for a cosmetic one. No action needed from the PM unless the bullet was meant as a hard requirement, in which case the measurement above overturns it."
        }
      ],
      "out_of_scope_findings": []
    }
    

    Generated by Claude Code

  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 21, 2026
  7. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 21, 2026
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:seattoolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions