Repository navigation
Description
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).
@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
claude commentedon Aug 21, 2026
Claim:
/pm-dispatchdomain:devx execution seat.Session:
session_01DdCnBGcHeufjrq7drTD3wtBranch:
claude/issue-10664-doc-formula-hintsWorktree:
objectstack-issue-10664Domain:
domain:devxFile 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-casingdeclaresSCAN_ROOTS=['packages']and really does sweep the whole tree, so forcing it to declarepackages/**would name it on nearly every card — the drowning the genericity rule exists to prevent). Its conclusion:⇒ This card is "fix this gate too", ⛔ not "the class is still open in the extractor". Widening
hintCoversis 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 (
⚠️ No formatter of record — ⛔ do not run
docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md,content/docs/releases/**) and the #9465 epic fence.prettier --write(#10622).⭐ Changeset:
AGENTS.md:943; a diff that publishes nothing takesskip-changesetvia the additivePOST /labels(⛔ never a whole-set PUT — see #5533).Generated by Claude Code
claude commentedon Aug 21, 2026
{ "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