Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/6600-doc-gate-app-docs-roots.md
Original file line numberDiff line numberDiff line change
@@ -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/<app>/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.
44 changes: 43 additions & 1 deletion scripts/__tests__/check-doc-fence-languages.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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/<app>/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', () => {
Expand Down
57 changes: 53 additions & 4 deletions scripts/check-doc-component-types.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<app>/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/<app>/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/<app>/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/<app>/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.
*
Expand DownExpand Up@@ -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.
Expand Down
41 changes: 39 additions & 2 deletions scripts/check-doc-fence-languages.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<name>/README.md`, and the root
* `README.md` (objectui#7115). It is re-implemented here rather than imported so
* `.md` under `content/docs`, every `packages/<name>/README.md`, the root
* `README.md` (objectui#7115), and every `.mdx` / `.md` under
* `apps/<app>/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
Expand DownExpand Up@@ -153,6 +158,35 @@ const DOCS_ROOT = 'content/docs';
const PACKAGES_DIR = 'packages';
const DOC_EXTENSIONS = ['.mdx', '.md'];

/**
* Per-app documentation trees, `apps/<app>/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/<app>/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/<app>/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).
*
Expand All@@ -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()) {
Expand Down
90 changes: 90 additions & 0 deletions scripts/check-doc-snippet-types.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<app>/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/<app>/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/<app>/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.
*
Expand DownExpand Up@@ -343,6 +379,57 @@ 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/<app>/docs/** ✓ ✓ ✓ objectui#6600
* README.md ✓ ✓ ✓ objectui#7115
* packages/<name>/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. 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/<app>/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/<app>/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/)'
*
* ⛔ `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.
*
Expand DownExpand Up@@ -679,6 +766,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()) {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/6600-doc-gate-app-docs-roots.md
Original file line numberDiff line numberDiff line change
@@ -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/<app>/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.
44 changes: 43 additions & 1 deletion scripts/__tests__/check-doc-fence-languages.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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/<app>/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', () => {
Expand Down
57 changes: 53 additions & 4 deletions scripts/check-doc-component-types.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<app>/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/<app>/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/<app>/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/<app>/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.
*
Expand DownExpand Up@@ -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.
Expand Down
41 changes: 39 additions & 2 deletions scripts/check-doc-fence-languages.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<name>/README.md`, and the root
* `README.md` (objectui#7115). It is re-implemented here rather than imported so
* `.md` under `content/docs`, every `packages/<name>/README.md`, the root
* `README.md` (objectui#7115), and every `.mdx` / `.md` under
* `apps/<app>/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
Expand DownExpand Up@@ -153,6 +158,35 @@ const DOCS_ROOT = 'content/docs';
const PACKAGES_DIR = 'packages';
const DOC_EXTENSIONS = ['.mdx', '.md'];

/**
* Per-app documentation trees, `apps/<app>/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/<app>/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/<app>/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).
*
Expand All@@ -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()) {
Expand Down
90 changes: 90 additions & 0 deletions scripts/check-doc-snippet-types.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<app>/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/<app>/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/<app>/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.
*
Expand DownExpand Up@@ -343,6 +379,57 @@ 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/<app>/docs/** ✓ ✓ ✓ objectui#6600
* README.md ✓ ✓ ✓ objectui#7115
* packages/<name>/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. 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/<app>/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/<app>/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/)'
*
* ⛔ `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.
*
Expand DownExpand Up@@ -679,6 +766,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()) {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/6600-doc-gate-app-docs-roots.md
Original file line numberDiff line numberDiff line change
@@ -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/<app>/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.
44 changes: 43 additions & 1 deletion scripts/__tests__/check-doc-fence-languages.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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/<app>/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', () => {
Expand Down
57 changes: 53 additions & 4 deletions scripts/check-doc-component-types.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<app>/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/<app>/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/<app>/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/<app>/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.
*
Expand DownExpand Up@@ -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.
Expand Down
41 changes: 39 additions & 2 deletions scripts/check-doc-fence-languages.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<name>/README.md`, and the root
* `README.md` (objectui#7115). It is re-implemented here rather than imported so
* `.md` under `content/docs`, every `packages/<name>/README.md`, the root
* `README.md` (objectui#7115), and every `.mdx` / `.md` under
* `apps/<app>/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
Expand DownExpand Up@@ -153,6 +158,35 @@ const DOCS_ROOT = 'content/docs';
const PACKAGES_DIR = 'packages';
const DOC_EXTENSIONS = ['.mdx', '.md'];

/**
* Per-app documentation trees, `apps/<app>/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/<app>/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/<app>/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).
*
Expand All@@ -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()) {
Expand Down
90 changes: 90 additions & 0 deletions scripts/check-doc-snippet-types.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<app>/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/<app>/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/<app>/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.
*
Expand DownExpand Up@@ -343,6 +379,57 @@ 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/<app>/docs/** ✓ ✓ ✓ objectui#6600
* README.md ✓ ✓ ✓ objectui#7115
* packages/<name>/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. 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/<app>/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/<app>/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/)'
*
* ⛔ `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.
*
Expand DownExpand Up@@ -679,6 +766,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()) {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/6600-doc-gate-app-docs-roots.md
Original file line numberDiff line numberDiff line change
@@ -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/<app>/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.
44 changes: 43 additions & 1 deletion scripts/__tests__/check-doc-fence-languages.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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/<app>/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', () => {
Expand Down
57 changes: 53 additions & 4 deletions scripts/check-doc-component-types.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<app>/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/<app>/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/<app>/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/<app>/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.
*
Expand DownExpand Up@@ -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.
Expand Down
41 changes: 39 additions & 2 deletions scripts/check-doc-fence-languages.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<name>/README.md`, and the root
* `README.md` (objectui#7115). It is re-implemented here rather than imported so
* `.md` under `content/docs`, every `packages/<name>/README.md`, the root
* `README.md` (objectui#7115), and every `.mdx` / `.md` under
* `apps/<app>/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
Expand DownExpand Up@@ -153,6 +158,35 @@ const DOCS_ROOT = 'content/docs';
const PACKAGES_DIR = 'packages';
const DOC_EXTENSIONS = ['.mdx', '.md'];

/**
* Per-app documentation trees, `apps/<app>/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/<app>/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/<app>/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).
*
Expand All@@ -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()) {
Expand Down
90 changes: 90 additions & 0 deletions scripts/check-doc-snippet-types.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<app>/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/<app>/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/<app>/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.
*
Expand DownExpand Up@@ -343,6 +379,57 @@ 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/<app>/docs/** ✓ ✓ ✓ objectui#6600
* README.md ✓ ✓ ✓ objectui#7115
* packages/<name>/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. 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/<app>/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/<app>/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/)'
*
* ⛔ `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.
*
Expand DownExpand Up@@ -679,6 +766,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()) {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/6600-doc-gate-app-docs-roots.md
Original file line numberDiff line numberDiff line change
@@ -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/<app>/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.
44 changes: 43 additions & 1 deletion scripts/__tests__/check-doc-fence-languages.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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/<app>/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', () => {
Expand Down
57 changes: 53 additions & 4 deletions scripts/check-doc-component-types.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<app>/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/<app>/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/<app>/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/<app>/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.
*
Expand DownExpand Up@@ -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.
Expand Down
41 changes: 39 additions & 2 deletions scripts/check-doc-fence-languages.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<name>/README.md`, and the root
* `README.md` (objectui#7115). It is re-implemented here rather than imported so
* `.md` under `content/docs`, every `packages/<name>/README.md`, the root
* `README.md` (objectui#7115), and every `.mdx` / `.md` under
* `apps/<app>/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
Expand DownExpand Up@@ -153,6 +158,35 @@ const DOCS_ROOT = 'content/docs';
const PACKAGES_DIR = 'packages';
const DOC_EXTENSIONS = ['.mdx', '.md'];

/**
* Per-app documentation trees, `apps/<app>/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/<app>/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/<app>/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).
*
Expand All@@ -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()) {
Expand Down
90 changes: 90 additions & 0 deletions scripts/check-doc-snippet-types.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<app>/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/<app>/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/<app>/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.
*
Expand DownExpand Up@@ -343,6 +379,57 @@ 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/<app>/docs/** ✓ ✓ ✓ objectui#6600
* README.md ✓ ✓ ✓ objectui#7115
* packages/<name>/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. 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/<app>/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/<app>/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/)'
*
* ⛔ `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.
*
Expand DownExpand Up@@ -679,6 +766,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()) {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/6600-doc-gate-app-docs-roots.md
Original file line numberDiff line numberDiff line change
@@ -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/<app>/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.
44 changes: 43 additions & 1 deletion scripts/__tests__/check-doc-fence-languages.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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/<app>/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', () => {
Expand Down
57 changes: 53 additions & 4 deletions scripts/check-doc-component-types.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<app>/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/<app>/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/<app>/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/<app>/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.
*
Expand DownExpand Up@@ -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.
Expand Down
41 changes: 39 additions & 2 deletions scripts/check-doc-fence-languages.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<name>/README.md`, and the root
* `README.md` (objectui#7115). It is re-implemented here rather than imported so
* `.md` under `content/docs`, every `packages/<name>/README.md`, the root
* `README.md` (objectui#7115), and every `.mdx` / `.md` under
* `apps/<app>/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
Expand DownExpand Up@@ -153,6 +158,35 @@ const DOCS_ROOT = 'content/docs';
const PACKAGES_DIR = 'packages';
const DOC_EXTENSIONS = ['.mdx', '.md'];

/**
* Per-app documentation trees, `apps/<app>/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/<app>/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/<app>/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).
*
Expand All@@ -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()) {
Expand Down
90 changes: 90 additions & 0 deletions scripts/check-doc-snippet-types.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<app>/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/<app>/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/<app>/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.
*
Expand DownExpand Up@@ -343,6 +379,57 @@ 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/<app>/docs/** ✓ ✓ ✓ objectui#6600
* README.md ✓ ✓ ✓ objectui#7115
* packages/<name>/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. 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/<app>/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/<app>/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/)'
*
* ⛔ `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.
*
Expand DownExpand Up@@ -679,6 +766,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()) {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/6600-doc-gate-app-docs-roots.md
Original file line numberDiff line numberDiff line change
@@ -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/<app>/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.
44 changes: 43 additions & 1 deletion scripts/__tests__/check-doc-fence-languages.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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/<app>/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', () => {
Expand Down
57 changes: 53 additions & 4 deletions scripts/check-doc-component-types.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<app>/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/<app>/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/<app>/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/<app>/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.
*
Expand DownExpand Up@@ -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.
Expand Down
41 changes: 39 additions & 2 deletions scripts/check-doc-fence-languages.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<name>/README.md`, and the root
* `README.md` (objectui#7115). It is re-implemented here rather than imported so
* `.md` under `content/docs`, every `packages/<name>/README.md`, the root
* `README.md` (objectui#7115), and every `.mdx` / `.md` under
* `apps/<app>/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
Expand DownExpand Up@@ -153,6 +158,35 @@ const DOCS_ROOT = 'content/docs';
const PACKAGES_DIR = 'packages';
const DOC_EXTENSIONS = ['.mdx', '.md'];

/**
* Per-app documentation trees, `apps/<app>/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/<app>/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/<app>/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).
*
Expand All@@ -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()) {
Expand Down
90 changes: 90 additions & 0 deletions scripts/check-doc-snippet-types.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<app>/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/<app>/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/<app>/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.
*
Expand DownExpand Up@@ -343,6 +379,57 @@ 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/<app>/docs/** ✓ ✓ ✓ objectui#6600
* README.md ✓ ✓ ✓ objectui#7115
* packages/<name>/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. 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/<app>/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/<app>/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/)'
*
* ⛔ `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.
*
Expand DownExpand Up@@ -679,6 +766,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()) {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/6600-doc-gate-app-docs-roots.md
Original file line numberDiff line numberDiff line change
@@ -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/<app>/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.
44 changes: 43 additions & 1 deletion scripts/__tests__/check-doc-fence-languages.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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/<app>/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', () => {
Expand Down
57 changes: 53 additions & 4 deletions scripts/check-doc-component-types.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<app>/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/<app>/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/<app>/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/<app>/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.
*
Expand DownExpand Up@@ -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.
Expand Down
41 changes: 39 additions & 2 deletions scripts/check-doc-fence-languages.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<name>/README.md`, and the root
* `README.md` (objectui#7115). It is re-implemented here rather than imported so
* `.md` under `content/docs`, every `packages/<name>/README.md`, the root
* `README.md` (objectui#7115), and every `.mdx` / `.md` under
* `apps/<app>/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
Expand DownExpand Up@@ -153,6 +158,35 @@ const DOCS_ROOT = 'content/docs';
const PACKAGES_DIR = 'packages';
const DOC_EXTENSIONS = ['.mdx', '.md'];

/**
* Per-app documentation trees, `apps/<app>/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/<app>/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/<app>/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).
*
Expand All@@ -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()) {
Expand Down
90 changes: 90 additions & 0 deletions scripts/check-doc-snippet-types.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/<app>/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/<app>/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/<app>/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.
*
Expand DownExpand Up@@ -343,6 +379,57 @@ 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/<app>/docs/** ✓ ✓ ✓ objectui#6600
* README.md ✓ ✓ ✓ objectui#7115
* packages/<name>/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. 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/<app>/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/<app>/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/)'
*
* ⛔ `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.
*
Expand DownExpand Up@@ -679,6 +766,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()) {
Expand Down
Loading