From b645edc0daaebfabf0e5d0729a62ce19f9bb23a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 16:12:29 +0000 Subject: [PATCH 1/2] fix(gate): check-doc-snippets walks every packages README, at any depth (objectui#7308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `listDocuments` collected `packages//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 9ba7e9c3: 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 Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr --- ...7308-doc-snippet-nested-package-readmes.md | 34 +++ .../check-doc-fence-languages.test.ts | 75 ++++++- .../__tests__/check-doc-snippet-types.test.ts | 211 ++++++++++++++++++ scripts/check-doc-snippet-types.mjs | 194 +++++++++++++++- 4 files changed, 501 insertions(+), 13 deletions(-) create mode 100644 .changeset/7308-doc-snippet-nested-package-readmes.md diff --git a/.changeset/7308-doc-snippet-nested-package-readmes.md b/.changeset/7308-doc-snippet-nested-package-readmes.md new file mode 100644 index 0000000000..495f079050 --- /dev/null +++ b/.changeset/7308-doc-snippet-nested-package-readmes.md @@ -0,0 +1,34 @@ +--- +--- + +Widen `check:doc-snippets`' package-README walk to every depth (objectui#7308). +Tooling only; no package is released by this change. + +The gate collected `packages//README.md` and nothing deeper — and it stated +that one level in its own header's SCAN SURFACE paragraph, so this was a +specification drawn too small rather than drift from one. Four tracked pages were +therefore 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, arriving one directory down instead of one level up. The +sibling gate `check-doc-links` had already closed the identical hole on the +identical four files (objectui#6026). + +Sized before it was changed, which is the only order in which the numbers are +readings. Re-derived on `9ba7e9c3`: of the 43 tracked files under `packages/` +whose basename is `README.md`, 39 sit at a package root and 4 sit deeper. With +the surface widened and no ledger row yet written, those 4 documents bring 20 +`ts`/`tsx` blocks, 13 of which fail — 3 in the syntax phase, 10 in the 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. It grows by three and not 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 was refused — that is the defect this card +reported, committed a second time. + +One page was worth the widening on its own: `packages/core/src/adapters/README.md` +teaches a custom-adapter template that declares `implements DataSource` while +omitting the interface's required `getObjectSchema`, so a reader who copies it +gets a class that does not satisfy the interface it claims. diff --git a/scripts/__tests__/check-doc-fence-languages.test.ts b/scripts/__tests__/check-doc-fence-languages.test.ts index 68ec775246..0aed9ef58e 100644 --- a/scripts/__tests__/check-doc-fence-languages.test.ts +++ b/scripts/__tests__/check-doc-fence-languages.test.ts @@ -19,6 +19,8 @@ import { AUDIT_DOCS as SNIPPET_AUDIT_DOCS, auditDocsPages as snippetAuditDocsPages, listDocuments as snippetDocuments, + NESTED_PACKAGE_READMES as SNIPPET_NESTED_PACKAGE_READMES, + nestedPackageReadmePages as snippetNestedPackageReadmes, ROOT_DOCS as SNIPPET_ROOT_DOCS, rootDocsPages as snippetRootDocsPages, ROOT_PAGES as SNIPPET_ROOT_PAGES, @@ -167,22 +169,40 @@ describe('check-doc-fence-languages: the scan surface is check-doc-snippet-types * OTHER divergence still fails here, which is the whole point of keeping the * comparison rather than deleting it. */ - /** The snippet gate's three root-`docs/` legs, taken from the gate itself. */ + /** + * The snippet gate's own legs, taken from the gate itself: its three + * root-`docs/` legs, plus objectui#7308's nested package READMEs. + * + * ⭐ objectui#7308 is the fourth enumerator and it arrives the way card 1 built + * this for: `check-doc-links` had already closed the same hole on the same four + * files (objectui#6026), but moving `check:doc-fences`' own surface is not that + * card's to do — so the divergence is SUBTRACTED BY IMPORT and every other + * drift between the two walks still fails here. A fifth nested README landing + * under `packages/` tomorrow travels into the snippet gate's side and into this + * subtraction by itself; a hand-written list of today's four filenames would + * have had to be re-typed for it. + */ const snippetDocsLegs = () => [ ...snippetRootDocsPages(ROOT), ...snippetAdrDocsPages(ROOT), ...snippetAuditDocsPages(ROOT), + ...snippetNestedPackageReadmes(ROOT), ]; - it('walks exactly the documents the snippet gate walks, minus that gate’s three docs/ legs', () => { + it('walks exactly the documents the snippet gate walks, minus that gate’s own legs', () => { const legOnly = new Set(snippetDocsLegs()); expect(fenceDocuments(ROOT)).toEqual(snippetDocuments(ROOT).filter((d: string) => !legOnly.has(d))); }); it('…and that subtraction is non-empty, so it is not silently subtracting nothing', () => { // Each leg separately: a union that is non-empty overall would stay green - // with one of its three members returning nothing at all. - for (const leg of [snippetRootDocsPages(ROOT), snippetAdrDocsPages(ROOT), snippetAuditDocsPages(ROOT)]) { + // with one of its four members returning nothing at all. + for (const leg of [ + snippetRootDocsPages(ROOT), + snippetAdrDocsPages(ROOT), + snippetAuditDocsPages(ROOT), + snippetNestedPackageReadmes(ROOT), + ]) { expect(leg.length).toBeGreaterThan(0); } // Every subtracted document really is on the snippet gate's side only. @@ -192,6 +212,53 @@ describe('check-doc-fence-languages: the scan surface is check-doc-snippet-types } }); + /** + * objectui#7308's leg, pinned where it stops rather than only that it is + * non-empty. + * + * Two claims a reading of the code cannot make: that the leg collects a README + * BELOW a package root and never the package's own top-level one (the + * no-double-collect guarantee objectui#6026 got structurally, by rooting the + * walk at each package's SUBdirectories), and that it does not follow pnpm's + * workspace symlinks out of the authored tree. The second is not hypothetical: + * `statSync` follows symlinks, and `packages/a/node_modules/@object-ui/b` leads + * back into `packages/b`, so an unguarded walk does not terminate. + */ + it('the nested-README leg collects below package roots only, and the tracked tree exactly', () => { + expect(SNIPPET_NESTED_PACKAGE_READMES).toEqual({ + dir: 'packages', + name: 'README.md', + recursive: true, + }); + const nested = snippetNestedPackageReadmes(ROOT); + // Below a package root, every one of them — never `packages//README.md`. + for (const doc of nested) { + expect(doc, `${doc} is not a README.md`).toMatch(/\/README\.md$/); + expect(doc, `${doc} sits at a package root, so the leg above already has it`).not.toMatch( + /^packages\/[^/]+\/README\.md$/, + ); + expect(doc.split('/'), `${doc} left the authored tree`).not.toContain('node_modules'); + } + // The leg plus the top-level README leg is EXACTLY the tracked population — + // measured against git, not against a hand-written list, so the walk cannot + // quietly gain a generated page or lose an authored one. + const tracked = execFileSync('git', ['ls-files', '--', 'packages/'], { cwd: ROOT, encoding: 'utf8' }) + .split('\n') + .filter((f) => /(^|\/)README\.md$/.test(f)) + .sort(); + const walked = snippetDocuments(ROOT) + .filter((d: string) => d.startsWith('packages/') && d.endsWith('/README.md')) + .sort(); + expect(walked).toEqual(tracked); + // Non-vacuous on both halves: the tracked population is not empty, and it + // really does contain pages at both depths. + expect(tracked.length).toBeGreaterThan(0); + expect(nested.length).toBeGreaterThan(0); + expect(tracked.filter((f) => /^packages\/[^/]+\/README\.md$/.test(f)).length).toBeGreaterThan(0); + // No document is collected twice by the two package legs together. + expect(new Set(walked).size).toBe(walked.length); + }); + /** * The boundary, pinned on the legs themselves. `recursive: false` on one and * `recursive: true` on the other two are claims about where each surface stops, diff --git a/scripts/__tests__/check-doc-snippet-types.test.ts b/scripts/__tests__/check-doc-snippet-types.test.ts index bdbfc96fe0..b807fded2a 100644 --- a/scripts/__tests__/check-doc-snippet-types.test.ts +++ b/scripts/__tests__/check-doc-snippet-types.test.ts @@ -12,6 +12,7 @@ import ts from 'typescript'; import { ADR_DOCS, AUDIT_DOCS, + NESTED_PACKAGE_READMES, EXIT_CODES, FRAGMENT_MARKER_EXAMPLES, ROOT_DECLARED_CONTROL_PACKAGE, @@ -27,6 +28,7 @@ import { listDocuments, moduleSpecifiersOf, moduleSpecifiersOfBlock, + nestedPackageReadmePages, resolvesOnlyThroughRootManifest, ROOT_DOCS, adrDocsPages, @@ -905,6 +907,215 @@ describe('objectui#7856 card 2 — docs/adr/** and docs/audits/** are in the sca }); }); +/** + * objectui#7308 — every `README.md` under `packages/`, at any depth, ledger-first. + * + * The defect this closes was a SPECIFICATION defect rather than drift: the + * header's SCAN SURFACE paragraph said `every packages//README.md`, one + * level, literally, and `listDocuments` implemented that sentence faithfully. So + * four nested pages were neither compiled nor ledgered — objectui#5174's "neither + * covered NOR declared ungated", one directory down — and `check-doc-links` had + * already closed the identical hole on the identical four files (objectui#6026). + * + * The proof this block owes is card 2's shape rather than card 1's, because this + * widening also lands LEDGER-FIRST: three of the four pages carry blocks that do + * not compile today, so showing them in the compiled tier is a claim this card + * may not make. What it asserts instead: + * + * 1. the pages are really in the walk, and the walk is EXACTLY the tracked + * population under `packages/` — measured against `git ls-files` rather than + * a hand-written list, which is the only version of this assertion that + * notices a fifth page landing tomorrow; + * 2. every one of them that holds a `ts` / `tsx` block is on the ledger, and + * every one that holds none is COVERED and ledgered nowhere — the second + * half is not decoration, it is why this card writes THREE rows for FOUR + * pages, and it is the mechanical refutation of "keep the ledger short by + * leaving the page outside the surface"; + * 3. the leg cannot double-collect a package's own top-level `README.md`, which + * objectui#6026 got structurally by rooting the walk one directory down; + * 4. ⚠️ the walk does not follow pnpm's workspace symlinks out of the authored + * tree. This is the one hazard no other leg in this file has: every other + * recursive walk here crosses an authored tree with nothing generated inside + * it, while `packages/` has a `node_modules/` per package whose entries are + * SYMLINKS to sibling workspace packages — and `statSync` follows symlinks, + * so `packages/a/node_modules/@object-ui/b` leads back into `packages/b` and + * onward forever. Measured on `9ba7e9c3` with the workspace installed: 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. The fixture below reproduces that cycle in + * miniature, so the guard is asserted rather than trusted. + * + * ⛔ What is deliberately NOT asserted: a `main()` refusal when `packages/` is + * missing, of the kind `ROOT_DOCS` and the two subtree legs carry. This leg walks + * the SAME directory the top-level package-README leg has always walked, and that + * leg has never had one — introducing a new precondition on the shared directory + * is a different change from widening the depth this one reads. The vacuity floor + * that does apply is this file's own "scans a plausible number of documents". + */ +describe('objectui#7308 — the nested package READMEs are in the scan set, ledger-first', () => { + const trackedPackageReadmes = () => + spawnSync('git', ['ls-files', '--', 'packages/'], { cwd: repoRoot, encoding: 'utf8' }) + .stdout.split('\n') + .filter((f) => /(^|\/)README\.md$/.test(f)) + .sort(); + + it('listDocuments reaches the nested pages, and the walk IS the tracked population', () => { + const documents = listDocuments(repoRoot); + const nested = nestedPackageReadmePages(repoRoot); + // Non-vacuous: the leg really finds pages, and they really are in the walk. + expect(nested.length).toBeGreaterThan(0); + for (const doc of nested) expect(documents).toContain(doc); + // Exactly the tracked population — no generated page gained, none lost. + const walked = documents.filter((d) => d.startsWith('packages/') && d.endsWith('/README.md')).sort(); + const tracked = trackedPackageReadmes(); + expect(tracked.length).toBeGreaterThan(0); + expect(walked).toEqual(tracked); + // Both depths are really represented, so the equality above is not green on + // a population that happens to be flat. + expect(tracked.some((f) => /^packages\/[^/]+\/README\.md$/.test(f))).toBe(true); + expect(tracked.some((f) => !/^packages\/[^/]+\/README\.md$/.test(f))).toBe(true); + }); + + it('collects BELOW a package root only, so the top-level leg cannot double-collect', () => { + for (const doc of nestedPackageReadmePages(repoRoot)) { + expect(doc, `${doc} sits at a package root`).not.toMatch(/^packages\/[^/]+\/README\.md$/); + } + const walked = listDocuments(repoRoot).filter((d) => d.startsWith('packages/')); + expect(new Set(walked).size).toBe(walked.length); + }); + + it('the widening is VISIBLE to the accounting: block-bearing pages are ledgered, block-free ones are covered', () => { + const state = analyze({}) as { + scans: Map; + covered: string[]; + }; + const nested = nestedPackageReadmePages(repoRoot); + const withBlocks = nested.filter((doc) => (state.scans.get(doc)?.blocks.length ?? 0) > 0); + const withoutBlocks = nested.filter((doc) => (state.scans.get(doc)?.blocks.length ?? 0) === 0); + // Non-vacuous on BOTH halves — this is the assertion that says why three rows + // were written for four pages. + expect(withBlocks.length).toBeGreaterThan(0); + expect(withoutBlocks.length).toBeGreaterThan(0); + expect([...withBlocks].sort()).toEqual( + Object.keys(UNGATED_DOCS as Record) + .filter((doc) => nested.includes(doc)) + .sort(), + ); + // A page with no ts/tsx block is COVERED at zero blocks and may not be + // ledgered: the stale-entry check would refuse it, which is exactly why + // leaving it out of the surface to keep the ledger short is not available. + for (const doc of withoutBlocks) expect(state.covered).toContain(doc); + }); + + it('every new ledger row carries a measured count, the phases, and what would have to change', () => { + const nested = new Set(nestedPackageReadmePages(repoRoot)); + const entries = Object.entries(UNGATED_DOCS as Record).filter(([doc]) => nested.has(doc)); + expect(entries.length).toBeGreaterThan(0); + for (const [doc, reason] of entries) { + expect(reason, `${doc}: names no block count`).toMatch(/\d+ `tsx?` blocks?/); + expect(reason, `${doc}: names no diagnostic count`).toMatch(/\d+ diagnostics/); + expect(reason, `${doc}: names no diagnostic code`).toMatch(/TS\d{4}/); + expect(reason, `${doc}: does not say which phase was measured`).toMatch(/syntax-phase|semantic-phase/); + expect(reason, `${doc}: does not say what would have to change`).toMatch(/What would have to change|would have to change/); + } + }); + + it('descends below a package root, and stops at the directories that hold no prose', () => { + const root = tempTree({ + 'packages/alpha/README.md': '# top level, the OTHER leg has this one\n', + 'packages/alpha/src/zod/README.md': '# nested\n', + 'packages/alpha/docs/verification/README.md': '# nested, deeper\n', + 'packages/alpha/src/NOTES.md': '# not a README, in no leg\n', + 'packages/alpha/dist/README.md': '# build output\n', + 'packages/alpha/node_modules/dep/README.md': '# an installed dependency\n', + 'packages/beta/README.md': '# another package root\n', + }); + try { + expect(nestedPackageReadmePages(root)).toEqual([ + 'packages/alpha/docs/verification/README.md', + 'packages/alpha/src/zod/README.md', + ]); + const documents = listDocuments(root); + // The top-level leg still has the package roots, exactly once each. + expect(documents.filter((d) => d === 'packages/alpha/README.md')).toEqual([ + 'packages/alpha/README.md', + ]); + expect(documents).toContain('packages/beta/README.md'); + // Not a README, and therefore in no leg — this card widened the depth the + // README rows read, and nothing else. + expect(documents).not.toContain('packages/alpha/src/NOTES.md'); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); + + it('does not follow a workspace symlink out of the authored tree — the walk terminates', () => { + const root = tempTree({ + 'packages/alpha/README.md': '# alpha\n', + 'packages/alpha/src/zod/README.md': '# the one real nested page\n', + 'packages/beta/README.md': '# beta\n', + 'packages/beta/src/adapters/README.md': '# beta nested\n', + }); + try { + // pnpm's shape, in miniature: each package's node_modules links to its + // sibling, so an unguarded walk loops alpha -> beta -> alpha forever. + fs.mkdirSync(path.join(root, 'packages/alpha/node_modules/@object-ui'), { recursive: true }); + fs.mkdirSync(path.join(root, 'packages/beta/node_modules/@object-ui'), { recursive: true }); + fs.symlinkSync( + path.join(root, 'packages/beta'), + path.join(root, 'packages/alpha/node_modules/@object-ui/beta'), + 'dir', + ); + fs.symlinkSync( + path.join(root, 'packages/alpha'), + path.join(root, 'packages/beta/node_modules/@object-ui/alpha'), + 'dir', + ); + // Terminates, and yields the authored pages only — each exactly once. + expect(nestedPackageReadmePages(root)).toEqual([ + 'packages/alpha/src/zod/README.md', + 'packages/beta/src/adapters/README.md', + ]); + // The control that makes the assertion above a reading: with the guard + // removed the SAME tree is a cycle, so an unguarded walk cannot finish. It + // is shown here bounded by depth rather than run to exhaustion. + const unguarded = (dir: string, depth: number): number => { + if (depth > 8) return 1; + let hits = 0; + for (const entry of fs.readdirSync(dir).sort()) { + const full = path.join(dir, entry); + if (fs.statSync(full).isDirectory()) hits += unguarded(full, depth + 1); + else if (entry === 'README.md') hits += 1; + } + return hits; + }; + expect(unguarded(path.join(root, 'packages'), 0)).toBeGreaterThan( + nestedPackageReadmePages(root).length, + ); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); + + it('an absent packages/ tree yields nothing here rather than throwing', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'doc-snippet-gate-nopkg-')); + try { + expect(nestedPackageReadmePages(root)).toEqual([]); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + expect(NESTED_PACKAGE_READMES).toEqual({ dir: 'packages', name: 'README.md', recursive: true }); + }); + + it('the header states the widened surface, so the specification cannot drift back', () => { + const source = fs.readFileSync(path.join(repoRoot, 'scripts/check-doc-snippet-types.mjs'), 'utf8'); + // The SCAN SURFACE paragraph is where objectui#7308's defect lived: it said + // `every packages//README.md`, and the walk implemented that sentence. + expect(source).toMatch(/every `README\.md` under `packages\/` AT ANY DEPTH/); + expect(source).not.toMatch(/every `packages\/\/README\.md`, every/); + }); +}); + describe('third-party resolution reaches exactly as far as the imported packages declare', () => { /** A workspace package with its own `node_modules`, the way pnpm links one. */ function treeWithDependency(files: Record = {}): string { diff --git a/scripts/check-doc-snippet-types.mjs b/scripts/check-doc-snippet-types.mjs index a984be9f4e..8265663f7d 100644 --- a/scripts/check-doc-snippet-types.mjs +++ b/scripts/check-doc-snippet-types.mjs @@ -424,11 +424,20 @@ * be read off the collector: * * every `.mdx` and `.md` page under `content/docs`, every page under an - * `apps//docs` tree, every `packages//README.md`, every `.md` / - * `.mdx` page at the TOP LEVEL of the repository-root `docs/` tree - * (objectui#7856 card 1), every page under `docs/adr/**` and under - * `docs/audits/**` (objectui#7856 card 2, recursively), and the root - * `README.md`. + * `apps//docs` tree, every `README.md` under `packages/` AT ANY DEPTH + * (objectui#7308), every `.md` / `.mdx` page at the TOP LEVEL of the + * repository-root `docs/` tree (objectui#7856 card 1), every page under + * `docs/adr/**` and under `docs/audits/**` (objectui#7856 card 2, + * recursively), and the root `README.md`. + * + * ⚠️ The package-README clause used to read `every packages//README.md`, + * one level, literally — and that is what objectui#7308 reported: the sentence + * above is where the surface is SPECIFIED, so a walk that stopped at the package + * root was not drift from this paragraph, it was this paragraph drawn too small. + * Four nested pages were therefore neither compiled nor ledgered, i.e. neither + * covered NOR declared ungated, one directory down from where objectui#5174 + * found the same state one level up. The sibling gate `check-doc-links` had + * already closed the identical hole on the identical four files (objectui#6026). * * ⚠️ Card 2 is the widening whose whole delivery is the LEDGER, and reading it * as coverage would be reading it backwards. Every page in those two subtrees @@ -700,6 +709,101 @@ export function auditDocsPages(root) { return subtreeDocPages(root, AUDIT_DOCS); } +/** + * The package-README leg, at EVERY depth (objectui#7308). + * + * The leg above this one — `packages//README.md` — was stated in this + * header's SCAN SURFACE paragraph as if the one level were the specification, + * so this was never implementation drift: the specification itself was drawn + * too small, and a page one directory deeper was "neither covered NOR declared + * ungated" — objectui#5174's phrase for the state that is strictly worse than a + * named debt, arriving here one directory down instead of one level up. + * + * Re-derived on `9ba7e9c3`, naming the population in words each time: of the 43 + * tracked files under `packages/` whose basename is `README.md`, 39 sit at a + * package root and 4 sit deeper, so 4 pages were in no gate's accounting. They + * are not private notes — `packages/types` lists the whole of `src/` in its + * manifest `files`, so `packages/types/src/zod/README.md` is inside the npm + * tarball a reader downloads. + * + * objectui#6026 closed exactly this hole, on exactly these four files, in the + * sibling gate `check-doc-links` — same defect, same count, different gate. This + * is that fix arriving here. + * + * ### Why this is its own leg rather than a `recursive` flag on the one above + * + * Two reasons, and the first is the same one `ADR_DOCS` / `AUDIT_DOCS` carry: a + * divergence between this gate's walk and `check-doc-fence-languages`' has to be + * something a pin can SUBTRACT by import rather than a hand-written list of + * today's four filenames, so the guard's walk-equality test keeps failing on any + * OTHER drift. The second is objectui#6026's own no-double-parse guarantee, + * inherited structurally: this leg is rooted at the SUBdirectories of each + * package, so a package's own top-level `README.md` is not inside any of them + * and cannot be collected twice. Nothing here has a notion of "top level". + * + * ### ⚠️ Why the skip set is load-bearing, measured rather than assumed + * + * Every other recursive leg in this file walks an authored tree with nothing + * generated inside it. `packages/` is not that tree. Under pnpm each package has + * its own `node_modules/` holding SYMLINKS to its workspace siblings, and + * `statSync` follows symlinks — so `packages/a/node_modules/@object-ui/b` leads + * back into `packages/b`, whose own `node_modules` leads onward. Measured on + * `9ba7e9c3` with the workspace installed: an unguarded recursive 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 `UNSCANNED_DIRS` applied the walk yields exactly 43 — the tracked + * population, to the file. + */ +export const NESTED_PACKAGE_READMES = { dir: PACKAGES_DIR, name: 'README.md', recursive: true }; + +/** + * Directory names this file's `packages/` walk never enters. + * + * The same set, for the same reason, that `check-doc-links.mjs` walks its own + * disk surfaces with (`UNSCANNED_DIRS` there): none of these holds authored + * prose — they hold installed dependencies and build output — and the first of + * them is what makes the walk above terminate at all. + */ +const UNSCANNED_DIRS = new Set(['node_modules', 'dist', 'build', '.next', '.turbo', '.git']); + +/** + * Every `README.md` strictly BELOW a package's own root, in a stable order. + * + * An absent `packages/` yields `[]` so a throwaway fixture tree stays listable, + * exactly as the legs above do. + * + * Exported so a sibling census can ask this gate what this leg contains instead + * of re-spelling it — which is what `check-doc-fence-languages.test.ts` does: the + * fence guard does NOT carry this leg (moving `check:doc-fences`' surface is not + * this card), and its walk-equality pin subtracts this enumerator BY IMPORT. + */ +export function nestedPackageReadmePages(root) { + const base = join(root, NESTED_PACKAGE_READMES.dir); + if (!existsSync(base) || !statSync(base).isDirectory()) return []; + const out = []; + const walk = (dir) => { + for (const entry of readdirSync(dir).sort()) { + const p = join(dir, entry); + if (statSync(p).isDirectory()) { + if (!UNSCANNED_DIRS.has(entry)) walk(p); + continue; + } + if (entry === NESTED_PACKAGE_READMES.name) out.push(relative(root, p).split(sep).join('/')); + } + }; + for (const entry of readdirSync(base).sort()) { + const pkg = join(base, entry); + if (!statSync(pkg).isDirectory()) continue; + // Rooted at each package's SUBdirectories: the package's own README.md is + // not inside any of them, so the leg above cannot double-collect it. + for (const sub of readdirSync(pkg).sort()) { + const p = join(pkg, sub); + if (statSync(p).isDirectory() && !UNSCANNED_DIRS.has(sub)) walk(p); + } + } + return out; +} + /** Fence languages treated as compilable TypeScript. `js` / `jsx` are NOT in the * set: they are not type-annotated, so a strict program judges them on rules * their authors never opted into. */ @@ -718,12 +822,14 @@ const TS_FENCE_LANGUAGES = new Set(['ts', 'tsx', 'typescript']); * apps//docs/** ✓ ✓ ✓ objectui#6600 * README.md ✓ ✓ ✓ objectui#7115 * packages//README.md ✓ ✓ ✗ ships inside `files` + * nested packages README.md ✗ ✓ ✗ objectui#7308 * docs/*.md (top level only) ✗ ✓ ✗ objectui#7856 card 1 * docs/adr/** ✗ ✓ ✗ objectui#7856 card 2 * docs/audits/** ✗ ✓ ✗ objectui#7856 card 2 * - * The three `docs/` rows are the legs THIS gate carries alone, and the asymmetry - * is deliberate rather than an oversight to be tidied up later: objectui#7856 + * The three `docs/` rows and the nested-README row are the legs THIS gate carries + * alone, and the asymmetry is deliberate rather than an oversight to be tidied up + * later: objectui#7856 * card 1 moves this gate's population only, so `check-doc-fence-languages` and * `check-doc-component-types` keep the surface they had. `check-doc-fence- * languages.test.ts` therefore no longer compares the two walks for equality @@ -735,6 +841,12 @@ const TS_FENCE_LANGUAGES = new Set(['ts', 'tsx', 'typescript']); * where it stops, and `docs/adr/**` being GOVERNED is the reason the boundary * between the rows is worth a line of code rather than a comment. * + * objectui#7308's nested-README row extends that same subtraction with + * `nestedPackageReadmePages()` — a fourth enumerator, imported rather than + * re-spelled — for the reason card 1 exported its own: moving `check:doc-fences`' + * surface is not this card's to do, so the divergence is NAMED and every OTHER + * drift between the two walks still fails that pin. + * * `check-doc-component-types` does not read the package READMEs — it asks * whether a documented `type` literal is a registered component key, and a * package README teaches its own package's API rather than the schema vocabulary. @@ -744,7 +856,8 @@ const TS_FENCE_LANGUAGES = new Set(['ts', 'tsx', 'typescript']); * ⚠️ EVERYTHING ELSE authored in markdown is read by no doc gate at all. That is * a statement of what the roots are today, ⛔ not a plan and not a promise. In * descending order of size, the unscanned population is: non-README `.md` under - * `packages/**` (by far the largest); the PUBLISHED + * `packages/**`, at any depth, which stays the largest of them — objectui#7308 + * brought the nested `README.md` files in and NOTHING else; the PUBLISHED * `skills/objectui/**`; the root pages that are not `README.md` (`AGENTS.md`, * `CONTRIBUTING.md`, `ROADMAP.md` and the rest); `examples/**`; the `apps/**` * pages that are not under an `apps//docs/` tree; `.claude/**`; @@ -764,7 +877,7 @@ const TS_FENCE_LANGUAGES = new Set(['ts', 'tsx', 'typescript']); * "which": * * git ls-files '*.md' '*.mdx' \ - * | grep -vE '^(content/docs/|apps/[^/]+/docs/|packages/[^/]+/README\.md$|README\.md$|docs/[^/]+\.mdx?$|docs/adr/|docs/audits/|\.changeset/)' + * | grep -vE '^(content/docs/|apps/[^/]+/docs/|packages/.*README\.md$|README\.md$|docs/[^/]+\.mdx?$|docs/adr/|docs/audits/|\.changeset/)' * * ⚠️ A subdirectory of `docs/` that is NEITHER `adr/` NOR `audits/` is in no leg * and therefore still in that population — the exclusion above names the two @@ -944,6 +1057,66 @@ const TS_FENCE_LANGUAGES = new Set(['ts', 'tsx', 'typescript']); * @type {Record} */ const UNGATED_DOCS = { + // objectui#7308 — the nested package READMEs, LEDGER-FIRST. Measured on + // `9ba7e9c3` with this gate's own analyzer against the closure `--build-filter` + // names (35/35 turbo tasks successful): `analyze({ ungated: {} })` for the + // population, `compileSnippets()` for the phases, with the surface widened and + // these rows NOT yet written — which is the only order in which the numbers + // below are readings rather than justifications. + // + // The census, with each population named: the widening adds 4 documents + // (245 -> 249 in the scan set) carrying 20 `ts`/`tsx` blocks, of which 13 fail + // — 3 in the syntax phase and 10 in the semantic phase — for 37 diagnostics. + // + // ⚠️ Only THREE rows appear below for those four documents, and the missing + // fourth is mechanical rather than an exclusion: `packages/plugin-gantt/docs/ + // verification/README.md` holds no `ts`/`tsx` fenced block at all (its fences + // are `sh`), so it joins the COVERED tier at zero blocks, and a row naming it + // here would fail this gate's own re-derivation as a stale entry — "an entry + // naming a file that ... holds no `ts` / `tsx` block at all, fails as a stale + // entry". ⛔ That is the opposite of keeping the ledger short by quietly leaving + // a page outside the surface, which is the defect objectui#7308 reported. + // + // ⚠️ These three rows are DEBT, not a terminal state — unlike the objectui#7856 + // card 2 rows below, which are records nobody may repair. A package README is a + // page a reader copies from, so every row here can and should leave by the page + // compiling. Paying it down is its own card; ⛔ softening this gate is not a way + // to pay it. + 'packages/components/src/__tests__/README.md': + '1 `ts` block (fence 104), 9 diagnostics, ALL semantic-phase: TS2593 x3, TS2304 x5, TS2552 x1. The ' + + "block is the page's \"Adding New Tests\" pattern: a bare `describe`/`it` body with no imports at all, " + + 'so the Vitest globals (`describe`, `it`, `expect`) and the three local helpers it calls ' + + '(`validateComponentRegistration`, `renderComponent`, `getAllDisplayIssues`) are all undefined names. ' + + 'What would have to change: the block imports `describe`/`it`/`expect` from `vitest` and the three ' + + 'helpers from wherever this test suite ships them — or, if the helpers are suite-local and have no ' + + 'importable home, the block gets a `FRAGMENT_MARKER` saying so.', + 'packages/core/src/adapters/README.md': + '5 `ts` blocks (fences 36, 70, 191, 216, 239); 2 of them compile untouched. 3 fail with 9 diagnostics, ' + + 'ALL semantic-phase: TS2304 x2, TS2355 x6, TS2420 x1. Two are excerpts naming a value the prose ' + + 'introduces but the block never declares (`contextDataSource` at fence 191, `dataSource` at fence 216). ' + + "⚠️ The third is a DOCUMENTED-API defect rather than a snippet-hygiene one, and it is the first thing " + + 'this widening found: the custom-adapter template at fence 239 declares `class MyCustomAdapter ' + + "implements DataSource` while omitting `getObjectSchema`, which `DataSource` requires (TS2420), " + + 'and its six method bodies are `// Your implementation` comments under non-`void` return annotations ' + + '(TS2355 x6). A reader who copies it gets a class that does not satisfy the interface it claims. What ' + + 'would have to change: the template gains `getObjectSchema` and returns a value from each body (or ' + + 'declares the bodies elided), and the two excerpts declare the value they use.', + 'packages/types/src/zod/README.md': + '14 `ts` blocks (fences 88, 110, 140, 151, 220, 245, 266, 281, 289, 301, 324, 334, 354, 368); 5 compile ' + + 'untouched. 9 fail with 19 diagnostics: 3 fail in the syntax-phase (fences 220, 301, 368 — TS1109 x10, ' + + 'so their semantic half is UNMEASURED, not clean) and 6 fail in the semantic-phase (TS2304 x8, ' + + 'TS2307 x1). Three ' + + 'classes, each with its own remedy. (1) Fence 220 is a SHAPE SKETCH — a bare object literal at ' + + "statement position with `?:` optionality markers written on keys and `type: string` standing where a " + + 'value goes; its fence language should be one this gate does not compile. (2) Fences 301 and 368 write ' + + 'the elision `{ ... }` literally, which TypeScript reads as a spread with no operand. (3) The six ' + + 'semantic failures are EXCERPTS that continue an earlier block’s imports — `ButtonSchema`, ' + + '`CardSchema`, `userInput`, `internalConfig`, `useForm`, `registry` — plus one specifier no imported ' + + 'package declares (`@hookform/resolvers/zod`, TS2307), which is the bound this header states rather ' + + 'than a page defect. What would have to change: each excerpt made self-contained against the built ' + + '`dist/*.d.ts`, the two elisions written as real initialisers, and the sketch’s fence relabelled. ' + + '⭐ This page SHIPS: `packages/types` lists the whole of `src/` in its manifest `files`, so it is ' + + 'inside the npm tarball a reader downloads — which is why objectui#7308 filed it first.', // objectui#7856 card 2. Measured on `fedfa3e4` with this gate's own analyzer // against the closure `--build-filter` names (35/35 turbo tasks successful): // `analyze({ ungated: {} })` for the population, `compileSnippets()` for the @@ -1205,6 +1378,9 @@ export function listDocuments(root = repoRoot) { if (existsSync(readme)) out.push(relative(root, readme).split(sep).join('/')); } } + // Every README.md BELOW a package root (objectui#7308), its own leg so the + // fence guard's walk-equality pin can subtract exactly this set by import. + out.push(...nestedPackageReadmePages(root)); // The root `docs/` tree, TOP LEVEL only (objectui#7856 card 1). Enumerated by // directory entry and filtered to files by `rootDocsPages`, so `docs/adr/**` // (governed) and `docs/audits/**` cannot arrive through THIS leg by accident — From d8d39205b15732cc7550175d2a73f42d7f2fa9c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 16:15:10 +0000 Subject: [PATCH 2/2] test(gate): the symlink-cycle pin exercises BOTH halves of the UNSCANNED_DIRS guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr --- scripts/__tests__/check-doc-snippet-types.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/__tests__/check-doc-snippet-types.test.ts b/scripts/__tests__/check-doc-snippet-types.test.ts index b807fded2a..557cf6ff62 100644 --- a/scripts/__tests__/check-doc-snippet-types.test.ts +++ b/scripts/__tests__/check-doc-snippet-types.test.ts @@ -1071,6 +1071,17 @@ describe('objectui#7308 — the nested package READMEs are in the scan set, ledg path.join(root, 'packages/beta/node_modules/@object-ui/alpha'), 'dir', ); + // ⚠️ The guard is written at TWO levels — once on each package's own + // directory entries, once inside the recursive descent — and only the + // second one covers a `node_modules` that is not a package's own. Without + // this deeper cycle the fixture ablates green when the inner guard is + // removed, which would make this assertion a pin on half the guard. + fs.mkdirSync(path.join(root, 'packages/alpha/src/node_modules/@object-ui'), { recursive: true }); + fs.symlinkSync( + path.join(root, 'packages/beta'), + path.join(root, 'packages/alpha/src/node_modules/@object-ui/beta'), + 'dir', + ); // Terminates, and yields the authored pages only — each exactly once. expect(nestedPackageReadmePages(root)).toEqual([ 'packages/alpha/src/zod/README.md',