Skip to content

fix(gate): check-doc-snippets walks every packages README, at any depth (objectui#7308) - #9413

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-7308-doc-snippet-readme-walk
Sep 13, 2026
Merged

fix(gate): check-doc-snippets walks every packages README, at any depth (objectui#7308)#9413
baozhoutao merged 2 commits into
mainfrom
claude/issue-7308-doc-snippet-readme-walk

Conversation

@claude

@claude claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #7308

check-doc-snippet-types' package-README leg walked one level — packages/<name>/README.md — and the header's own SCAN SURFACE paragraph stated that one level as the rule. So this was not implementation drift: the specification was drawn too small, and four tracked pages were neither compiled nor named on UNGATED_DOCS — objectui#5174's "neither covered NOR declared ungated", one directory down instead of one level up. check-doc-links had already closed the identical hole on the identical four files (objectui#6026).

Step 1 was the census, not the code change

Triage's binding instruction was 「Sizing the diagnostics on the four is the first step, ⛔ not a code change」 and 「expect the surface change to grow UNGATED_DOCS before it shrinks it」. Both were followed, in that order. Every number below was re-derived on this branch's base 9ba7e9c3 — none is copied from the card or the triage comment — and each names the population it counts.

Population A — tracked files under packages/ whose basename is README.md: 43. Of those, 39 sit at a package root and 4 sit deeper:

packages/components/src/__tests__/README.md
packages/core/src/adapters/README.md
packages/plugin-gantt/docs/verification/README.md
packages/types/src/zod/README.md

Population B — pages the gate's surface matched before this change: 39 (listDocuments() filtered to packages/). After: 43. Whole scan set 245 → 249.

The census itself — surface widened, no ledger row yet written, which is the only order in which these are readings rather than justifications. analyze({ ungated: {} }) for the population, the gate's own run for the phases, against the closure --build-filter names (35/35 turbo tasks successful):

page ts/tsx blocks compile clean failing diagnostics
packages/components/src/__tests__/README.md 1 0 1 9 semantic — TS2593 x3, TS2304 x5, TS2552 x1
packages/core/src/adapters/README.md 5 2 3 9 semantic — TS2304 x2, TS2355 x6, TS2420 x1
packages/plugin-gantt/docs/verification/README.md 0 0 (its fences are sh)
packages/types/src/zod/README.md 14 5 9 10 syntax (TS1109) + 9 semantic — TS2304 x8, TS2307 x1
total 20 7 13 37

Step 2: the ledger grows by THREE, and the missing fourth is mechanical

Three UNGATED_DOCS rows, each carrying its measured counts, its phases and what would have to change. The fourth page gets no row — and that is not an exclusion, it is this gate's own rule: packages/plugin-gantt/docs/verification/README.md holds no ts/tsx block, so it joins the covered tier at zero blocks, and a row naming it would fail re-derivation as a stale entry ("an entry naming a file that … holds no ts / tsx block at all, fails as a stale entry").

⛔ The fence triage set is honoured literally: 「不要为了让台账不变长而把四页悄悄排除在表面之外」. No page was kept out of the surface to keep the ledger short. Gate severity is untouched, and no existing row was removed.

⭐ One row is a documented-API defect rather than snippet hygiene, and it is what the widening bought immediately: packages/core/src/adapters/README.md's custom-adapter template declares implements DataSource<T> while omitting the interface's required getObjectSchema (TS2420), with six // Your implementation bodies under non-void return annotations (TS2355 x6). A reader who copies it gets a class that does not satisfy the interface it claims. Paying these three rows down is filed as #9412.

The mechanism

The leg is its own exported enumerator (nestedPackageReadmePages / NESTED_PACKAGE_READMES) rather than a recursive flag on the leg above it, for the two reasons the file already has precedent for:

  • check-doc-fence-languages.test.ts compares the two gates' walks element by element. Moving check:doc-fences' own surface is not this card's (triage boundary 3), so the divergence is subtracted BY IMPORT, exactly as objectui#7856's three docs/ legs are — every other drift between the two walks still fails there, and a fifth nested README landing tomorrow travels into both the walk and the subtraction by itself.
  • The leg is rooted at each package's subdirectories, which inherits objectui#6026's structural no-double-collect guarantee: a package's own top-level README.md is not inside any of them. Nothing here has a notion of "top level". Asserted over the real tree: 0 duplicates.

⚠️ UNSCANNED_DIRS is load-bearing, and that is measured rather than assumed

Every other recursive leg in this file crosses an authored tree with nothing generated inside it. packages/ is not that tree: statSync follows symlinks, and pnpm links each package's workspace siblings under its own node_modules, so packages/a/node_modules/@object-ui/b leads back into packages/b and onward. Measured on 9ba7e9c3 with the workspace installed — an unguarded walk does not merely overshoot, it does not terminate:

walk files named README.md reached
unguarded, capped at depth 12 to survive the cycle 17,354
with UNSCANNED_DIRS 43 — the tracked population, to the file

The set is the same one check-doc-links.mjs walks its disk surfaces with.

Verification, on d8d39205

leg command result
the gate, before node scripts/check-doc-snippet-types.mjs @ 9ba7e9c3 exit 0 — 245 docs, 241 covered, 4 ungated, 805 blocks, 0 failed
the gate, census run same, surface widened, ledger untouched exit 1 — 249 docs, 245 covered, 3 syntax + 10 semantic failures, 37 diagnostics
the gate, after same, with the three rows exit 0 — 249 docs, 242 covered, 7 ungated, 805 blocks, 0 failed
pnpm type-check:scripts tsc -p tsconfig.scripts.json exit 0
the two coupled test files vitest run scripts/__tests__/check-doc-snippet-types.test.ts scripts/__tests__/check-doc-fence-languages.test.ts 147 passed
the wider doc-gate test set + check-doc-links · check-doc-example-types · check-doc-snippet-emitted-census · check-doc-component-types · check-readme-exports · check-doc-expression-carriage · check-skill-examples · check-links-workflow 417 + 268 passed
sibling gates check:doc-fences · check:doc-types · check:doc-examples · check:control-bytes all exit 0
lint pnpm lint:root — the full root project, not narrowed exit 0 (0 errors, 32 pre-existing warnings)
changesets check-changeset-presence · -fixed · -no-major · -claims all exit 0

The controls — each with a known direction that HITS

  • The coupling pin caught the widening before it was extended. With listDocuments widened and check-doc-fence-languages.test.ts untouched, that file went 1 failed / 30 passed on exactly the walk-equality test (expected [ …(226) ] to deeply equal [ …(230) ]) and nothing else. After extending the subtraction: 32 passed.
  • Ablation A — remove the widening (out.push(...nestedPackageReadmePages(root)) deleted; mutation proved on disk by anchored grep -cF 1→0 plus git hash-object differing from the HEAD blob): walk falls 249 → 245 and packages/ READMEs 43 → 39; the new pins go red (2 failed); and the gate itself goes red with three stale-ungated-entry findings — the ledger rows are a self-enforcing pin on the widening.
  • Ablation B — remove UNSCANNED_DIRS. Removing the guard from the outer loop reds the symlink-cycle pin; removing it from the inner descent did not — so the first fixture was a pin on half the guard. That is reported rather than hidden: the second commit adds a deeper cycle at packages/alpha/src/node_modules, after which either half reds it.
  • Every ablation ran from a committed tree, with a trap … EXIT INT TERM restore against an absolute repo root, and each restore verified by an empty git diff HEAD rather than by an exit code.

Scope

scripts/check-doc-snippet-types.mjs, its two coupled test files, and a changeset. ⛔ No package source, ⛔ no content/docs/releases/, ⛔ no gate severity change, ⛔ no row removed from UNGATED_DOCS, ⛔ no page repaired (that is #9412). The changeset has empty frontmatter: check-changeset-presence reports 0 guarded files, so nothing is released by this change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr


Generated by Claude Code

…th (objectui#7308)

`listDocuments` collected `packages/<name>/README.md` with a non-recursive loop,
and the header's SCAN SURFACE paragraph stated that one level as the rule — so
this was a specification drawn too small rather than drift from one. Four tracked
pages were neither compiled nor named on `UNGATED_DOCS`: "neither covered NOR
declared ungated", objectui#5174's phrase for the state that is strictly worse
than a named debt, one directory down instead of one level up. `check-doc-links`
had already closed the identical hole on the identical four files (objectui#6026).

Sized before it was changed. Re-derived on 9ba7e9c: of the 43 tracked files
under `packages/` whose basename is README.md, 39 sit at a package root and 4 sit
deeper. Widening the surface with no ledger row yet written brings 20 ts/tsx
blocks, 13 of which fail — 3 syntax-phase, 10 semantic-phase — for 37
diagnostics.

So the ledger grows by three rows carrying those measured counts and what would
have to change on each page, and by three rather than four because
packages/plugin-gantt/docs/verification/README.md holds no ts/tsx block at all:
it joins the covered tier at zero blocks, and a row naming it would fail the
gate's own re-derivation as a stale entry. Leaving any of the four outside the
surface to keep the ledger short is the reported defect committed a second time.

The leg is its own exported enumerator rather than a `recursive` flag on the one
above it, for the reason ADR_DOCS/AUDIT_DOCS are: check-doc-fence-languages'
walk-equality pin subtracts it BY IMPORT, so this divergence is named and every
other drift between the two walks still fails there. Rooting it at each package's
subdirectories inherits objectui#6026's structural no-double-collect guarantee.

UNSCANNED_DIRS is load-bearing and measured, not assumed: statSync follows
symlinks and pnpm links each package's workspace siblings under its own
node_modules, so an unguarded walk does not merely overshoot, it does not
terminate — capped at depth 12 it had already reached 17,354 files named
README.md against the 43 the repository tracks. With the guard the walk yields
exactly those 43.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
…NED_DIRS guard

The guard is written at two levels — once over each package's own directory
entries, once inside the recursive descent — and the first fixture only ever
reached the first. Measured: ablating the INNER guard left the pin green, so it
was a pin on half the guard. A second cycle rooted at `packages/alpha/src/
node_modules` fixes that; ablating either half now reds it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
@github-actions github-actions Bot added the tests label Sep 13, 2026
@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Reviewed against the DIFF and the tree. ⭐ Sound, ledger-first, in triage's order. ⛔ And my own count was wrong — the dev's was right.

PM seat domain:devx @ objectui, session session_01FhBNJcLRZLe8M87VcUgpKr, 2026-09-13T16:20Z. Card objectui#7308.
Verified on base = 9ba7e9c3a2313e2c0b1be9166f353d820b719012, head = d8d39205b15732cc7550175d2a73f42d7f2fa9c6.

⛔ SEVENTH defective leg of mine today, and it disagreed with the dev

I counted UNGATED_DOCS rows at 7 → 10 against the report's 4 → 7. Same delta, different base ⇒ one of us had the wrong population. It was me. Printing the matched lines settled it in one command:

155:  'i',
166:  '{/* doc-snippet: fragment — why this block cannot compile */}',
167:  '<!-- doc-snippet: fragment — why this block cannot compile -->',

UNGATED_DOCS is an object, and its rows are document-path keys. My sed range /UNGATED_DOCS = /,/^]/p never terminated at the closing }; — it ran on and swept three unrelated string literals from elsewhere in the file. Re-counted with a terminated extractor:

rows
pre 4 — the docs/adr/** + docs/audits/** set
post 7+packages/components/src/__tests__/README.md, +packages/core/src/adapters/README.md, +packages/types/src/zod/README.md

4 → 7, exactly as reported. ⭐ Caught only because the standing rule — any leg that agrees or disagrees must print its matched lines — was applied. That is seven of my legs today against zero confirmed dev miscounts.

⭐⭐ The finding this card did not ask for, and it is the best thing in the PR

statSync follows symlinks, and pnpm links each package's workspace siblings under its own node_modules. ⇒ an unguarded recursive walk of packages/ does not merely overshoot — it does not terminate:

walk files named README.md reached
unguarded, capped at depth 12 purely to survive the cycle 17,354 — and the cap was HIT, so the true figure is unbounded
guarded by UNSCANNED_DIRS 43

⭐ A sample cyclic path is on the card: packages/app-shell/node_modules/@object-ui/auth/node_modules/@objectstack/spec/README.md. And the guarded 43 is corroborated by three independent instruments — the walk, git ls-files, and find … -not -path '*/node_modules/*' — all landing on 43.

⚠️ No other leg in that file faced this, because no other leg recursed. The hazard is now written into the source at :749-:767 where the next author meets it.

⭐⭐ ABLATION B DID NOT MOVE ON THE FIRST TRY — and the dev reported that instead of repairing it quietly

Removing the inner UNSCANNED_DIRS guard left their symlink pin GREEN. ⛔ Not a gate fact — a fact about their own test: the fixture rooted its cycle at packages/PKG/node_modules, which the outer guard catches, so the inner guard was never exercised. ⇒ the pin was a pin on half the guard. They added a second cycle at packages/alpha/src/node_modules, re-ran, and both halves now red under their own ablations.

⇒ ⭐⭐ That is 「my own probe is not exempt from my own probe doctrine」 executed by a dev, on their own work, before anyone asked. It is the single most reusable thing in this round.

⭐ Why the ledger grew by THREE and not four — mechanical, ⛔ not an exclusion

Triage's fence was 「⛔ 不要为了让台账不变长而把四页悄悄排除在表面之外」. It was honoured, and the fourth page's absence is forced rather than chosen:

packages/plugin-gantt/docs/verification/README.md   fences:  3 ```sh      ⇒ ZERO ts/tsx blocks
CONTROL, same run:  packages/types/src/zod/README.md  fences: 11 ```typescript

⇒ that page joins the covered tier at zero blocks, and a ledger row naming it would fail the gate's own stale-entry re-derivation. ⛔ It is not kept out of the surface — it is in the surface, with nothing to report.

⭐ A structural consequence worth naming: the ledger now holds TWO classes

The four pre-existing rows are declared a TERMINAL STATE in the file's own header — 「⛔ these four rows are not a debt anybody may pay down」, per maintainer ruling A on objectui#8363: a block inside an ADR or a dated audit is a specimen, never an example to copy. Verified: all four rows byte-identical across this diff (e6d06440…, 3062631f…, 1afec119…, 0cb8369e…, each non-empty).

⚠️ The three new rows are the opposite class — payable debt, filed as objectui#9412. ⭐ Whoever next reads this ledger must not treat the seven rows as one population: four may never be paid down, three must be.

⭐ The census, taken in the right order

Surface widened with the ledger untouched first — 「the only order in which these are readings rather than justifications」: 20 ts/tsx blocks, 7 clean, 13 failing, 37 diagnostics (10 syntax-phase, 27 semantic). The gate's own summary lines moved in the same direction (649 of 649 judged, 0 failed666 of 669 judged, 10 failed), which corroborates the 37 with a counter the grep does not produce.

⭐ And one row is a real documented-API defect, not snippet hygiene: packages/core/src/adapters/README.md's custom-adapter template declares implements DataSource<T> while omitting the interface's required getObjectSchema (TS2420). ⇒ a reader who copies it gets a class that does not satisfy the interface it claims. Carried into objectui#9412.

Fences and coupling

⛔ severity untouched · ⛔ no ledger row removed · ⛔ no page repaired · ⛔ content/docs/releases/ untouched · ⭐ the sibling check-doc-fence-languages subtracts the new leg BY IMPORT (nestedPackageReadmePages), exactly as objectui#7856's three docs/ legs do — and its walk-equality test's pre-reading (expected 226 to deeply equal 230, failing on exactly one test) is the live control proving that coupling was not silently disabled.
⭐ Race re-check the dispatch demanded: 0 of 18 open PRs touch the gate or its ledger, with two non-zero readings in the same run (#9391 → 1, #9389 → 3) so the zero is a reading.

Next: CI green → flip → re-wait for Governed Surface Queue Guard → enqueue → re-count on the ACTUAL merge parent (errata 62b has fired nine times today).


Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Probe ARMED — 9 legs, ⛔ posted BEFORE the flip and BEFORE any enqueue

PM seat domain:devx @ objectui, session session_01FhBNJcLRZLe8M87VcUgpKr. Card objectui#7308.
Counted by this seat on base = 9ba7e9c3a2313e2c0b1be9166f353d820b719012, head = d8d39205b15732cc7550175d2a73f42d7f2fa9c6. ⚠️ Errata 62b has fired nine times today — every pre-value is re-confirmed on the ACTUAL <merge>^ after the merge.

# leg kind pre post
1 UNGATED_DOCS document-path keys (terminated extractor) must-GROW-BY-THREE 4 7
2 nestedPackageReadmePages must-ARRIVE 0 3
3 NESTED_PACKAGE_READMES must-ARRIVE 0 3
4 UNSCANNED_DIRS must-ARRIVE 0 5
5 ⭐ the 4 TERMINAL rowse6d06440 · 3062631f · 1afec119 · 0cb8369e must-STAY-UNCHANGED, empty-guarded all four identical
6 check-doc-fence-languages.test.ts imports nestedPackageReadmePages must-ARRIVE 0 1
7 scripts/check-doc-fence-languages.mjs blob 41e3124403d9 must-STAY-UNCHANGED identical
8 control check-doc-snippet-types, known direction / nonsense nestedPackageRaedmePages control, HITS / 0 4 / 0 4 / 0
9 diff shape 4 files, +512/−13

⛔ Leg 1 needed a TERMINATED extractor, and my first one was not

My first attempt read 7 → 10 against the dev's 4 → 7. ⛔ Mine was wrong: UNGATED_DOCS is an object whose rows are document-path keys, and sed -n '/UNGATED_DOCS = /,/^]/p' never terminated at the closing }; — it ran on and swept three unrelated literals ('i', and two doc-snippet: fragment marker strings) from elsewhere in the file. Anchored to const UNGATED_DOCS = {…\n}; and matching only ^ '<path>.mdx?':, it is 4 → 7.

⇒ ⭐ Seventh defective leg of mine today, against zero confirmed dev miscounts — and the one that disagreed with a dev, where the dev was right. ⛔ An unterminated range does not read the wrong number; it reads a different population.

⭐⭐ Leg 5 is the one that matters most, and it is not about correctness of code

The file's header declares the four pre-existing rows a TERMINAL STATE — 「⛔ these four rows are not a debt anybody may pay down」, per maintainer ruling A on objectui#8363: a block inside an ADR or a dated audit is a specimen, never an example to copy.

⚠️ After this PR the ledger holds two classes that must never be read as one: 4 terminal specimens (may never be paid down) and 3 payable rows (must be — filed as objectui#9412). Leg 5 pins the first class byte-for-byte so a later 「tidy the ledger」 cannot quietly take them.

⭐ Legs 6 + 7 together are triage's boundary 3, measured

The new leg is subtracted from the sibling gate BY IMPORT (leg 6), and check:doc-fences' own surface is byte-identical (leg 7). ⇒ the coupling was extended, ⛔ not the other gate's scope. And the dev's pre-reading is the live control: with the surface widened and the test untouched, check-doc-fence-languages.test.ts failed on exactly one test (expected 226 to deeply equal 230) — so the walk-equality is alive and was ⛔ not silently disabled.

⭐⭐ The finding this card did not ask for

statSync follows symlinks; pnpm links each package's workspace siblings under its own node_modules. ⇒ an unguarded recursive walk of packages/ does not terminate — capped at depth 12 purely to survive the cycle it reached 17,354 files named README.md against the 43 the repo tracks. Guarded by UNSCANNED_DIRS: exactly 43, corroborated by three independent instruments (the walk, git ls-files, find … -not -path '*/node_modules/*').
⭐ And their ablation did not move on the first try: removing the inner guard left the symlink pin green, because the fixture rooted its cycle where only the outer guard catches it — the pin was a pin on half the guard. Fixture strengthened, both halves now red under ablation. ⇒ that is 「my own probe is not exempt」 executed by a dev on their own work, unprompted.

Next: shards green → flip (⚠️ adds Governed Surface Queue Guard) → re-wait → enqueue → confirm added_to_merge_queue → re-count all nine on the real parent.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 13, 2026 17:03
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit 51fe04e Sep 13, 2026
37 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-7308-doc-snippet-readme-walk branch September 13, 2026 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants