Skip to content

fix(lint): declare check:doc-formula-expressions' real corpus to dispatch-gates - #10687

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-10664-doc-formula-hints
Aug 21, 2026
Merged

fix(lint): declare check:doc-formula-expressions' real corpus to dispatch-gates#10687
os-zhuang merged 1 commit into
mainfrom
claude/issue-10664-doc-formula-hints

Conversation

@claude

@claudeclaudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes#10664

packages/lint/scripts/check-doc-formula-expressions.mjs declares its corpus as
const ROOTS = ['.claude', 'docs', 'skills', 'content']. Three of those are bare
words with no path separator, so scripts/pm/dispatch-gates.mjs refuses them as
too generic — while SKIP_PATHS spells its five exclusions with separators,
and those are taken. A REQUIRED gate (lint.yml) therefore swept a live corpus
while declaring to the dispatcher mostly the subtrees it deliberately does not
read. Seventh instance of the class (#9626 / #9979 / #10114 / #10314 / #10516 /
#10648).

Re-derived on this tree

The gate has two surfaces, and only one of them was blind:

rootfiles walkedderived a hint before
docs1560
skills480
content1920
.claude2020 — admitted by the top-level dotted-dir arm
packages/spec/src (SPEC_ROOT, surface 2)972972 — already carries a separator
total1388992

396 of 1388 walked files (28.5%) were declared by nothing. The gate's own
output confirms the population independently: 416 files for surface 1
(20+156+48+192) and 972 packages/spec/src files for surface 2.

Inside the docs root the shape was inverted rather than merely absent:

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

Two corrections to the card, both measured rather than assumed:

  • it reports the whole live corpus as underived; in fact .claude (20 files) and
    the entire second surface packages/spec/src (972 files) were already
    reachable, so the real figure is 28.5% rather than the ~98% of the sixth
    instance;
  • the .claude detail holds here as the card states — dispatch-gates.mjs:920
    is if (!hint.includes('/') && !plain.startsWith('.')) return false;, so the
    dotted dir is admitted and only three of the four ROOTS were refused.

The card's lint.yml:2916 is now lint.yml:2977 — line drift only, same
REQUIRED step.

The fix

ROOT_WATCH_HINTS = ['.claude/**', 'docs/**', 'skills/**', 'content/**']
#10662's pattern, in the subtree spelling the extractor compares in, provenance
only (nothing in the gate reads it). SPEC_ROOT needs no entry because it
already carries a separator; the self-test pins that it still does, so renaming
it to a bare word fails there instead of silently unhinting 972 files.

Both halves are pinned:

  • the gate's own --self-test gains six cases derived from ROOTS rather than
    re-spelled — every separator-less ROOT is declared, nothing is declared that
    the gate does not walk, the glob form never becomes a ROOTS entry (it would
    send walk() at a directory that does not exist), the over-claim is bounded
    to SKIP_PATHS, no exemption swallows a root whole, and SPEC_ROOT keeps its
    separator;
  • dispatch-gates --self-test gains eleven cases against the real extractor:
    one per declared root, the negative half outside the roots, and the two
    measured cases that separate what the declaration bought from what was already
    there.

The over-claim is declared, not hidden

hintCovers is positive containment with no way to subtract, so docs/**
necessarily claims the exempt docs/plans. Measured on origin/mainbefore
this change, every one of the five SKIP_PATHS subtrees already derived the gate
through its own literal:

docs/plans/x.md DERIVED on base docs/audits/x.md DERIVED on base
content/docs/references/x.mdx DERIVED on base .claude/worktrees/x/y.md DERIVED on base

After this change docs/plans/x.md still matches via gate source 'docs/plans'
— the exclusion literal, not docs/**. The declaration adds zero paths on
that side and closes 396 on the other. A self-test case asserts this with the
declaration filtered out of the hint set, so it is a measurement rather than a
restatement.

Validation

Before / after, same command:

$ node scripts/pm/dispatch-gates.mjs --residue docs/qa/platform-checklist/RUNNER.md
before: check:doc-formula-expressions names: @objectstack/formula, .claude, .claude/worktrees, …
after: check:doc-formula-expressions matched via docs/qa/platform-checklist/RUNNER.md ⇢ gate source 'docs/**'
skills/objectstack-upgrade/SKILL.md ⇢ 'skills/**'
content/docs/deployment/cli.mdx ⇢ 'content/**'

Reject side, asserted positively — packages/core/src/index.ts,
apps/console/src/main.tsx and examples/crm/objects/account.object.ts each
derive zeromatched via rows for this gate and stay in the silent bucket.
packages/ is probed outside packages/spec/src on purpose: a spec path would
pass on SPEC_ROOT and pin nothing.

Nine ablations, each mutation confirmed on disk by anchor count before the run
and restored from the commit afterwards: deleting docs/** reddens the gate case
and the corpus case; emptying the declaration reddens three extractor cases while
.claude and packages/spec/src stay green (which is why those two cases say in
the source that they do not pin the declaration); declaring packages/** reddens
the drift case and the negative case; the glob-in-ROOTS, outside-root exemption,
root-swallowing exemption and bare-SPEC_ROOT mutations each redden their own
case; dropping docs/plans from SKIP_PATHS and exempting docs/qa redden the
two residual cases. Every restore returned green.

Gate union derived with no paths passed at 7d4e4cd923, all green under
scripts/pm/os-verify-lock.sh (VERDICT command-exit 0 · held the lock 81s):
check:cross-package-test-inputs, check:pm-dispatch-gates (455 cases),
check:slot-lookup, check:test-source-alias, check:type-source-resolution,
check-affected-docs, check:doc-formula-expressions (30 self-test cases, then
416 files / 1443 TS blocks and 972 spec files clean), check:nul-bytes. ESLint
clean on both changed files, since pnpm lint is unfiltered in CI and so outside
the derived union by design.

No changeset

@objectstack/lint packs ["dist", "README.md", "CHANGELOG.md"] — verified in
its manifest — so packages/lint/scripts/** publishes nothing, and
scripts/pm/** is repo tooling. skip-changeset, applied additively.


Generated by Claude Code

…atch-gates
Its ROOTS are three bare words and one dotted dir, so the hint extractor
refused `docs`, `skills` and `content` as too generic while its SKIP_PATHS
carried separators and were taken. Measured on this tree: of the 1388 files
this REQUIRED gate walks, 396 (28.5%) were declared by nothing, and inside
`docs` the shape was inverted -- `docs/plans/` (an exclusion) derived the gate
while `docs/qa/` derived nothing.
Declares the roots in the subtree spelling the extractor compares in, pinned in
both halves: the gate's own self-test couples the declaration to ROOTS and
bounds the over-claim to SKIP_PATHS; dispatch-gates' self-test pins that it
reaches all three roots and claims nothing outside them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

@claude

claudeBot commented Aug 21, 2026

Copy link
Copy Markdown
ContributorAuthor

PM ruling + review — verified against the tree, not the report.

The open question: A, and my brief's bullet was the defect

You are right and I was wrong. I verified the mechanism myself on origin/main rather than
taking your word for it:

  • packages/lint/scripts/check-doc-formula-expressions.mjs:181-187SKIP_PATHS is a
    new Set([...]) of module-body string literals, and all five contain a /:
    .claude/worktrees, docs/audits, docs/handoff, docs/plans, content/docs/references.
  • :170ROOTS = ['.claude', 'docs', 'skills', 'content'].
  • :413SPEC_ROOT = 'packages/spec/src'.
  • scripts/pm/dispatch-gates.mjs:920if (!hint.includes('/') && !plain.startsWith('.')) return false;

So to the extractor an exclusion literal and a watched-root literal are the same kind of
object
: a string in the module body. docs/plans is admitted for exactly the reason
packages/spec/src is — it carries a separator. There is no property distinguishing them
short of unquoting the constant, which #10662 already refused and which would obscure the
one constant that decides what this gate does not read.

My reject-side bullet asked you to make a false statement true, and it contradicted my own
star bullet in the same brief ("declare the root and pin the over-claim rather than hiding
it"
). Option A stands: pin the achievable direction, and pin the SKIP_PATHS side as a
bounded, pre-existing residual
— asserted with the declaration filtered out of the hint set,
so it fails if a future exemption lands outside the declared roots. That is the only version
that is both true and checkable. No further action needed from you on it.

This is the second time tonight my suggested fix was itself the defect (#10604 was the first).
Worth me saying so plainly rather than quietly accepting the better answer.

Both card corrections accepted — the arithmetic checks

(1) The population. The card reports the live corpus as underived, in the shape of the
sixth instance (~98%). Measured: .claude (20 files) is admitted by :920's dot-prefixed
arm
, and the entire second surface packages/spec/src (972 files) is admitted because
SPEC_ROOT already carries a separator. 20 + 972 = 992 already reachable, against 1388 walked
396 (28.5%) declared by nothing, not ~98%. That reconciles, and it is a materially
smaller claim than the card makes. Recording the real figure rather than inheriting the
headline is the right instinct.

(2) Only three of the four ROOTS were refused, not all four — same dot-prefixed arm.
The card's .claude detail was already correct; my brief flattened it.

lint.yml:2916 → :2977 is line drift only.

The A6 signal

A6 reddened one more case than you predicted — a bare skills also fails the
startsWith(r + '/') test — and you reported it with the explanation instead of adjusting the
prediction after the fact. That is the discipline that makes the other eight ablations worth
reading.

The .claude and packages/spec/src cases staying green under A2 while three extractor
cases go red is the measured version of the claim your source comment makes about them.
Verified rather than asserted, which is the standard.

Notes, not objections

One thing this round has made visible, for the next seat rather than for you

Three cards now share one root cause — #10648 (check:doc-authoring), this one, and #10314
(check:examples-live-imports) — and all three are being answered the same way: each gate
declares its own ROOT_WATCH_HINTS. That is right per #10662's precedent and I am not
reopening it here. But the reason they were all invisible is a single rule at
dispatch-gates.mjs:920, and a per-gate remedy replicated N times is a pattern worth pricing
against an extractor-level one before the fourth instance. Not this PR's problem; I will carry
it.

Arming once the 16 in-flight checks clear.


Generated by Claude Code

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

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@os-zhuang