From eba0a9b7a9049e11baf9ca4a36516c520a03c465 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 05:26:15 +0000 Subject: [PATCH 1/2] feat(scripts): point the three doc gates at apps/*/docs (#6600) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three documentation gates all rooted their walk at `content/docs` (plus, for two of them, the package READMEs and the root `README.md`). None descended into `apps/`, so `apps/console/docs/**` — the console's operator and deployment guides — was read by no documentation gate at all. The only check whose surface contained those files was `check:control-bytes`, which enumerates `git ls-files` and therefore covers every tracked text file: they were checked for control bytes and for nothing else. `check:doc-fences` and `check:doc-snippets` move together because `check-doc-fence-languages.test.ts` pins their document lists deep-equal. `check:doc-types` joins them so that the tree is not left in the split-surface geometry objectui#7115 was filed about, where a file fell between two gates' differing roots and was read by neither. No `UNGATED_DOCS` entry is added and no allowlist mechanism is built; both shrink-only ledgers are byte-identical to `main`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019aCUUSwWefnbCJ4Xk1vqQW --- .changeset/6600-doc-gate-app-docs-roots.md | 10 +++ .../check-doc-fence-languages.test.ts | 44 ++++++++++- scripts/check-doc-component-types.mjs | 57 +++++++++++++- scripts/check-doc-fence-languages.mjs | 41 +++++++++- scripts/check-doc-snippet-types.mjs | 76 +++++++++++++++++++ 5 files changed, 221 insertions(+), 7 deletions(-) create mode 100644 .changeset/6600-doc-gate-app-docs-roots.md diff --git a/.changeset/6600-doc-gate-app-docs-roots.md b/.changeset/6600-doc-gate-app-docs-roots.md new file mode 100644 index 0000000000..5d58fe4fb2 --- /dev/null +++ b/.changeset/6600-doc-gate-app-docs-roots.md @@ -0,0 +1,10 @@ +--- +--- + +Doc gate scan roots only, no published package source changed. + +`check:doc-fences`, `check:doc-snippets` and `check:doc-types` now walk +`apps//docs/**` in addition to `content/docs`, the root `README.md` and +(for the first two) the package READMEs. The three console operator guides were +previously read by no documentation gate at all — `check:control-bytes` was the +only check whose surface contained them. diff --git a/scripts/__tests__/check-doc-fence-languages.test.ts b/scripts/__tests__/check-doc-fence-languages.test.ts index 814732dd37..15c45c9de4 100644 --- a/scripts/__tests__/check-doc-fence-languages.test.ts +++ b/scripts/__tests__/check-doc-fence-languages.test.ts @@ -6,17 +6,22 @@ import { fileURLToPath } from 'node:url'; import { parse as parseYaml } from 'yaml'; import { + APP_DOCS as FENCE_APP_DOCS, census, listDocuments as fenceDocuments, ROOT_PAGES as FENCE_ROOT_PAGES, TS_FENCE_LANGUAGES as GUARD_TS_FENCES, } from '../check-doc-fence-languages.mjs'; import { + APP_DOCS as SNIPPET_APP_DOCS, listDocuments as snippetDocuments, ROOT_PAGES as SNIPPET_ROOT_PAGES, TS_FENCE_LANGUAGES as GATE_TS_FENCES, } from '../check-doc-snippet-types.mjs'; -import { ROOT_PAGES as COMPONENT_ROOT_PAGES } from '../check-doc-component-types.mjs'; +import { + APP_DOCS as COMPONENT_APP_DOCS, + ROOT_PAGES as COMPONENT_ROOT_PAGES, +} from '../check-doc-component-types.mjs'; const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..'); const GUARD = 'scripts/check-doc-fence-languages.mjs'; @@ -89,6 +94,43 @@ describe('check-doc-fence-languages: the scan surface is check-doc-snippet-types // Not implied by the equality above: both lists could lose it together. expect(fenceDocuments(ROOT)).toContain('README.md'); }); + + /** + * objectui#6600 — the `apps//docs/**` half of the surface. + * + * The equality assertion above does NOT cover this: both walks could drop the + * tree together and stay equal, which is precisely the state this card was + * filed about — three gates agreeing with each other about a tree none of them + * opened. `check-doc-component-types`' own header states the rule these two + * assertions implement: "Widening a scan surface is the change that can be + * GREEN ABOUT NOTHING… Anything added here later is owed the same proof." + * + * So membership is pinned by NAME, and the shared constant is pinned across all + * three gates the way `ROOT_PAGES` is. + */ + it('all three doc gates carry the same APP_DOCS — the surface objectui#6600 widened', () => { + expect(FENCE_APP_DOCS).toEqual({ dir: 'apps', subdir: 'docs' }); + expect(SNIPPET_APP_DOCS).toEqual(FENCE_APP_DOCS); + expect(COMPONENT_APP_DOCS).toEqual(FENCE_APP_DOCS); + }); + + it('the apps/*/docs guides are really in the walk — the widening, pinned', () => { + const docs = fenceDocuments(ROOT); + expect(docs).toContain('apps/console/docs/deployment.md'); + expect(docs).toContain('apps/console/docs/error-tracking.md'); + expect(docs).toContain('apps/console/docs/UI_IMPROVEMENT_PROPOSAL.md'); + }); + + /** + * The walk takes ONE app-directory level before the `docs` segment, rather + * than any depth. `apps/site/app/docs` is a Next.js route directory; + * collecting it would be collecting routes, and the + * only reason nothing breaks today is that it holds `.tsx` rather than `.md`. + * Pinned so a later "make the glob more general" edit has to argue with a test. + */ + it('does not descend into nested route directories that happen to be named docs', () => { + expect(fenceDocuments(ROOT).filter((d) => d.startsWith('apps/site/'))).toEqual([]); + }); }); describe('check-doc-fence-languages: non-vacuity, through the shipped module', () => { diff --git a/scripts/check-doc-component-types.mjs b/scripts/check-doc-component-types.mjs index afee8fe4fa..736ba6c974 100644 --- a/scripts/check-doc-component-types.mjs +++ b/scripts/check-doc-component-types.mjs @@ -209,12 +209,58 @@ const scriptDir = dirname(fileURLToPath(import.meta.url)); // ── Configuration ──────────────────────────────────────────────────────────── -/** Where the teaching prose lives. This gate walks `content/docs` plus the root - * pages named below, and nothing else: not `skills/**`, not the package READMEs +/** Where the teaching prose lives. This gate walks `content/docs`, every + * `apps//docs/**` tree (objectui#6600) and the root pages named below, and + * nothing else: not `skills/**`, not the package READMEs * (`check-doc-snippet-types.mjs` covers those for its own question), not - * `docs/**`. */ + * `docs/**`. The full ownership map for all three doc gates — including the + * trees NO gate reads, and why `skills/**` is deliberately not one of them — is + * stated once in `check-doc-snippet-types.mjs`, beside `UNGATED_DOCS`. */ const DOCS_ROOT = 'content/docs'; +/** + * Per-app documentation trees, `apps//docs/**` (objectui#6600). + * + * ⚠️ This gate joining the move is the one judgement ruling D left to the + * implementing lane, and it is joining at ZERO PRESENT YIELD: the three files + * under `apps/console/docs/**` carry 0 `type` literals today, so this walk finds + * nothing on the day it lands. Stated plainly because the alternative reading — + * that a widened scope was justified by a discovery — is false here. + * + * The argument for joining anyway is the split-surface defect one directory over, + * which this gate has already been burned by ONCE. objectui#7115: this gate + * walked `content/docs`; `check-doc-snippet-types` walked `content/docs` plus the + * package READMEs; the root `README.md` fell BETWEEN the two and was read by + * neither, and it taught the unregistered type `stat-card` four times for as long + * as the example existed. Leaving this gate pointed away from a tree its two + * siblings now read would rebuild that exact geometry, deliberately, in the same + * gate family — and `apps/console/docs/UI_IMPROVEMENT_PROPOSAL.md` is a proposal + * about console UI shape, i.e. the file in that tree most likely to grow the + * first `type` literal. A forward guard at zero yield is what objectui#7115 + * wishes had existed. + * + * ⛔ What this is NOT: a precedent for widening onto any other unscanned tree. + * The population here is three files in a directory two sibling gates are moving + * onto in the same change. No allowlist mechanism exists and none is wanted. + * + * The walk is `apps//docs`, one level of app directory and no deeper before + * the `docs` segment. `apps/site/app/docs` is a Next.js ROUTE directory of `.tsx` + * route files, not a documentation tree. + */ +export const APP_DOCS = { dir: 'apps', subdir: 'docs' }; + +/** Every `apps//docs` directory that exists, in a stable order. */ +export function appDocsDirs(root) { + const appsDir = join(root, APP_DOCS.dir); + if (!existsSync(appsDir)) return []; + const out = []; + for (const entry of readdirSync(appsDir).sort()) { + const docs = join(appsDir, entry, APP_DOCS.subdir); + if (existsSync(docs) && statSync(docs).isDirectory()) out.push(docs); + } + return out; +} + /** * Pages at the repository ROOT that join the walk by name. * @@ -999,7 +1045,10 @@ export function deriveRegistryKeys(root, options = {}) { */ export function scanDocs(root) { const docsDir = join(root, DOCS_ROOT); - const files = walkFiles(docsDir, (f) => DOC_EXTENSIONS.some((ext) => f.endsWith(ext))).sort(); + const isDoc = (f) => DOC_EXTENSIONS.some((ext) => f.endsWith(ext)); + const files = walkFiles(docsDir, isDoc).sort(); + // Per-app docs trees (objectui#6600), appended sorted after the content tree. + for (const dir of appDocsDirs(root)) files.push(...walkFiles(dir, isDoc).sort()); // Root pages join by name rather than by walk. An absent one is dropped here so // a throwaway fixture tree stays scannable; the CLI refuses to publish a // verdict when one is missing from a real run, which is where that must bite. diff --git a/scripts/check-doc-fence-languages.mjs b/scripts/check-doc-fence-languages.mjs index 7988dc66c4..de67f1db1b 100644 --- a/scripts/check-doc-fence-languages.mjs +++ b/scripts/check-doc-fence-languages.mjs @@ -118,8 +118,13 @@ * ## What it reads, and what it deliberately does not * * The scan surface is `check-doc-snippet-types`'s, exactly: every `.mdx` and - * `.md` under `content/docs`, every `packages//README.md`, and the root - * `README.md` (objectui#7115). It is re-implemented here rather than imported so + * `.md` under `content/docs`, every `packages//README.md`, the root + * `README.md` (objectui#7115), and every `.mdx` / `.md` under + * `apps//docs/**` (objectui#6600). The full ownership map for all three doc + * gates — including the trees NO gate reads — is stated once in + * `check-doc-snippet-types.mjs`, beside `UNGATED_DOCS`; this gate's roots are + * that gate's roots by construction, which is the pin below. + * It is re-implemented here rather than imported so * this gate needs NO install — that gate imports `typescript`, and an * install-gated docs check is one that a docs-only pull request skips, which is * the shape objectui#5174 and `doc-component-types.yml`'s header both record as @@ -153,6 +158,35 @@ const DOCS_ROOT = 'content/docs'; const PACKAGES_DIR = 'packages'; const DOC_EXTENSIONS = ['.mdx', '.md']; +/** + * Per-app documentation trees, `apps//docs/**` (objectui#6600). + * + * ⛔ Deliberately a COPY of `check-doc-snippet-types.mjs`'s constant, for the same + * reason `ROOT_PAGES` below is one: importing anything from that module pulls in + * its `import ts from 'typescript'` at load, and this gate's whole value is that + * it runs with no install. Exported so the equality is checked rather than hoped + * for — `check-doc-fence-languages.test.ts` pins all three gates' copies. + * + * The walk is `apps//docs`, one level of app directory and no deeper before + * the + * `docs` segment: `apps/site/app/docs` is a Next.js ROUTE directory holding + * `.tsx` route files, not a documentation tree, and a `**`-shaped walk that + * happened to pick it up would be collecting routes. + */ +export const APP_DOCS = { dir: 'apps', subdir: 'docs' }; + +/** Every `apps//docs` directory that exists, in a stable order. */ +export function appDocsDirs(root) { + const appsDir = join(root, APP_DOCS.dir); + if (!existsSync(appsDir)) return []; + const out = []; + for (const entry of readdirSync(appsDir).sort()) { + const docs = join(appsDir, entry, APP_DOCS.subdir); + if (existsSync(docs) && statSync(docs).isDirectory()) out.push(docs); + } + return out; +} + /** * Pages at the repository ROOT that join the scan set by name (objectui#7115). * @@ -179,6 +213,9 @@ export function listDocuments(root = repoRoot) { }; const docsRoot = join(root, DOCS_ROOT); if (existsSync(docsRoot)) walk(docsRoot); + // Per-app docs trees, in the same slot the snippet gate appends them in — + // the coupling pin compares the two lists element by element. + for (const dir of appDocsDirs(root)) walk(dir); const pkgDir = join(root, PACKAGES_DIR); if (existsSync(pkgDir)) { for (const entry of readdirSync(pkgDir).sort()) { diff --git a/scripts/check-doc-snippet-types.mjs b/scripts/check-doc-snippet-types.mjs index 1945ac294d..bc7ddb46bd 100644 --- a/scripts/check-doc-snippet-types.mjs +++ b/scripts/check-doc-snippet-types.mjs @@ -301,6 +301,42 @@ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const DOCS_ROOT = 'content/docs'; const PACKAGES_DIR = 'packages'; +/** + * Per-app documentation trees, `apps//docs/**` (objectui#6600). + * + * That card measured the hole: the three doc gates all rooted at `content/docs`, + * so `apps/console/docs/**` — the console's operator and deployment guides — was + * read by NO doc gate. The only check whose surface contained those files was + * `check:control-bytes`, which enumerates `git ls-files` and therefore covers + * every tracked text file, i.e. they were checked for control bytes and for + * nothing else. What accumulated there is objectui#6599: a guide that had drifted + * far enough that following it literally rebuilt the ungated telemetry init + * objectui#5522 deliberately removed, plus a fabricated CSP section and two env + * vars with zero read sites. Nothing mechanical could have noticed any of it. + * + * The walk is `apps//docs`, one level of app directory and no deeper before + * the + * `docs` segment. `apps/site/app/docs` is a Next.js ROUTE directory holding + * `.tsx` route files, not a documentation tree; a `**`-shaped walk that picked it + * up would be collecting routes. + * + * Exported so the equality is checked rather than hoped for: three gates carry + * this constant and `check-doc-fence-languages.test.ts` pins all three copies. + */ +export const APP_DOCS = { dir: 'apps', subdir: 'docs' }; + +/** Every `apps//docs` directory that exists, in a stable order. */ +export function appDocsDirs(root) { + const appsDir = join(root, APP_DOCS.dir); + if (!existsSync(appsDir)) return []; + const out = []; + for (const entry of readdirSync(appsDir).sort()) { + const docs = join(appsDir, entry, APP_DOCS.subdir); + if (existsSync(docs) && statSync(docs).isDirectory()) out.push(docs); + } + return out; +} + /** * Pages at the repository ROOT that join the scan set by name. * @@ -343,6 +379,43 @@ const DOC_EXTENSIONS = ['.mdx', '.md']; const TS_FENCE_LANGUAGES = new Set(['ts', 'tsx', 'typescript']); /** + * ── What the three doc gates own, and what nothing owns (objectui#6600) ────── + * + * Stated once, here, because this gate has the widest surface and holds the + * coverage ledger below. The other two headers state their own roots and point + * at this block. + * + * root fences · snippets · types + * ─────────────────────────── ───────────────────────── + * content/docs/** ✓ ✓ ✓ + * apps//docs/** ✓ ✓ ✓ objectui#6600 + * README.md ✓ ✓ ✓ objectui#7115 + * packages//README.md ✓ ✓ ✗ ships inside `files` + * + * `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. + * That is the ONE deliberate asymmetry, and it is why that gate cannot join the + * document-list equality pin the other two share. + * + * ⚠️ 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. When + * this block was written the unscanned population was 114 files (excluding the + * ephemeral `.changeset/`), the largest groups being non-README `.md` under + * `packages/**` (54), `docs/**` ADRs and audits (17), and the PUBLISHED + * `skills/objectui/**` (16). Re-derive it rather than trusting that number, + * which drifts with every batch: + * + * git ls-files '*.md' '*.mdx' \ + * | grep -vE '^(content/docs/|apps/[^/]+/docs/|packages/[^/]+/README\.md$|README\.md$|\.changeset/)' + * + * ⛔ `skills/objectui/**` is NOT claimed by any gate here, and this line is the + * opposite of a claim on it: it is a governed, published surface with its own + * review path, so pointing a doc gate at it is a decision for whoever owns that + * surface — never a side effect of a root move. Writing an unscanned tree down + * is what keeps it a KNOWN debt; a tree nobody names is objectui#5174's + * "neither covered NOR declared ungated", which is strictly worse. + * * Documents whose snippets are NOT compiled, each with the reason. The default * is covered; this list is the debt, by name, and it can only shrink. * @@ -679,6 +752,9 @@ export function listDocuments(root = repoRoot) { }; const docsRoot = join(root, DOCS_ROOT); if (existsSync(docsRoot)) walk(docsRoot); + // Per-app docs trees, in the same slot the fence guard appends them in — the + // coupling pin compares the two lists element by element. + for (const dir of appDocsDirs(root)) walk(dir); const pkgDir = join(root, PACKAGES_DIR); if (existsSync(pkgDir)) { for (const entry of readdirSync(pkgDir).sort()) { From 126a6318d060960735fb5d3d3f1c02bf43105098 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 05:51:16 +0000 Subject: [PATCH 2/2] docs(scripts): drop the frozen counts from the doc-gate ownership map (#6600) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first draft of the ownership map stated the unscanned markdown population as 114 files. The re-derivation command the same block ships answers 111, at both `origin/main` and this branch: the 114 counted the three `apps//docs/` guides that this very pull request brings under the gates. Rather than correct 114 to 111, this removes every hand-copied count from the block — the total and the per-tree ones alike. A number in a header drifts from the tree and nothing fails when it does, which is objectui#7448 exactly, and which `UNGATED_DOCS`'s own header already records happening to its `12 .mdx pages and 32 package READMEs` sentence. The trees are now named in descending order of size, which is the ownership statement the ruling asked for, and the command is left as the durable answer to both "how many" and "which". Comment-only: no code line changes, and all three gates report identical numbers before and after. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019aCUUSwWefnbCJ4Xk1vqQW --- scripts/check-doc-snippet-types.mjs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/scripts/check-doc-snippet-types.mjs b/scripts/check-doc-snippet-types.mjs index bc7ddb46bd..c4cc82ce4c 100644 --- a/scripts/check-doc-snippet-types.mjs +++ b/scripts/check-doc-snippet-types.mjs @@ -399,12 +399,26 @@ const TS_FENCE_LANGUAGES = new Set(['ts', 'tsx', 'typescript']); * document-list equality pin the other two share. * * ⚠️ 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. When - * this block was written the unscanned population was 114 files (excluding the - * ephemeral `.changeset/`), the largest groups being non-README `.md` under - * `packages/**` (54), `docs/**` ADRs and audits (17), and the PUBLISHED - * `skills/objectui/**` (16). Re-derive it rather than trusting that number, - * which drifts with every batch: + * 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); `docs/**` (ADRs and audits); 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/**`; + * `.github/**`; and `patches/**`. The ephemeral `.changeset/` is excluded as + * noise rather than counted as debt. + * + * ⛔ Deliberately NO count is written here, neither a total nor a per-tree one. + * That is not laziness, it is objectui#7448's defect avoided at the source: a + * hand-copied number in a header drifts from the tree and nothing fails when it + * does, which is the same lesson `UNGATED_DOCS`'s own header records after both + * halves of its `12 .mdx pages and 32 package READMEs` went stale ("a pointer to + * the list now rather than a copy of its length"). The first draft of THIS block + * proved the point inside a single pull request: it said 114, counting the three + * `apps//docs/` guides that the very same change was bringing under the + * gates. + * The command below is the durable answer, and it answers both "how many" and + * "which": * * git ls-files '*.md' '*.mdx' \ * | grep -vE '^(content/docs/|apps/[^/]+/docs/|packages/[^/]+/README\.md$|README\.md$|\.changeset/)'