Uh oh!
There was an error while loading. Please reload this page.
fix(scripts): scan the nested README.md files no doc-link gate could see - #6279
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
…-file partition Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
yinlianghui-tw
commented
Aug 25, 2026
PM review — ACCEPT. ⭐ The differential non-vacuity proof is the strongest of tonight's four, and the structural no-double-parse is the right kind of guarantee.Reviewed by the ⭐ The RED demonstration — run against BOTH gates on the identical mutated treeThe dispatch demanded a red because this card's entry price is zero (0 dead links), so a green run proves nothing. What landed is better than what I asked for:
That is a differential proof: same tree, same defect, old gate blind, new gate names the file and line. It rules out the failure mode a single-sided red cannot — that the red came from somewhere other than the new coverage. Mutation confirmed on disk by marker counts before trusting it, revert verified with Option 1 with the |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#6026
scripts/check-doc-links.mjscould not see fourREADME.mdfiles. Thepackages/*/apps/*rows added in #4938 carryexclude: ['README.md', 'CHANGELOG.md'], andexcludedrops a basename at every depth — so a README that is not at a package's top level fell
out of those rows, and it was never in range of the exact
packages/*/README.md/apps/*/README.mdglobs above them either. Two gates, both empty verdicts.Premise re-measured on
origin/main@ef2a3bd8dThe card's file set is unchanged — the same four files, re-derived from the tree rather
than copied from the card:
The card's link counts are stale, and by a lot. It measured 10 relative links in
plugin-gantt/docs/verification/README.mdand none incore/src/adapters/README.md.Today:
packages/components/src/__tests__/README.mdpackages/core/src/adapters/README.mdpackages/plugin-gantt/docs/verification/README.mdpackages/types/src/zod/README.mdcore/src/adapters/README.mdwas rewritten by #6227 and now carries two links; the ganttverification log has grown to 94. Entry price is still 0 dead links, which is the
number that matters — this row arrives at a green surface (#3572's shape), it does not tow
its own backlog (#3479 arrived with 16, #3490 with 18).
The option taken, and why
Option 1 from the card — a collect-by-basename mode — but with the top-level exclusion
falling out of the path shape rather than out of a new filter:
collectisexclude's inverse: the only basenameswalk()keeps, again at every depth.The second wildcard segment is the load-bearing part.
expandWildcard()turns eachsegment into the directories at that level, so these rows are rooted at each package's
subdirectories — and a package's own top-level
README.mdis not inside any of them. Itcannot be double-parsed, because it cannot be reached.
Option 2 (narrow the
excludeto top-level READMEs only) was not taken, for the reason thecard anticipated and one more:
walk()would need "depth relative to the row's glob root", a concept it does not haveand which would then be load-bearing for every future row, not just this one.
Here it is structural: nothing in the new rows mentions "top level" at all.
CHANGELOG.mdstays excluded at every depth, untouched — there is no nested one today, andthe exclusion is about what the name means, not where the file sits.
walk()still has nonotion of depth.
The three package rows now partition each package directory: the exact top-level
README.md, every nestedREADME.md, everything else that is not aCHANGELOG.md. Thatpartition is asserted repo-wide rather than argued (see below).
Before / after — the scan surface, since the verdict proves nothing
Entry price is zero dead links, so a passing run is not evidence on its own. What changed
is what the gate opens:
New per-row counts:
packages/*/*gives 4,apps/*/*gives 0 (no nested README under anapp today; the row is bought while empty, and its mechanism is exercised by fixture).
Non-vacuity: the gate goes red, and only because of this change
packages/core/src/adapters/README.mdwas temporarily repointed at a nonexistent target(
../../../data-objectstack/NOWHERE-6026.md), and the pre-PR bytes of the gate wererun against the identical mutated tree from a temporary copy inside the worktree:
The mutation was confirmed on disk before either run (injected marker count 1, old anchor
count 2 down to 1) and the tree was restored afterwards:
git diff --exit-codereturned 0,git status --porcelainempty, zero residual occurrences of the marker, temporary gatecopy removed.
The tests were ablated the same way: with the two new rows deleted from
SCAN_ROOTS,8 tests fail (
Tests 8 failed | 93 passed), including both real-tree surface assertions.The ablation script carried a
trap ... EXIT INT TERMrestore, and the restore wasverified byte-exact with
git diff --exit-code.Tests
Eight new tests in
scripts/__tests__/check-doc-links.test.ts, all collected by name under--reporter=verbose. Two of them assert over the real tree and are the ones the zeroentry price makes necessary:
opens every nested README that is really in the tree— the population is derived froman unfiltered walk of the same roots, not listed in the test, so the fifth nested README
someone writes is covered without an edit.
opens every file exactly once — the rows partition the tree, they do not overlap—repo-wide, and this is what pins the card's "do not double-scan" constraint mechanically
instead of by comment.
The
objectui#4938test that asserted nested READMEs are excluded is narrowed toCHANGELOG.md(which is still excluded at every depth) and now points at the new describe.Gates — verdict lines, all at
31e1e2557lint:rootpopulation control:--format jsonreports 196 files linted, and bothchanged files appear in it — the green is a measurement, not a no-op.
type-check:scriptswas red first, withTS2345reporting that an argument of typeSet(string) | undefinedis not assignable to a parameter of typenull | undefined: thecollect = nulldefault made checkJs infer anull-only parameter. Fixed with explicit@paramJSDoc onwalk()andcollectFiles(), then green. (Spelled with parentheseshere on purpose — the angle-bracket form is eaten by GitHub's body sanitizer.)
Declared narrowing: the full root vitest suite
Not run locally. The shared verify lock returned
queue-timeout (exit 99) · never acquiredtwice for it (9m00s each, ~18 minutes), while another agent held it for a root run of their
own. What was run instead is the whole
scripts/__tests__surface (77 files, 2215 tests),and the narrowing is a measurement rather than a guess:
scripts/;scripts/check-doc-links.mjsanywhere in the repo (grep over*.ts,*.tsx,*.mjs,*.js,*.cjs, node_modules excluded) isscripts/__tests__/check-doc-links.test.ts;scripts/tree wholesale already lives inscripts/__tests__and ran.CI runs the farm in full regardless.
Changeset
None owed. The diff touches only
scripts/, andcheck-changeset-presence.mjsguardseach versioned package's own
src/**for the packages in thefixedgroup — nothingpublished changes here. Read from the diff, not from the gate's exit code (it exits 0
either way). Precedent in the last 30 commits: #6212
feat(scripts), #6216feat(tooling)and #6260docs(tooling)all carried none. objectui has noskip-changesetlabel, and an empty-frontmatter changeset is the declaration for guardedsource that publishes nothing, which this is not.
Generated by Claude Code