diff --git a/package.json b/package.json
index 3932c1536..f60fa6ab4 100644
--- a/package.json
+++ b/package.json
@@ -62,6 +62,7 @@
"check:skill-eval-tokens": "node scripts/check-skill-eval-tokens.mjs",
"check:doc-types": "node scripts/check-doc-component-types.mjs",
"check:doc-snippets": "node scripts/check-doc-snippet-types.mjs",
+ "check:doc-examples": "node scripts/check-doc-example-types.mjs",
"check:doc-fences": "node scripts/check-doc-fence-languages.mjs",
"check:eager-closure": "node scripts/check-eager-closure-budget.mjs",
"check:side-effects-array": "node scripts/check-side-effects-array.mjs",
diff --git a/scripts/__tests__/check-doc-example-types.test.ts b/scripts/__tests__/check-doc-example-types.test.ts
new file mode 100644
index 000000000..71f15fdb9
--- /dev/null
+++ b/scripts/__tests__/check-doc-example-types.test.ts
@@ -0,0 +1,390 @@
+import { describe, expect, it } from 'vitest';
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+// Plain-JS CI helper; its types are inferred from the .mjs source by
+// `tsconfig.scripts.json` (`allowJs`), so no `@ts-expect-error` here.
+import ts from 'typescript';
+import {
+ EXIT_CODES,
+ MIN_REASON_LENGTH,
+ TS_FENCE_LANGUAGES,
+ UNGATED_EXAMPLES,
+ codesMatch,
+ exampleCensus,
+ exportedOwnerOf,
+ fencesOfExample,
+ funnelLines,
+ judge,
+ ledgerKey,
+ listExampleSources,
+ preludeFor,
+} from '../check-doc-example-types.mjs';
+
+/**
+ * objectui#8258 — the test for `scripts/check-doc-example-types.mjs`.
+ *
+ * The gate compiles JSDoc `@example` blocks on exported symbols against the
+ * BUILT types. Two kinds of assertion live here, and the split matters:
+ *
+ * INSTRUMENT cases built from fixture text, which prove the extraction and
+ * the four ledger verdicts behave as the header says — including
+ * the two that make the ledger shrink-only, which are the ones a
+ * reader would otherwise have to take on trust.
+ * CORPUS cases about THIS repository, which prove the instrument is
+ * pointed at a real population and that the ledger is exact. They
+ * are what stops the gate going green by scanning nothing.
+ *
+ * ⛔ What is deliberately NOT here: a case that runs the whole gate and asserts
+ * exit 0. That is `pnpm check:doc-examples`' job, it needs the built closure,
+ * and duplicating it here would make this file's runtime depend on a build.
+ */
+
+const here = path.dirname(fileURLToPath(import.meta.url));
+const repoRoot = path.resolve(here, '..', '..');
+const gateSource = fs.readFileSync(path.join(repoRoot, 'scripts', 'check-doc-example-types.mjs'), 'utf8');
+
+// ── instrument: extraction ───────────────────────────────────────────────────
+
+describe('fence extraction inside an `@example` body', () => {
+ it('reads ts, tsx and typescript fences and nothing else', () => {
+ const fences = fencesOfExample(
+ ['```ts', 'const a = 1;', '```', '```json', '{}', '```', '```tsx', '
', '```'].join('\n'),
+ );
+ expect(fences.map((f) => f.language)).toEqual(['ts', 'json', 'tsx']);
+ expect(fences.filter((f) => TS_FENCE_LANGUAGES.has(f.language))).toHaveLength(2);
+ });
+
+ it('closes a block at a fence of its own run length, so a wider wrapper is one block', () => {
+ const fences = fencesOfExample(['````ts', '```', 'const a = 1;', '```', '````'].join('\n'));
+ expect(fences).toHaveLength(1);
+ expect(fences[0].body).toBe(['```', 'const a = 1;', '```'].join('\n'));
+ });
+
+ it('keeps the snippet own indentation', () => {
+ const fences = fencesOfExample(['```ts', 'if (x) {', ' go();', '}', '```'].join('\n'));
+ expect(fences[0].body).toContain('\n go();');
+ });
+
+ it('an unterminated fence runs to the end rather than swallowing the walk', () => {
+ const fences = fencesOfExample(['```ts', 'const a = 1;'].join('\n'));
+ expect(fences).toHaveLength(1);
+ expect(fences[0].body).toBe('const a = 1;');
+ });
+});
+
+describe('the exported owner is read from the AST, never from the text', () => {
+ const ownerOf = (source: string, needle: string) => {
+ const sf = ts.createSourceFile('probe.tsx', source, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TSX);
+ let found: ReturnType | null = null;
+ const visit = (node: ts.Node) => {
+ if (node.getText(sf).startsWith(needle) && found === null) found = exportedOwnerOf(node);
+ ts.forEachChild(node, visit);
+ };
+ ts.forEachChild(sf, visit);
+ return found;
+ };
+
+ it('names an exported function', () => {
+ expect(ownerOf('export function useThing() {}', 'export function')).toEqual({
+ exported: true,
+ symbol: 'useThing',
+ });
+ });
+
+ it('names the VARIABLE, not the statement, for an exported const', () => {
+ expect(ownerOf('export const thing = 1;', 'export const')).toEqual({
+ exported: true,
+ symbol: 'thing',
+ });
+ });
+
+ it('walks OUTWARD: a property inside an exported interface belongs to the interface', () => {
+ const sf = ts.createSourceFile(
+ 'probe.ts',
+ 'export interface Shape {\n /** @example 1 */\n size: number;\n}',
+ ts.ScriptTarget.ES2022,
+ true,
+ );
+ const iface = sf.statements[0] as ts.InterfaceDeclaration;
+ expect(exportedOwnerOf(iface.members[0])).toEqual({ exported: true, symbol: 'Shape' });
+ });
+
+ it('reports a non-exported declaration as not exported', () => {
+ expect(ownerOf('function local() {}', 'function local')).toEqual({
+ exported: false,
+ symbol: null,
+ });
+ });
+
+ it('the word `export` inside a STRING does not make a declaration exported', () => {
+ expect(ownerOf('const doc = "export function fake() {}";\nfunction real() {}', 'function real')).toEqual({
+ exported: false,
+ symbol: null,
+ });
+ });
+});
+
+// ── instrument: the one transformation ───────────────────────────────────────
+
+describe('the documented symbol import is injected only when all three conditions hold', () => {
+ const block = { symbol: 'useThing', package: '@object-ui/x', body: 'const r = useThing();' };
+
+ it('injects when the block references the symbol and does not import it', () => {
+ expect(preludeFor(block, new Set())).toBe("import { useThing } from '@object-ui/x';\n");
+ });
+
+ it('does NOT inject when the block never references the symbol', () => {
+ expect(preludeFor({ ...block, body: 'const r = 1;' }, new Set())).toBe('');
+ });
+
+ it('does NOT inject when the block already imports the symbol itself', () => {
+ expect(
+ preludeFor({ ...block, body: "import { useThing } from 'somewhere';\nuseThing();" }, new Set()),
+ ).toBe('');
+ });
+
+ it('does NOT inject when the package entry does not export the symbol — the gate never blames an example for this transformation', () => {
+ expect(preludeFor(block, new Set(['@object-ui/x useThing']))).toBe('');
+ });
+});
+
+// ── instrument: the four ledger verdicts ─────────────────────────────────────
+
+describe('the ledger is re-derived, never trusted', () => {
+ const reason = 'a written reason long enough to be a declaration';
+
+ it('a failing block with no row is a FAILURE — the default is COVERED', () => {
+ const { findings } = judge({ results: [{ key: 'a.ts:1 A', codes: [2322] }], ledger: {} });
+ expect(findings.map((f) => f.reason)).toEqual(['undeclared-failure']);
+ });
+
+ it('a failing block whose row matches is exempt, and nothing is reported', () => {
+ const { findings, exempt } = judge({
+ results: [{ key: 'a.ts:1 A', codes: [2322] }],
+ ledger: { 'a.ts:1 A': { codes: [2322], reason } },
+ });
+ expect(findings).toEqual([]);
+ expect(exempt).toEqual(['a.ts:1 A']);
+ });
+
+ it('SHRINK-ONLY: a row whose block now COMPILES is stale, and that is a red', () => {
+ const { findings } = judge({
+ results: [{ key: 'a.ts:1 A', codes: [] }],
+ ledger: { 'a.ts:1 A': { codes: [2322], reason } },
+ });
+ expect(findings).toHaveLength(1);
+ expect(findings[0].reason).toBe('stale-ledger-row');
+ expect(findings[0].detail).toContain('COMPILES now');
+ });
+
+ it('SHRINK-ONLY: a row whose block fails DIFFERENTLY is a red — a row may not cover a failure it never declared', () => {
+ const { findings } = judge({
+ results: [{ key: 'a.ts:1 A', codes: [2322] }],
+ ledger: { 'a.ts:1 A': { codes: [2304], reason } },
+ });
+ expect(findings).toHaveLength(1);
+ expect(findings[0].reason).toBe('ledger-row-drifted');
+ expect(findings[0].detail).toContain('TS2304');
+ expect(findings[0].detail).toContain('TS2322');
+ });
+
+ it('a row naming a block that is gone is stale', () => {
+ const { findings } = judge({ results: [], ledger: { 'gone.ts:1 A': { codes: [2322], reason } } });
+ expect(findings.map((f) => f.reason)).toEqual(['stale-ledger-row']);
+ expect(findings[0].detail).toContain('no such example');
+ });
+
+ it('a row with no written reason is not a declaration', () => {
+ const { findings } = judge({
+ results: [{ key: 'a.ts:1 A', codes: [2322] }],
+ ledger: { 'a.ts:1 A': { codes: [2322], reason: 'short' } },
+ });
+ expect(findings.map((f) => f.reason)).toEqual(['unexplained-ledger-row']);
+ });
+
+ it('a clean block with no row is silent — the whole point of the exercise', () => {
+ expect(judge({ results: [{ key: 'a.ts:1 A', codes: [] }], ledger: {} }).findings).toEqual([]);
+ });
+
+ it('codes are compared as a SET: a doubled diagnostic is not a changed failure', () => {
+ expect(codesMatch([2322, 2322], [2322])).toBe(true);
+ expect(codesMatch([2322, 2304], [2304, 2322])).toBe(true);
+ expect(codesMatch([2322], [2322, 2304])).toBe(false);
+ });
+});
+
+// ── corpus: this repository ──────────────────────────────────────────────────
+
+describe('this repository', () => {
+ const census = exampleCensus({ root: repoRoot });
+
+ it('walks a plausible number of sources — an empty walk makes every verdict vacuous', () => {
+ expect(census.files.length).toBeGreaterThan(500);
+ expect(census.excludedAsTooling.length).toBeGreaterThan(100);
+ });
+
+ it('has examples to judge — the compiled tier is not empty', () => {
+ expect(census.blocks.length).toBeGreaterThan(50);
+ });
+
+ it('every block in the compiled tier names an exported symbol and a package', () => {
+ for (const block of census.blocks) {
+ expect(block.symbol, block.file).toBeTruthy();
+ expect(block.package, block.file).toMatch(/^@object-ui\/|^object-ui$/);
+ }
+ });
+
+ it('the bare tier is COUNTED, not dropped — the header stands or falls on that number', () => {
+ expect(census.bare.length).toBeGreaterThan(0);
+ expect(census.exported.length).toBe(
+ census.withTsFence.length + census.otherFenceOnly.length + census.bare.length,
+ );
+ });
+
+ it('the funnel prints every narrowing step, so the enforced number is derived', () => {
+ const lines = funnelLines(census);
+ expect(lines.join('\n')).toContain('EXPORTED declarations');
+ expect(lines.join('\n')).toContain('BLOCKS in the compiled tier');
+ expect(lines.at(-1)).toContain(String(census.blocks.length));
+ });
+
+ it('tooling files contribute nothing to the compiled tier', () => {
+ const toolingFiles = new Set(census.excludedAsTooling);
+ for (const block of census.blocks) expect(toolingFiles.has(block.file)).toBe(false);
+ });
+});
+
+describe('the real ledger', () => {
+ const census = exampleCensus({ root: repoRoot });
+ const keys = new Set(census.blocks.map((b) => ledgerKey(b)));
+
+ it('every row names a block that is actually in the compiled tier', () => {
+ for (const key of Object.keys(UNGATED_EXAMPLES)) expect(keys.has(key), key).toBe(true);
+ });
+
+ it('every row carries a written reason and a code list', () => {
+ for (const [key, row] of Object.entries(UNGATED_EXAMPLES)) {
+ expect(row.reason.trim().length, key).toBeGreaterThanOrEqual(MIN_REASON_LENGTH);
+ expect(row.codes.length, key).toBeGreaterThan(0);
+ expect(row.codes.every((c) => Number.isInteger(c)), key).toBe(true);
+ }
+ });
+
+ it('is a DEBT, not the corpus: some blocks are off it and therefore actually judged', () => {
+ const declared = Object.keys(UNGATED_EXAMPLES).length;
+ expect(declared).toBeLessThan(census.blocks.length);
+ expect(census.blocks.length - declared).toBeGreaterThan(0);
+ });
+
+ it('a row that names a card names one this repository can be asked about', () => {
+ for (const [key, row] of Object.entries(UNGATED_EXAMPLES)) {
+ if (row.card === null) continue;
+ expect(row.card, key).toMatch(/^objectui#\d+$/);
+ }
+ });
+});
+
+// ── the card's own acceptance criterion ──────────────────────────────────────
+
+describe('objectui#7974 — the defect this gate was filed for', () => {
+ const key = 'packages/mobile/src/useSpecGesture.ts:69 useSpecGesture';
+
+ it('its example is IN the compiled tier — the gate reaches the block the card named', () => {
+ const census = exampleCensus({ root: repoRoot });
+ expect(census.blocks.map((b) => ledgerKey(b))).toContain(key);
+ });
+
+ it('the scalar `direction` the card measured is what the block still carries', () => {
+ const source = fs.readFileSync(path.join(repoRoot, 'packages/mobile/src/useSpecGesture.ts'), 'utf8');
+ expect(source).toContain("direction: 'left'");
+ });
+
+ it('its row records TS2322 by NUMBER and names the card that owns the repair', () => {
+ const row = UNGATED_EXAMPLES[key];
+ expect(row).toBeDefined();
+ expect(row.codes).toContain(2322);
+ expect(row.card).toBe('objectui#7974');
+ });
+
+ it('the row is the ONLY thing keeping this green — remove it and the block is an undeclared failure', () => {
+ const { findings } = judge({
+ results: [{ key, codes: UNGATED_EXAMPLES[key].codes }],
+ ledger: {},
+ });
+ expect(findings.map((f) => f.reason)).toEqual(['undeclared-failure']);
+ });
+
+ it("when that lane repairs the example the row goes STALE, so the debt cannot outlive the defect", () => {
+ const { findings } = judge({
+ results: [{ key, codes: [] }],
+ ledger: { [key]: UNGATED_EXAMPLES[key] },
+ });
+ expect(findings).toHaveLength(1);
+ expect(findings[0]).toMatchObject({ reason: 'stale-ledger-row', site: key });
+ });
+});
+
+// ── the decisions this gate is asked to state in its own source ──────────────
+
+describe('the script states its own rulings, so they cannot drift out of the source', () => {
+ it('states why bare `@example` bodies are counted and not compiled', () => {
+ expect(gateSource).toContain('Why bare `@example` bodies are counted and not compiled');
+ });
+
+ it('states the ONE transformation and that it is the only one', () => {
+ expect(gateSource).toContain('The ONE transformation, stated out loud');
+ });
+
+ it('records that the `declare var NAME: any` route was priced and REFUSED', () => {
+ expect(gateSource).toContain('declare var NAME: any');
+ expect(gateSource).toContain('REFUSED');
+ });
+
+ it('states the template-literal decision and points at the instrument that already owns it', () => {
+ expect(gateSource).toContain('The template-literal half of objectui#8258');
+ expect(gateSource).toContain('--emit-census');
+ });
+
+ it('carries the three exit codes the sibling carries, for the reason the sibling gives', () => {
+ expect(EXIT_CODES).toEqual({ verified: 0, examplesFailed: 1, couldNotRun: 2 });
+ });
+
+ it('refuses a verdict when the walk collapses or the compiled tier is empty', () => {
+ expect(gateSource).toContain('would be a zero that means nothing');
+ expect(gateSource).toContain('The compiled tier is EMPTY');
+ });
+});
+
+// ── the sibling is not perturbed ─────────────────────────────────────────────
+
+describe('the sibling harness is imported, not forked', () => {
+ it('imports the compile harness rather than re-spelling it', () => {
+ expect(gateSource).toContain("from './check-doc-snippet-types.mjs'");
+ expect(gateSource).toMatch(/import \{[^}]*compileSnippets[^}]*\} from '\.\/check-doc-snippet-types\.mjs'/s);
+ });
+
+ it("does not re-declare the sibling's controls — a second sentinel would be a second answer", () => {
+ expect(gateSource).not.toContain('ThisNameIsDefinitelyNotExported');
+ });
+
+ it('the strictness region of the sibling is untouched by this card', () => {
+ const sibling = fs.readFileSync(
+ path.join(repoRoot, 'scripts', 'check-doc-snippet-types.mjs'),
+ 'utf8',
+ );
+ const banner = sibling.indexOf('── Fence scanning');
+ expect(banner).toBeGreaterThan(0);
+ // The region is licensed: this card may read it, never edit it. Pinned by
+ // content rather than by hash so the failure names WHAT moved.
+ expect(sibling.slice(banner)).toContain('export function scanFences(source)');
+ expect(sibling.slice(banner)).toContain('export function compileSnippets(');
+ });
+
+ it('the walk uses the same tooling rule the other source-walking gates use', () => {
+ const { files, excludedAsTooling } = listExampleSources(repoRoot);
+ expect(files.some((f) => f.includes('__tests__'))).toBe(false);
+ expect(excludedAsTooling.some((f) => f.includes('__tests__') || /\.test\./.test(f))).toBe(true);
+ });
+});
diff --git a/scripts/check-doc-example-types.mjs b/scripts/check-doc-example-types.mjs
new file mode 100644
index 000000000..c71311fdf
--- /dev/null
+++ b/scripts/check-doc-example-types.mjs
@@ -0,0 +1,1349 @@
+#!/usr/bin/env node
+/**
+ * Every ```ts / ```tsx fenced block inside a JSDoc `@example` on an EXPORTED
+ * declaration under `packages/NAME/src/**` must COMPILE, `--strict`, against the
+ * packages' BUILT `dist/*.d.ts` — or be DECLARED in the ledger below with a
+ * written reason and the diagnostics it currently produces.
+ *
+ * Run: node scripts/check-doc-example-types.mjs (also `pnpm check:doc-examples`)
+ * Exit: 0 = every covered example compiles, or fails exactly as its ledger row
+ * says it does; the harness proved itself on its own controls.
+ * 1 = THE GATE RAN AND FOUND ERRORS. An example failed and nothing
+ * declared it, an example a row declares now compiles, or a row's
+ * diagnostics no longer match what the example produces.
+ * 2 = THE GATE COULD NOT RUN, so nothing printed above is a verdict about
+ * any example: the packages are unbuilt (or typed from source), the walk
+ * collapsed, or one of the harness's own controls failed.
+ *
+ * This is a SIBLING of `scripts/check-doc-snippet-types.mjs`, not a fork. The
+ * scan surface, the extraction and the ledger are this file's; the compiler
+ * host, the built-`.d.ts` resolution, the root bound and every control are
+ * IMPORTED from that harness and run unmodified, so an `@example` block is
+ * judged by exactly the program a documentation fence is judged by. The two
+ * gates' exit codes are deliberately the same three, for the reason that file's
+ * header gives: "I could not run" and "I ran and found errors" are different
+ * facts and must not share a code.
+ *
+ * ## What this gate answers, and the four things it does NOT
+ *
+ * It answers exactly one question: **does this shipped `@example` still compile
+ * against the published types.** objectui#7974 is the defect that shape has:
+ * `useSpecGesture`'s own `@example` passes a scalar `swipe.direction` where the
+ * declared type is `SpecSwipeDirection[]`, so a reader who copies the example
+ * out of their IDE's hover gets TS2322 — and nothing in this repository had ever
+ * compiled it, because the Markdown gate's surface stops at `content/docs`, the
+ * per-app docs trees, the package READMEs and the root pages.
+ *
+ * It does NOT answer:
+ *
+ * 1. **Whether a BARE `@example` body is correct.** Only FENCED ts/tsx blocks
+ * are compiled. The census below counts the bare ones rather than hiding
+ * them, and the reason they stay out is measured, not assumed — see
+ * "Why bare `@example` bodies are counted and not compiled".
+ * 2. **Whether the example is idiomatic, runnable, or true.** A block that
+ * compiles can still teach a call nobody should make. That is review.
+ * 3. **Schema-key validity** — the sibling's blind spot 1, unchanged here: a
+ * metadata literal that `safeParse` would reject can still satisfy a
+ * TypeScript annotation.
+ * 4. **Code inside template literals.** That population already has an
+ * instrument: `check-doc-snippet-types.mjs --emit-census` (objectui#7864)
+ * walks the same `packages/NAME/src/**` tree, recognises a template that
+ * carries an `import`, substitutes its holes and compiles it through the
+ * same `compileSnippets()`. Adding a second reader of the same population
+ * here would be a second answer to a question that already has one. See
+ * "The template-literal half of objectui#8258" below.
+ *
+ * ## The population, and why the compiled tier is the fenced blocks
+ *
+ * Measured on the branch point (objectui#8258's first run), with the funnel this
+ * gate re-prints on every run so the number it enforces stays DERIVED rather
+ * than asserted:
+ *
+ * 1418 source files under packages/NAME/src (.ts/.tsx), tooling excluded
+ * 2421 ... tooling files excluded by TOOLING_FILE (tests, mocks, stories)
+ * 228 `@example` tags found by the AST
+ * 0 ... in tooling files
+ * 15 ... on a NON-exported declaration
+ * 213 `@example` tags on EXPORTED declarations
+ * 124 ... carrying a ts/tsx/typescript fence <- THE COMPILED TIER
+ * 1 ... carrying only an unlabelled fence
+ * 88 ... BARE, no fence at all
+ *
+ * ### Why bare `@example` bodies are counted and not compiled
+ *
+ * Not an optimisation and not a silent skip — a measurement. 67 of the 88 bare
+ * bodies parse as TSX, which sounds like a compilable population and is not: the
+ * dominant shape is a VALUE illustration on an interface property, and it parses
+ * only because a comma-separated list of string literals is a legal expression
+ * statement. From `packages/types/src/base.ts`:
+ *
+ * Component type identifier. Determines which renderer to use.
+ * `@example` 'input', 'button', 'form', 'grid'
+ *
+ * Compiling that judges nothing — it is four string literals — while the fenced
+ * tier holds the blocks whose authors wrote a module. Feeding the bare tier to a
+ * strict program would judge prose on rules its author never opted into, which is
+ * the same reason `TS_FENCE_LANGUAGES` in the sibling excludes `js` and `jsx`.
+ * The count is printed every run, so the day someone starts fencing them the
+ * number moves and this paragraph is re-read.
+ *
+ * ## The ONE transformation, stated out loud
+ *
+ * A JSDoc `@example` is read in the IDE beside the declaration it documents, so
+ * the documented symbol is in scope for its reader by construction; a fenced
+ * block almost never imports it (measured: 8 of 124 import anything at all). The
+ * gate therefore prepends ONE line:
+ *
+ * import { SYMBOL } from 'PACKAGE';
+ *
+ * and only when all three hold, each re-checked per run:
+ *
+ * - the block's text references SYMBOL;
+ * - the block does not already import SYMBOL itself;
+ * - PACKAGE's BUILT entry really exports SYMBOL — probed in the same program,
+ * never assumed. Measured: 111 of 114 documented symbols are on their
+ * package's public entry; the 3 that are not are reported by name.
+ *
+ * ⚠️ The probe asks the package's ROOT specifier and only that one, so a symbol
+ * published under a SUBPATH reads as "not on a public entry" here. That is a
+ * deliberately conservative answer — it withholds the injection rather than
+ * guessing a subpath — but it means the printed list is "symbols this gate did
+ * not inject", NOT a list of defects. On this corpus 2 of the 3 are subpath
+ * exports (`@object-ui/types/zod`) and both blocks compile anyway; the third,
+ * `MetadataCache`, is genuinely absent from its package's only export.
+ *
+ * Prepended, not appended, because an `import` must precede the code that uses
+ * it; the printed line numbers therefore carry an offset, which `formatDiagnostic`
+ * is given as the block's fence line. Without this transformation the run is
+ * unreadable rather than strict: 303 of 348 diagnostics were TS2304 naming the
+ * documented symbol itself, and objectui#7974's real TS2322 was MASKED behind
+ * `Cannot find name 'useSpecGesture'`. That is the whole argument for the line —
+ * it does not make failures go away, it makes the surviving ones be about the
+ * documented API.
+ *
+ * ⛔ What was priced and REFUSED: a second pass declaring every remaining free
+ * name as `declare var NAME: any`. It shrinks the ledger from 90 rows to 32 and
+ * is the wrong trade in every direction. It INVENTS diagnostics (measured: 6
+ * TS2749 "refers to a value, but is being used as a type" and 2 TS2451
+ * redeclarations that exist only because of the injected declarations), it makes
+ * every downstream check on those names vacuous, and a green bought with `any`
+ * teaches the next author that referencing an undeclared name is how you stop the
+ * gate looking — the consumer-side tolerance AGENTS.md commandment #0.1 bans, one
+ * level up. A 90-row ledger that says a true thing beats a 32-row ledger that
+ * launders 58 rows through `any`.
+ *
+ * ## The ledger, and what makes it shrink-only
+ *
+ * `UNGATED_EXAMPLES` is keyed by `path:line symbol` and each row carries the
+ * diagnostic CODES the example currently produces, a written reason, and the card
+ * that owns it. Four verdicts, and only the first is silent:
+ *
+ * block fails + row's codes match -> declared debt, exempt
+ * block fails + no row -> RED. A new example must compile.
+ * block COMPILES + row -> RED, stale. The debt was paid; the
+ * row outlived it and must be deleted.
+ * block fails + row's codes DIFFER -> RED. The failure changed underneath
+ * the declaration; re-derive the row.
+ * row naming a block that is gone -> RED, stale.
+ *
+ * The third verdict is what makes the ledger shrink-only, and it is the same pin
+ * `UNGATED_DOCS` carries in the sibling: a debt that can be declared once and
+ * never re-examined is not a ledger, it is a mute button. The fourth is this
+ * gate's addition — a row that says "fails with TS2304" must not go on covering
+ * the block after the failure became TS2322.
+ *
+ * ⚠️ Recording codes makes a row sensitive to the TypeScript version, on purpose.
+ * An upgrade that renumbers or splits a diagnostic reddens the rows it moved,
+ * with "re-derive" printed beside them. That is a loud, correct signal about a
+ * ledger whose rows are claims about a compiler; the alternative — a row that
+ * covers whatever the block does today — is the mute button again.
+ *
+ * ### The first run: 90 of 124, and why it lands as a ledger rather than a clean
+ *
+ * 119 blocks reached the semantic phase (5 do not parse), 34 compile, 85 fail.
+ * The failures are overwhelmingly ONE shape: a usage fragment that references
+ * ambient names it never declares (`manager`, `evaluator`, `navigate`, `App`).
+ * Those are not defects in the documented API and repairing them is editorial
+ * work on 66 separate files — which is precisely what a declared, shrink-only
+ * ledger is for, and precisely how objectui#5174 burned `UNGATED_DOCS` down to
+ * the empty object it is today. Each row names what the example references, so
+ * the row goes stale the moment the example is made self-contained.
+ *
+ * ⚠️ objectui#7974 is OPEN, and its row is the reason this gate ships with a
+ * ledger rather than a green: `packages/mobile/src/useSpecGesture.ts` still
+ * carries the scalar `direction` on `main`, its card is on another lane's queue
+ * (`domain:ui`, `pm:queue`), and this gate may not fix it. The row records
+ * TS2322 by number. When that lane repairs the example the row goes STALE and
+ * reddens on THEIR pull request, which is the hand-off working as designed, not
+ * a defect in it.
+ *
+ * ## The template-literal half of objectui#8258
+ *
+ * The card asks this gate to "decide per case whether to extract (a fenced marker
+ * inside the literal) or to exempt with a reason". The decision, measured:
+ * **no new rule here, and the exemption has a reason that is not "too hard".**
+ *
+ * - The extraction the card imagines ALREADY EXISTS and already runs on this
+ * exact tree. `--emit-census` reports 20 recognised templates across
+ * `packages/cli`, `packages/create-plugin` and `packages/vscode-extension`,
+ * compiled through the same `compileSnippets()`. Building a second extractor
+ * here would put two readers on one population, which is how the two answers
+ * start disagreeing.
+ * - objectui#7977 is CLOSED (by objectui#8112, which corrected the prose).
+ * - objectui#7976's residue is NOT a template-parsing problem. It is
+ * `packages/vscode-extension/DESIGN.md` holding a hand-copied MIRROR of a
+ * template's text. Compiling the template — which `--emit-census` already
+ * does, at 0 diagnostics — says nothing about whether the mirror still
+ * matches it. That card needs an equality pin between two texts, which is a
+ * different instrument from a type-checker and belongs on that card.
+ *
+ * @type {never}
+ */
+
+import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
+import { join, relative, resolve, sep, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import ts from 'typescript';
+import { isEntrypoint } from './invoked-as.mjs';
+import { TOOLING_FILE } from './check-phantom-dependencies.mjs';
+import { analyze, compileSnippets } from './check-doc-snippet-types.mjs';
+
+const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
+
+// ── Configuration ────────────────────────────────────────────────────────────
+
+/** The tree this gate walks. Identical to the emitted census's, and to
+ * `check-phantom-dependencies`', so "a package's own source" means one thing. */
+export const PACKAGES_DIR = 'packages';
+export const SOURCE_SUBDIR = 'src';
+
+/** Source extensions collected. */
+const SOURCE_EXTENSION = /\.tsx?$/;
+
+/** Fence languages treated as compilable TypeScript. Deliberately the same set
+ * the sibling uses; `js` / `jsx` are excluded there for a reason that holds
+ * here unchanged — they are not type-annotated. */
+export const TS_FENCE_LANGUAGES = new Set(['ts', 'tsx', 'typescript']);
+
+/** The shortest reason that counts as a declaration. The sibling's number. */
+export const MIN_REASON_LENGTH = 12;
+
+// ── Extraction ───────────────────────────────────────────────────────────────
+
+/**
+ * Every `.ts`/`.tsx` file under a workspace package's `src/`, split by the rule
+ * `check-phantom-dependencies.mjs` already owns. The tooling half is RETURNED
+ * rather than dropped: a population hiding in a test fixture is a different fact
+ * from no population at all, and the funnel prints both.
+ *
+ * @param {string} root
+ * @returns {{ files: string[], excludedAsTooling: string[] }}
+ */
+export function listExampleSources(root = repoRoot) {
+ const files = [];
+ const excludedAsTooling = [];
+ const pkgDir = join(root, PACKAGES_DIR);
+ if (!existsSync(pkgDir)) return { files, excludedAsTooling };
+ for (const entry of readdirSync(pkgDir).sort()) {
+ const src = join(pkgDir, entry, SOURCE_SUBDIR);
+ if (!existsSync(src) || !statSync(src).isDirectory()) continue;
+ const walk = (dir) => {
+ for (const name of readdirSync(dir).sort()) {
+ const p = join(dir, name);
+ if (statSync(p).isDirectory()) {
+ walk(p);
+ continue;
+ }
+ if (!SOURCE_EXTENSION.test(name)) continue;
+ const rel = relative(root, p).split(sep).join('/');
+ (TOOLING_FILE.test(rel) ? excludedAsTooling : files).push(rel);
+ }
+ };
+ walk(src);
+ }
+ return { files, excludedAsTooling };
+}
+
+/**
+ * The name of the EXPORTED declaration that owns this JSDoc, or `null` when the
+ * nearest declaration carrying an `export` modifier has no identifier to name.
+ *
+ * Walks OUTWARD from the documented node, because the `export` keyword sits on
+ * the statement while the JSDoc may hang off a member inside it — an `@example`
+ * on an interface property belongs to the exported interface. A
+ * `VariableStatement` carries the modifier but not the name; its first
+ * declaration does.
+ *
+ * ⚠️ Read from the AST, never from a regex over the text, for the reason
+ * `moduleSpecifiersOf` gives in the sibling (objectui#7555): the word `export`
+ * inside a string or a comment is not an export.
+ *
+ * @param {ts.Node} node
+ * @returns {{ exported: boolean, symbol: string | null }}
+ */
+export function exportedOwnerOf(node) {
+ let n = node;
+ while (n) {
+ const modifiers = ts.canHaveModifiers(n) ? ts.getModifiers(n) : undefined;
+ if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
+ if (ts.isVariableStatement(n)) {
+ const d = n.declarationList.declarations[0];
+ return { exported: true, symbol: d?.name && ts.isIdentifier(d.name) ? d.name.text : null };
+ }
+ const named = /** @type {{ name?: ts.Node }} */ (n).name;
+ return { exported: true, symbol: named && ts.isIdentifier(named) ? named.text : null };
+ }
+ n = n.parent;
+ }
+ return { exported: false, symbol: null };
+}
+
+/**
+ * Every fenced block inside one `@example` body, matched by its own fence run
+ * length so a wider wrapper containing ``` does not confuse the walk.
+ *
+ * A JSDoc comment body has already had its leading ` * ` column stripped by the
+ * TypeScript parser, so no quote-prefix machinery is needed here — that is the
+ * one piece of `scanFences` this file does not reuse, and the reason it does
+ * not.
+ *
+ * @param {string} text
+ * @returns {{ language: string, body: string }[]}
+ */
+export function fencesOfExample(text) {
+ const lines = text.split('\n');
+ const out = [];
+ for (let i = 0; i < lines.length; i++) {
+ const open = /^[ \t]*(`{3,})(.*)$/.exec(lines[i]);
+ if (!open) continue;
+ const ticks = open[1];
+ let close = lines.length;
+ for (let j = i + 1; j < lines.length; j++) {
+ const c = /^[ \t]*(`{3,})[ \t]*$/.exec(lines[j]);
+ if (c && c[1].length >= ticks.length) {
+ close = j;
+ break;
+ }
+ }
+ out.push({
+ language: (open[2].trim().split(/\s+/)[0] || '').toLowerCase(),
+ body: lines.slice(i + 1, close).join('\n'),
+ });
+ i = close;
+ }
+ return out;
+}
+
+/**
+ * The census: every `@example` tag in the tree, classified. Counts nothing away
+ * — the funnel it feeds prints each narrowing step, so the number the gate
+ * enforces is derived from the population rather than asserted about it.
+ *
+ * @param {{ root?: string }} options
+ */
+export function exampleCensus({ root = repoRoot } = {}) {
+ const { files, excludedAsTooling } = listExampleSources(root);
+ const packageNameOf = {};
+ const pkgDir = join(root, PACKAGES_DIR);
+ if (existsSync(pkgDir)) {
+ for (const entry of readdirSync(pkgDir).sort()) {
+ const manifest = join(pkgDir, entry, 'package.json');
+ if (!existsSync(manifest)) continue;
+ const name = JSON.parse(readFileSync(manifest, 'utf8')).name;
+ if (name) packageNameOf[`${PACKAGES_DIR}/${entry}`] = name;
+ }
+ }
+
+ const tags = [];
+ const collect = (fileList, tooling) => {
+ for (const file of fileList) {
+ const source = readFileSync(join(root, file), 'utf8');
+ if (!source.includes('@example')) continue;
+ const sf = ts.createSourceFile(
+ file,
+ source,
+ ts.ScriptTarget.ES2022,
+ true,
+ file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
+ );
+ const packageDir = file.split('/').slice(0, 2).join('/');
+ const visit = (node) => {
+ const jsDoc = /** @type {{ jsDoc?: ts.JSDoc[] }} */ (node).jsDoc;
+ for (const doc of Array.isArray(jsDoc) ? jsDoc : []) {
+ for (const tag of doc.tags ?? []) {
+ if (tag.tagName.text !== 'example') continue;
+ const { exported, symbol } = exportedOwnerOf(node);
+ const text =
+ typeof tag.comment === 'string'
+ ? tag.comment
+ : (ts.getTextOfJSDocComment(tag.comment) ?? '');
+ tags.push({
+ file,
+ line: sf.getLineAndCharacterOfPosition(tag.getStart(sf)).line + 1,
+ tooling,
+ exported,
+ symbol,
+ package: packageNameOf[packageDir],
+ fences: fencesOfExample(text),
+ text,
+ });
+ }
+ }
+ ts.forEachChild(node, visit);
+ };
+ ts.forEachChild(sf, visit);
+ }
+ };
+ collect(files, false);
+ collect(excludedAsTooling, true);
+
+ const inSources = tags.filter((t) => !t.tooling);
+ const exported = inSources.filter((t) => t.exported && t.symbol !== null);
+ const blocks = [];
+ for (const tag of exported) {
+ for (const fence of tag.fences) {
+ if (!TS_FENCE_LANGUAGES.has(fence.language)) continue;
+ blocks.push({
+ file: tag.file,
+ line: tag.line,
+ symbol: tag.symbol,
+ package: tag.package,
+ language: fence.language,
+ body: fence.body,
+ });
+ }
+ }
+ const withTsFence = exported.filter((t) =>
+ t.fences.some((f) => TS_FENCE_LANGUAGES.has(f.language)),
+ );
+
+ return {
+ files,
+ excludedAsTooling,
+ tags,
+ inSources,
+ exported,
+ withTsFence,
+ otherFenceOnly: exported.filter(
+ (t) => t.fences.length > 0 && !t.fences.some((f) => TS_FENCE_LANGUAGES.has(f.language)),
+ ),
+ bare: exported.filter((t) => t.fences.length === 0),
+ blocks,
+ };
+}
+
+// ── The ledger ───────────────────────────────────────────────────────────────
+
+/**
+ * The declared debt: examples that do not compile today, each with the codes it
+ * produces, a written reason, and the card that owns it.
+ *
+ * Keys are `path:line symbol`. The line is the `@example` TAG's line, which is
+ * where a reader looking for the block starts; it moves when the file moves, and
+ * a row whose key no longer resolves is reported as stale rather than ignored.
+ *
+ * ⛔ A row is not a place to park a defect. Every row here is a claim that the
+ * example is a FRAGMENT (it references context its reader supplies) or that a
+ * named card owns the repair. Adding a row to silence a real defect in a
+ * documented API is the failure this gate exists to catch, one level up.
+ *
+ * @type {Record}
+ */
+export const UNGATED_EXAMPLES = {
+ 'packages/auth/src/AuthGuard.tsx:36 AuthGuard': {
+ card: null,
+ codes: [2657],
+ reason:
+ 'two sibling JSX elements with no wrapper: the block is a render-body excerpt, not a module',
+ },
+ 'packages/auth/src/AuthProvider.tsx:142 AuthProvider': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `App`, which the example never declares',
+ },
+ 'packages/auth/src/AuthProvider.tsx:149 AuthProvider': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `App`, which the example never declares',
+ },
+ 'packages/auth/src/AuthProvider.tsx:155 AuthProvider': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `App`, which the example never declares',
+ },
+ 'packages/auth/src/AuthShell.tsx:66 AuthShell': {
+ card: null,
+ codes: [2304, 2552],
+ reason:
+ 'usage fragment: references `LoginForm`, `navigate`, which the example never declares',
+ },
+ 'packages/auth/src/createAuthClient.ts:270 createAuthClient': {
+ card: null,
+ codes: [18004],
+ reason:
+ 'shorthand `{ email, password }` stands for credentials the caller supplies; the example never declares them',
+ },
+ 'packages/auth/src/ForgotPasswordForm.tsx:107 ForgotPasswordForm': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `setShowSuccess`, which the example never declares',
+ },
+ 'packages/auth/src/LoginForm.tsx:126 LoginForm': {
+ card: null,
+ codes: [2552],
+ reason:
+ 'usage fragment: references `navigate`, which the example never declares',
+ },
+ 'packages/auth/src/RegisterForm.tsx:102 RegisterForm': {
+ card: null,
+ codes: [2552],
+ reason:
+ 'usage fragment: references `navigate`, which the example never declares',
+ },
+ 'packages/auth/src/useAuth.ts:16 useAuth': {
+ card: null,
+ codes: [18047],
+ reason:
+ 'guards on `isAuthenticated`, which strict null checking cannot correlate with `user` being non-null',
+ },
+ 'packages/auth/src/UserMenu.tsx:31 UserMenu': {
+ card: null,
+ codes: [2552],
+ reason:
+ 'usage fragment: references `navigate`, which the example never declares',
+ },
+ 'packages/components/src/notifications/NotificationAlerts.tsx:58 NotificationAlerts': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `App`, `NotificationProvider`, which the example never declares',
+ },
+ 'packages/components/src/notifications/NotificationBanners.tsx:38 NotificationBanners': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `Outlet`, which the example never declares',
+ },
+ 'packages/components/src/notifications/NotificationInline.tsx:43 NotificationInline': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `notify`, which the example never declares',
+ },
+ 'packages/components/src/notifications/NotificationSnackbar.tsx:43 NotificationSnackbar': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `App`, `NotificationProvider`, which the example never declares',
+ },
+ 'packages/core/src/actions/TransactionManager.ts:129 TransactionManager': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `actionExecutor`, `createOrderAction`, `manager`, `sendNotificationAction`, `updateInventoryAction`, which the example never declares',
+ },
+ 'packages/core/src/actions/TransactionManager.ts:244 TransactionManager': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `dataSource`, `manager`, which the example never declares',
+ },
+ 'packages/core/src/actions/TransactionManager.ts:324 TransactionManager': {
+ card: null,
+ codes: [2304, 7006],
+ reason:
+ 'usage fragment: references `items`, `manager`, which the example never declares, so what depends on them is judged unbound',
+ },
+ 'packages/core/src/adapters/resolveDataSource.ts:36 resolveDataSource': {
+ card: null,
+ codes: [2304, 18047],
+ reason:
+ 'usage fragment: references `contextDataSource`, which the example never declares, so what depends on it is judged unbound',
+ },
+ 'packages/core/src/data-scope/DataScopeManager.ts:68 DataScopeManager': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `myDataSource`, which the example never declares',
+ },
+ 'packages/core/src/data-scope/ViewDataProvider.ts:139 ViewDataProvider': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `myFetcher`, which the example never declares',
+ },
+ 'packages/core/src/evaluator/ExpressionEvaluator.ts:264 ExpressionEvaluator': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `evaluator`, which the example never declares',
+ },
+ 'packages/core/src/evaluator/ExpressionEvaluator.ts:334 ExpressionEvaluator': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `evaluator`, which the example never declares',
+ },
+ 'packages/core/src/evaluator/ExpressionEvaluator.ts:534 ExpressionEvaluator': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `fmt`, which the example never declares',
+ },
+ 'packages/core/src/registry/WidgetRegistry.ts:41 WidgetRegistry': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `registry`, which the example never declares',
+ },
+ 'packages/core/src/utils/debug.ts:109 debugLog': {
+ card: null,
+ codes: [7017],
+ reason:
+ 'sets a debug flag on `globalThis`, which has no index signature under strict mode',
+ },
+ 'packages/core/src/utils/freeze-schema.ts:144 defineSystemView': {
+ card: null,
+ codes: [2339],
+ reason:
+ 'demonstrates that the returned view is frozen by showing a `push` the readonly type rejects — the diagnostic IS the lesson',
+ },
+ 'packages/core/src/utils/record-source.ts:140 resolveRecordSourceConfig': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `resolveRecordSourceObjectName`, `schema`, `useMemo`, which the example never declares',
+ },
+ 'packages/core/src/utils/record-source.ts:69 resolveRecordSourceObjectName': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `resolveRecordSourceConfig`, `schema`, `useMemo`, which the example never declares',
+ },
+ 'packages/core/src/validation/schema-validator.ts:490 assertValidSchema': {
+ card: null,
+ codes: [2304, 18046],
+ reason:
+ 'usage fragment: references `schema`, which the example never declares, so what depends on it is judged unbound',
+ },
+ 'packages/core/src/validation/schema-validator.ts:515 isValidSchema': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `data`, `renderSchema`, which the example never declares',
+ },
+ 'packages/data-objectstack/src/cache/MetadataCache.ts:56 MetadataCache': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `MetadataCache`, `fetchSchemaFromServer`, which the example never declares',
+ },
+ 'packages/data-objectstack/src/index.ts:6156 createObjectStackAdapter': {
+ card: null,
+ codes: [2591],
+ reason:
+ 'usage fragment: references `process`, which the example never declares, so what depends on it is judged unbound',
+ },
+ 'packages/i18n/src/provider.tsx:370 I18nProviderProps': {
+ card: null,
+ codes: [2304, 7006],
+ reason:
+ 'usage fragment: references `App`, `I18nProvider`, which the example never declares, so what depends on them is judged unbound',
+ },
+ 'packages/i18n/src/provider.tsx:393 I18nProviderProps': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `App`, `I18nProvider`, `loadLanguage`, `loadLocales`, which the example never declares',
+ },
+ 'packages/i18n/src/provider.tsx:428 I18nProvider': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `App`, which the example never declares',
+ },
+ 'packages/i18n/src/useObjectLabel.ts:88 useObjectLabel': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `objectDef`, which the example never declares',
+ },
+ 'packages/i18n/src/utils/spec-formatters.ts:64 resolvePlural': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'annotates with `SpecPluralRule`, a type the example does not import',
+ },
+ 'packages/layout/src/AppSchemaRenderer.tsx:485 AppSchemaRenderer': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `Outlet`, `appJson`, `can`, `evaluateVisibility`, `evaluator`, which the example never declares',
+ },
+ 'packages/layout/src/NavigationRenderer.tsx:1243 NavigationRenderer': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `appSchema`, `can`, `evaluateVisibility`, `evaluator`, `saveOrder`, `searchTerm`, `updatePin`, which the example never declares',
+ },
+ 'packages/layout/src/ResponsiveGrid.tsx:119 ResponsiveGrid': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `Card`, which the example never declares',
+ },
+ 'packages/mobile/src/useGesture.ts:29 useGesture': {
+ card: null,
+ codes: [1108],
+ reason:
+ 'a hook-body excerpt: its `return` sits outside any function, so the block is a fragment by shape',
+ },
+ 'packages/mobile/src/useSpecGesture.ts:69 useSpecGesture': {
+ card: 'objectui#7974',
+ codes: [1108, 2322],
+ reason:
+ 'the scalar `swipe.direction` this example passes is rejected by the declared `SpecSwipeDirection[]` (TS2322). objectui#7974 owns BOTH halves — the example and the lenient cast that hides it — and is on another lane. Delete this row when that card lands; the block also returns outside a function (TS1108), a hook-body excerpt',
+ },
+ 'packages/mobile/src/useTouchTarget.ts:33 useTouchTarget': {
+ card: null,
+ codes: [1108],
+ reason:
+ 'a hook-body excerpt: its `return` sits outside any function, so the block is a fragment by shape',
+ },
+ 'packages/plugin-designer/src/EditorModeToggle.tsx:46 EditorModeToggle': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `mode`, `setMode`, which the example never declares',
+ },
+ 'packages/plugin-designer/src/hooks/useDesignerHistory.ts:24 useDesignerHistory': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `DesignerState`, `initialState`, `newState`, which the example never declares',
+ },
+ 'packages/plugin-form/src/FormSection.tsx:109 FormSectionContainer': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `FormField`, which the example never declares',
+ },
+ 'packages/plugin-form/src/ObjectForm.tsx:122 ObjectForm': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `dataSource`, which the example never declares',
+ },
+ 'packages/plugin-form/src/TabbedForm.tsx:220 TabbedForm': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `dataSource`, which the example never declares',
+ },
+ 'packages/plugin-form/src/WizardForm.tsx:361 WizardForm': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `dataSource`, which the example never declares',
+ },
+ 'packages/plugin-grid/src/VirtualGrid.tsx:49 VirtualGrid': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `items`, which the example never declares',
+ },
+ 'packages/plugin-list/src/ListView.tsx:808 ListViewHandle': {
+ card: null,
+ codes: [2304, 2686],
+ reason:
+ 'names the `React` UMD global, which a module-shaped block may not reach without an import',
+ },
+ 'packages/plugin-report/src/LiveReportExporter.ts:150 exportExcelWithFormulas': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `data`, `report`, which the example never declares',
+ },
+ 'packages/plugin-report/src/LiveReportExporter.ts:234 createScheduleTrigger': {
+ card: null,
+ codes: [1109],
+ reason:
+ 'the block is a prose-and-code mixture that does not parse as TSX in isolation',
+ },
+ 'packages/plugin-report/src/LiveReportExporter.ts:88 exportWithLiveData': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `myAdapter`, `report`, which the example never declares',
+ },
+ 'packages/plugin-view/src/ObjectView.tsx:591 ObjectView': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `dataSource`, which the example never declares',
+ },
+ 'packages/plugin-view/src/ObjectView.tsx:605 ObjectView': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `dataSource`, which the example never declares',
+ },
+ 'packages/plugin-view/src/ObjectView.tsx:622 ObjectView': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `dataSource`, which the example never declares',
+ },
+ 'packages/react/src/context/ActionContext.tsx:72 ActionProvider': {
+ card: null,
+ codes: [2304, 18004],
+ reason:
+ 'shorthand `{ user }` stands for context the caller supplies; the example never declares it',
+ },
+ 'packages/react/src/context/DndContext.tsx:128 DndProvider': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `KanbanBoard`, `handleDrop`, which the example never declares',
+ },
+ 'packages/react/src/context/NotificationContext.tsx:377 NotificationProvider': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `App`, `NotificationAlerts`, `NotificationBanners`, `NotificationSnackbar`, `toast`, which the example never declares',
+ },
+ 'packages/react/src/context/ThemeContext.tsx:120 ThemeProvider': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `App`, `myTheme`, which the example never declares',
+ },
+ 'packages/react/src/element-data-source/ElementDataSourceGate.tsx:182 useElementDataSourceSchema': {
+ card: null,
+ codes: [1108, 2304],
+ reason:
+ 'usage fragment: references `schema`, which the example never declares, so what depends on it is judged unbound',
+ },
+ 'packages/react/src/hooks/useActionRunner.ts:42 useActionRunner': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `formData`, `toast`, which the example never declares',
+ },
+ 'packages/react/src/hooks/useClientNotifications.ts:103 useClientNotifications': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `Button`, which the example never declares',
+ },
+ 'packages/react/src/hooks/useCrudShortcuts.ts:37 useCrudShortcuts': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `closeDialog`, `deleteSelected`, `openCreateDialog`, `saveRecord`, which the example never declares',
+ },
+ 'packages/react/src/hooks/useDataRefresh.ts:24 useDataRefresh': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `dataSource`, `objectName`, `params`, `schema`, `setData`, `useEffect`, which the example never declares',
+ },
+ 'packages/react/src/hooks/useDebugMode.ts:34 useDebugMode': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `DebugPanel`, which the example never declares',
+ },
+ 'packages/react/src/hooks/useDensityMode.ts:77 useDensityMode': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `activeView`, `dataSource`, `obj`, `vid`, which the example never declares',
+ },
+ 'packages/react/src/hooks/useDiscovery.ts:88 useDiscovery': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `AuthProvider`, `LoadingScreen`, which the example never declares',
+ },
+ 'packages/react/src/hooks/useDynamicApp.ts:58 useDynamicApp': {
+ card: null,
+ codes: [2304, 2307, 2693],
+ reason:
+ 'imports \'../config/app.json\', a sibling file the reader\'s own project supplies, and names `Console` as a value',
+ },
+ 'packages/react/src/hooks/useElementDataSource.ts:125 useElementDataSource': {
+ card: null,
+ codes: [1108, 2304],
+ reason:
+ 'usage fragment: references `adapter`, `schema`, which the example never declares, so what depends on them is judged unbound',
+ },
+ 'packages/react/src/hooks/useETagCache.ts:174 useETagCache': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `User`, `setUser`, `useEffect`, which the example never declares',
+ },
+ 'packages/react/src/hooks/useExpression.ts:163 useExpression': {
+ card: null,
+ codes: [18004],
+ reason:
+ 'shorthand `{ data, user }` stands for the scope the caller supplies; the example never declares it',
+ },
+ 'packages/react/src/hooks/useKeyboardShortcuts.ts:34 useKeyboardShortcuts': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `closeModal`, `createNew`, `openSearch`, which the example never declares',
+ },
+ 'packages/react/src/hooks/useNavigationOverlay.ts:211 useNavigationOverlay': {
+ card: null,
+ codes: [1003, 1382],
+ reason:
+ 'the block is a prose-and-code mixture that does not parse as TSX in isolation',
+ },
+ 'packages/react/src/hooks/useOffline.ts:239 useOffline': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `Banner`, which the example never declares',
+ },
+ 'packages/react/src/hooks/usePageVariables.tsx:249 usePageVariableBinding': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `record`, `schema`, which the example never declares',
+ },
+ 'packages/react/src/hooks/usePageVariables.tsx:98 PageVariablesProvider': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `MyComponents`, which the example never declares',
+ },
+ 'packages/react/src/hooks/usePerformance.ts:139 usePerformance': {
+ card: null,
+ codes: [2304, 2345],
+ reason:
+ 'usage fragment: references `NormalList`, `VirtualList`, which the example never declares, so what depends on them is judged unbound',
+ },
+ 'packages/react/src/hooks/usePerformanceBudget.ts:131 usePerformanceBudget': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `Dashboard`, `analytics`, which the example never declares',
+ },
+ 'packages/react/src/hooks/useSchemaPersistence.ts:212 useSchemaPersistence': {
+ card: null,
+ codes: [2304, 2451, 7006],
+ reason:
+ 'usage fragment: references `SchemaPersistenceAdapter`, `pageSchema`, which the example never declares, so what depends on them is judged unbound',
+ },
+ 'packages/react/src/hooks/useSettledSchema.ts:112 useSettledSchema': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `dataConfig`, `resolveRecordSourceObjectName`, `schema`, which the example never declares',
+ },
+ 'packages/react/src/hooks/useViewData.ts:72 useViewData': {
+ card: null,
+ codes: [2304, 7031],
+ reason:
+ 'usage fragment: references `ErrorMessage`, `Spinner`, `Table`, which the example never declares, so what depends on them is judged unbound',
+ },
+ 'packages/react/src/hooks/useViewSharing.ts:53 useViewSharing': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `currentFilters`, `currentSort`, `initialViews`, which the example never declares',
+ },
+ 'packages/types/src/data.ts:263 GlobalSearchHit': {
+ card: null,
+ codes: [2304, 7006],
+ reason:
+ 'usage fragment: references `DataSource`, `User`, `buildQuery`, which the example never declares, so what depends on them is judged unbound',
+ },
+ 'packages/types/src/data.ts:740 DataSource': {
+ card: null,
+ codes: [2304, 7006],
+ reason:
+ 'usage fragment: references `dataSource`, `refreshList`, which the example never declares, so what depends on them is judged unbound',
+ },
+ 'packages/types/src/icon-key-migration.ts:122 migrateIconNodeKeys': {
+ card: null,
+ codes: [2304],
+ reason:
+ 'usage fragment: references `save`, `storedPage`, which the example never declares',
+ },
+ 'packages/types/src/objectql.ts:1572 ObjectFormSchema': {
+ card: null,
+ codes: [1005, 1109],
+ reason:
+ 'the block is a prose-and-code mixture that does not parse as TSX in isolation',
+ },
+ 'packages/types/src/plugin-scope.ts:227 AppMetadataPlugin': {
+ card: null,
+ codes: [1128],
+ reason:
+ 'the block is a prose-and-code mixture that does not parse as TSX in isolation',
+ },
+};
+
+// ── The run ──────────────────────────────────────────────────────────────────
+
+/**
+ * Which documented symbols the built package entries really export.
+ *
+ * Probed, never assumed: one throwaway module per `PACKAGE SYMBOL` pair, handed
+ * to the same `compileSnippets()` the blocks go through, so the answer comes
+ * from the same resolution the verdict does. A pair that cannot be imported is
+ * reported by name and its block is judged WITHOUT the injected import, so the
+ * gate never blames an example for this file's own transformation.
+ *
+ * @param {{ root: string, blocks: {package: string, symbol: string}[], paths: object, declaredSpecifiers: string[] }} options
+ * @returns {Set} the `PACKAGE SYMBOL` pairs that are NOT on a public entry
+ */
+export function probeExportedSymbols({ root, blocks, paths, declaredSpecifiers }) {
+ const pairs = [...new Set(blocks.map((b) => `${b.package} ${b.symbol}`))].sort();
+ const probes = pairs.map((pair, index) => {
+ const [pkg, symbol] = pair.split(' ');
+ return {
+ doc: `probe/${pkg}`,
+ fenceLine: index,
+ language: 'ts',
+ quoteDepth: 0,
+ fragmentReason: null,
+ // `[typeof S]` rather than `typeof S`: a tuple accepts a value position for
+ // a name that is only a type, so this probe answers "is it importable"
+ // without also asking "is it a value", which is a different question.
+ body: `import { ${symbol} } from '${pkg}';\nexport type P = [typeof ${symbol}];\n`,
+ pair,
+ };
+ });
+ if (probes.length === 0) return new Set();
+ const run = compileSnippets({ root, compiled: probes, paths, declaredSpecifiers });
+ const missing = new Set();
+ for (const { block, diagnostics } of run.semanticFailures) {
+ if (diagnostics.some((d) => d.code === 2305 || d.code === 2307)) missing.add(block.pair);
+ }
+ return missing;
+}
+
+/**
+ * The ONE transformation, applied per block. See the header.
+ *
+ * @param {{ symbol: string, package: string, body: string }} block
+ * @param {Set} missing pairs the export probe could not import
+ */
+export function preludeFor(block, missing) {
+ const references = new RegExp(`\\b${block.symbol}\\b`).test(block.body);
+ const alreadyImported = new RegExp(`import[^;]*\\b${block.symbol}\\b[^;]*from`).test(block.body);
+ const onPublicEntry = !missing.has(`${block.package} ${block.symbol}`);
+ return references && !alreadyImported && onPublicEntry
+ ? `import { ${block.symbol} } from '${block.package}';\n`
+ : '';
+}
+
+/** `path:line symbol` — the ledger key for one block. */
+export function ledgerKey(block) {
+ return `${block.file}:${block.line} ${block.symbol}`;
+}
+
+/**
+ * Compare a block's actual diagnostic codes against the row that declares it.
+ * Sorted, de-duplicated and compared as a SET: a compiler that reports the same
+ * failure twice has not changed the failure.
+ *
+ * @param {number[]} actual
+ * @param {number[]} declared
+ */
+export function codesMatch(actual, declared) {
+ const a = [...new Set(actual)].sort((x, y) => x - y);
+ const d = [...new Set(declared)].sort((x, y) => x - y);
+ return a.length === d.length && a.every((code, i) => code === d[i]);
+}
+
+/**
+ * The verdicts, from a completed run. Pure: it prints nothing and it exits
+ * nothing, so the test can hand it a fixture ledger and read the same answers
+ * `main` reads.
+ *
+ * @param {{ results: {key: string, codes: number[]}[], ledger: Record }} input
+ */
+export function judge({ results, ledger }) {
+ const findings = [];
+ const seen = new Set(results.map((r) => r.key));
+ const exempt = [];
+ for (const result of results) {
+ const row = ledger[result.key];
+ const failed = result.codes.length > 0;
+ if (!failed) {
+ if (row) {
+ findings.push({
+ reason: 'stale-ledger-row',
+ site: result.key,
+ detail:
+ 'this example COMPILES now — the debt was paid and the row outlived it. Delete the row.',
+ });
+ }
+ continue;
+ }
+ if (!row) {
+ findings.push({ reason: 'undeclared-failure', site: result.key, codes: result.codes });
+ continue;
+ }
+ if (!codesMatch(result.codes, row.codes)) {
+ findings.push({
+ reason: 'ledger-row-drifted',
+ site: result.key,
+ detail:
+ `declared TS${[...new Set(row.codes)].sort((a, b) => a - b).join(', TS')} but produces ` +
+ `TS${[...new Set(result.codes)].sort((a, b) => a - b).join(', TS')} — the failure changed ` +
+ 'underneath the declaration. Re-derive the row.',
+ });
+ continue;
+ }
+ exempt.push(result.key);
+ }
+ for (const [key, row] of Object.entries(ledger)) {
+ if (!seen.has(key)) {
+ findings.push({
+ reason: 'stale-ledger-row',
+ site: key,
+ detail: 'no such example in the scan set',
+ });
+ continue;
+ }
+ if (!row.reason || row.reason.trim().length < MIN_REASON_LENGTH) {
+ findings.push({
+ reason: 'unexplained-ledger-row',
+ site: key,
+ detail: 'an entry with no written reason is not a declaration',
+ });
+ }
+ }
+ return { findings, exempt };
+}
+
+// ── Reporting ────────────────────────────────────────────────────────────────
+
+/**
+ * The funnel. Printed every run so the enforced number stays derived.
+ *
+ * @param {ReturnType} census
+ */
+export function funnelLines(census) {
+ const row = (n, label) => ` ${String(n).padStart(5)} ${label}`;
+ return [
+ 'Population funnel (every narrowing step, so the enforced number is derived):',
+ row(census.files.length, `source files under ${PACKAGES_DIR}/NAME/${SOURCE_SUBDIR} (.ts/.tsx)`),
+ row(census.excludedAsTooling.length, ' ... tooling files excluded (tests, mocks, benchmarks, stories)'),
+ row(census.tags.length, '`@example` tags found by the AST'),
+ row(census.tags.length - census.inSources.length, ' ... in a tooling file'),
+ row(
+ census.inSources.length - census.exported.length,
+ ' ... on a declaration that is not exported (or has no name)',
+ ),
+ row(census.exported.length, '`@example` tags on EXPORTED declarations'),
+ row(census.withTsFence.length, ' ... carrying a ts/tsx/typescript fence'),
+ row(census.otherFenceOnly.length, ' ... carrying only a non-ts fence'),
+ row(census.bare.length, ' ... BARE, no fence — counted, never compiled (see the header)'),
+ row(census.blocks.length, 'BLOCKS in the compiled tier'),
+ ];
+}
+
+function formatDiagnostic(diagnostic, block) {
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, ' ');
+ if (diagnostic.file && typeof diagnostic.start === 'number') {
+ const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
+ return `${block.doc}:${block.fenceLine + line}:${character + 1} TS${diagnostic.code}: ${message}`;
+ }
+ return `${block.doc}:${block.fenceLine} TS${diagnostic.code}: ${message}`;
+}
+
+// ── main ─────────────────────────────────────────────────────────────────────
+
+export const EXIT_CODES = {
+ /** Every covered example compiles or fails exactly as its row declares. */
+ verified: 0,
+ /** The gate RAN. An example or the ledger is at fault. */
+ examplesFailed: 1,
+ /** The gate COULD NOT RUN. Nothing printed is a verdict about any example. */
+ couldNotRun: 2,
+};
+
+function main() {
+ const census = exampleCensus({ root: repoRoot });
+
+ if (census.files.length === 0) {
+ console.error(
+ `The walk collected 0 file(s) under ${PACKAGES_DIR}/NAME/${SOURCE_SUBDIR}, so every count below ` +
+ 'would be a zero that means nothing. A walk that collapsed is a verdict about this instrument, ' +
+ 'never about the corpus.',
+ );
+ return EXIT_CODES.couldNotRun;
+ }
+ if (census.blocks.length === 0) {
+ console.error(
+ 'The compiled tier is EMPTY: the walk found files but no ts/tsx fenced `@example` on any exported ' +
+ 'declaration. A gate that judges nothing must not report a pass.',
+ );
+ return EXIT_CODES.couldNotRun;
+ }
+
+ // The sibling's `analyze()` derives the built-`.d.ts` map and the declared
+ // dependency reach for the DOCUMENTATION corpus. Reused wholesale: an
+ // `@example` block imports the same workspace packages a documentation fence
+ // does, and a gate that re-derived the map would be a second answer to a
+ // question that already has one.
+ const state = analyze({ root: repoRoot });
+ const blocking = state.findings.filter(
+ (f) => f.reason === 'unbuilt-package' || f.reason === 'source-typed-package',
+ );
+ if (blocking.length > 0) {
+ console.error('THE GATE COULD NOT RUN — the packages these examples import are not built:');
+ for (const f of blocking) console.error(` ${f.site}: ${f.detail}`);
+ console.error(
+ '\n pnpm exec turbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter) --concurrency=2\n' +
+ ' pnpm check:doc-examples',
+ );
+ return EXIT_CODES.couldNotRun;
+ }
+
+ const missing = probeExportedSymbols({
+ root: repoRoot,
+ blocks: census.blocks,
+ paths: state.paths,
+ declaredSpecifiers: state.declaredSpecifiers,
+ });
+
+ const compiled = census.blocks.map((block) => {
+ const prelude = preludeFor(block, missing);
+ return {
+ doc: block.file,
+ // `formatDiagnostic` prints `fenceLine + line`. The prelude shifts the
+ // body down by its own line count, so the anchor is pulled back by the
+ // same amount and a printed number still points at the real source line.
+ fenceLine: block.line - (prelude === '' ? 0 : prelude.split('\n').length - 1),
+ language: block.language,
+ quoteDepth: 0,
+ fragmentReason: null,
+ body: prelude + block.body,
+ key: ledgerKey(block),
+ injected: prelude !== '',
+ };
+ });
+
+ const run = compileSnippets({
+ root: repoRoot,
+ compiled,
+ paths: state.paths,
+ declaredSpecifiers: state.declaredSpecifiers,
+ });
+
+ // ── the harness's own controls, the sibling's, unmodified ─────────────────
+ // A program that resolves everything to `any` reports a clean corpus forever,
+ // and a clean corpus is exactly what this card must not manufacture.
+ const controls = [];
+ if (!run.resolvedFileName || !/[\\/]dist[\\/].*\.d\.ts$/.test(run.resolvedFileName)) {
+ controls.push(
+ `resolution did not land on a built artifact (${run.resolvedFileName ?? 'unresolved'})`,
+ );
+ }
+ if (run.srcLeaks.length > 0) {
+ controls.push(
+ `${run.srcLeaks.length} file(s) under a package's src/ entered the program, e.g. ${run.srcLeaks[0]}`,
+ );
+ }
+ if (!run.sentinelDiagnostics.map((d) => d.code).includes(2305)) {
+ controls.push("the planted sentinel produced no TS2305 — the program is resolving everything to 'any'");
+ }
+ if (run.positiveDiagnostics.length > 0) {
+ controls.push(
+ `the positive control failed (${ts.flattenDiagnosticMessageText(run.positiveDiagnostics[0].messageText, ' ')})`,
+ );
+ }
+ if (controls.length > 0) {
+ console.error('HARNESS CONTROL FAILED — no verdict below is a fact about any example:');
+ for (const c of controls) console.error(` - ${c}`);
+ return EXIT_CODES.couldNotRun;
+ }
+
+ // ── collect one result per block ──────────────────────────────────────────
+ const codesOf = new Map();
+ const detailOf = new Map();
+ for (const { block, diagnostics } of run.parseFailures) {
+ codesOf.set(block.key, diagnostics.map((d) => d.code));
+ detailOf.set(block.key, diagnostics.map((d) => `[syntax] ${formatDiagnostic(d, block)}`));
+ }
+ for (const { block, diagnostics } of run.semanticFailures) {
+ codesOf.set(block.key, diagnostics.map((d) => d.code));
+ detailOf.set(block.key, diagnostics.map((d) => `[semantic] ${formatDiagnostic(d, block)}`));
+ }
+ for (const { block, specifiers } of run.boundFailures) {
+ codesOf.set(block.key, [0]);
+ detailOf.set(block.key, [
+ `[bound] ${block.doc}:${block.fenceLine} imports ${specifiers.map((s) => `'${s}'`).join(', ')}, ` +
+ "which resolve only through this repository's ROOT package.json",
+ ]);
+ }
+ const results = compiled.map((block) => ({ key: block.key, codes: codesOf.get(block.key) ?? [] }));
+
+ const { findings, exempt } = judge({ results, ledger: UNGATED_EXAMPLES });
+
+ // ── report ───────────────────────────────────────────────────────────────
+ for (const line of funnelLines(census)) console.log(line);
+ console.log('');
+ console.log('Controls:');
+ console.log(` resolution ${run.resolvedFileName}`);
+ console.log(
+ ` sentinel importing a name no package exports produced ${run.sentinelDiagnostics.length} diagnostic(s) (TS2305)`,
+ );
+ console.log(` positive importing a real export produced ${run.positiveDiagnostics.length} diagnostic(s)`);
+ console.log(` src leaks ${run.srcLeaks.length}`);
+ console.log(
+ ` injection ${compiled.filter((b) => b.injected).length} of ${compiled.length} block(s) received the documented symbol's import; ` +
+ `${missing.size} documented symbol(s) are NOT on a public entry`,
+ );
+ for (const pair of [...missing].sort()) console.log(` not on a public entry: ${pair}`);
+ console.log('');
+
+ const clean = results.length - codesOf.size;
+ console.log(
+ `Examples: ${results.length} block(s) — ${clean} compile, ${codesOf.size} fail, ` +
+ `${exempt.length} of those declared in the ledger (${Object.keys(UNGATED_EXAMPLES).length} row(s)).`,
+ );
+
+ if (findings.length === 0) {
+ console.log('\nEvery covered `@example` compiles, or fails exactly as its ledger row declares.');
+ return EXIT_CODES.verified;
+ }
+
+ console.error('');
+ for (const finding of findings) {
+ if (finding.reason === 'undeclared-failure') {
+ console.error(`UNDECLARED FAILURE ${finding.site}`);
+ for (const line of detailOf.get(finding.site) ?? []) console.error(` ${line}`);
+ console.error(
+ ` Fix the example, or declare it: add a row keyed \`${finding.site}\` carrying ` +
+ `codes [${[...new Set(finding.codes)].sort((a, b) => a - b).join(', ')}], a written reason and the card that owns it.`,
+ );
+ } else {
+ console.error(`${finding.reason.toUpperCase().replace(/-/g, ' ')} ${finding.site}`);
+ console.error(` ${finding.detail}`);
+ }
+ }
+ console.error(
+ '\n`@example` blocks on exported symbols must compile against the built types. See the header of this script.',
+ );
+ return EXIT_CODES.examplesFailed;
+}
+
+if (isEntrypoint(import.meta.url)) {
+ process.exit(main());
+}
+
+export { main };