Skip to content

Commit b172e2e

Browse files
Elon Muskclaude
andauthored
fix(scripts): doc-authoring Rule 3 reaches a GENERATED guidance table (#13283)
A guidance table does not have to be written as an object literal. Two in packages/spec are BUILT -- one prescription filed under each of a list of keys via `Object.fromEntries(keys.map((k) => [k, '...']))`: - SEPARATOR_NAV_ITEM_GUIDANCE (ui/app.zod.ts), spread into the `guidance` of the per-variant nav options table; - the container-key prescription at `guidance:` in ui/view.zod.ts, generated inline at the key itself. Both are printed verbatim at a refusing author and both sat outside the rule's population entirely: the climb died on the unrecognised `map` / `fromEntries` call between the callback and its sink, so the fourth population's factory clause -- which teaches the climb to leave a function that BUILDS an options record -- never got the chance to fire. Measured by planting a distinct id in every literal of the nav options table and the five tables it references: 48 of 49 candidate ids red, and the whole of SEPARATOR_NAV_ITEM_GUIDANCE (four prose literals) silent. `fromEntries` / `map` / `flatMap` join TRANSPARENT_CALLS. Transparency only lets the climb CONTINUE -- it must still terminate at a recognised position -- so a `.map()` in an ordinary helper stays silent, pinned as a precision case beside the two new red/green pairs. Population +19 strings (strictObject 3349 -> 3364, functionBuilt 217 -> 221), 0 new violations: none of the newly reached prose carries an internal id. Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw Co-authored-by: Claude <noreply@anthropic.com>
1 parent b6d3d76 commit b172e2e

1 file changed

Lines changed: 114 additions & 5 deletions

File tree

scripts/check-doc-authoring.mjs

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -551,11 +551,47 @@ const STRICT_OBJECT_CALLS = new Set(['strictObject', 'strictObjectError']);
551551
const TOMBSTONE_CALLS = new Set(['retiredKey']);
552552

553553
/**
554-
* Wrappers that pass their argument through unchanged, so the climb continues
555-
* rather than stopping. `Object.freeze({ … })` around a guidance table is the
556-
* measured case; a stop here would drop the whole table.
554+
* Wrappers whose VALUE is the text the climb is already carrying, so the climb
555+
* continues rather than stopping. `Object.freeze({ … })` around a guidance
556+
* table is the original measured case; a stop here would drop the whole table.
557+
*
558+
* ## The GENERATED table: `Object.fromEntries(keys.map((k) => [k, '…']))`
559+
*
560+
* A guidance table does not have to be WRITTEN as an object literal. Two in
561+
* this tree are BUILT — one prescription filed under each of a list of keys,
562+
* because every one of those keys has the same answer:
563+
*
564+
* - `SEPARATOR_NAV_ITEM_GUIDANCE` (`ui/app.zod.ts`), spread into the
565+
* `guidance` of the per-variant nav options table;
566+
* - the container-key prescription at `guidance:` in `ui/view.zod.ts`,
567+
* generated inline at the key itself.
568+
*
569+
* Both are printed verbatim at a refusing author, and both sat OUTSIDE THE
570+
* POPULATION ENTIRELY — measured by planting a distinct id in every literal of
571+
* the nav options table and the five tables it references: 48 of 49 candidate
572+
* ids red, and the whole of `SEPARATOR_NAV_ITEM_GUIDANCE` — four prose
573+
* literals — silent. It is the fourth population's defect one shape over: the
574+
* factory clause taught the climb to leave a function that BUILDS an options
575+
* record, while a table built by a `.map()` INSIDE one stayed unreachable,
576+
* because the climb died on the unrecognised `map` / `fromEntries` call sitting
577+
* between the callback and its sink.
578+
*
579+
* ⛔ This does NOT make `.map()` a text position. Transparency only lets the
580+
* climb CONTINUE; it must still terminate at a recognised position — a
581+
* `message:` / `error:`, a `.describe()`, a tombstone argument, a
582+
* STRICT_OPTION_KEYS key under a `strictObject` call, or a
583+
* {@link collectTextSinkConsts} sink. A `.map()` in an ordinary helper reaches
584+
* none of those and stays silent, exactly as before. What transparency buys is
585+
* that a table's SPELLING — literal vs generated — stops deciding whether its
586+
* prose is judged, which is the property this rule was missing.
587+
*
588+
* `flatMap` rides with `map` rather than waiting for a live case: the pair is
589+
* one spelling of one idea, and a set that knows only half of it is the next
590+
* silent miss (`data/driver/config-registry.zod.ts` builds its alias tables
591+
* with `flatMap` today — values, not prose, so it moves no verdict, but it is
592+
* the shape arriving).
557593
*/
558-
const TRANSPARENT_CALLS = new Set(['freeze']);
594+
const TRANSPARENT_CALLS = new Set(['freeze', 'fromEntries', 'map', 'flatMap']);
559595

560596
/**
561597
* zod validators whose trailing positional argument is a refusal message.
@@ -1779,7 +1815,7 @@ function selfTest() {
17791815
console.error(`\n✗ check-doc-authoring self-test failed:\n${failures.join('\n')}\n`);
17801816
process.exit(1);
17811817
}
1782-
console.log('✓ check-doc-authoring self-test: scope wiring (.claude and the live docs/ corpus in, .claude/worktrees and docs/{audits,handoff,plans} out), detection, the dead-root hard error (red when a ROOT is renamed, green when restored), the empty-scan hard error (red when a root yields nothing and when the whole scan does, green when restored), the published-catalog internal-id rule (red on a planted id in prose, in a fenced comment and in the repo#NNNN spelling, green when removed; hex colours, version numbers, HTTP codes, array indices and the "#1" ordinal all pass; references/ reached, generated artifacts and the internal roots out; the `#<n>` placeholder passes while the concrete ids it replaced stay red, with no exemption to reach for), the spec customer-facing-text internal-id rule (red on an id planted on a LATER line of a concatenated message — the shape a line-oriented census cannot see, proven here — and in a template chain, a positional validator message, the repo#NNNN spelling, a nested strictObject `guidance` prescription, a HOISTED guidance const, a `KeySetGuidance` const consumed only CROSS-MODULE in both the annotated and the `as const satisfies` spelling, a HOISTED refusal message, a `retiredKey()` tombstone, `new Map` and `Object.freeze` guidance tables, `.describe()` prose, and the nested `guidance` of a whole options table written `satisfies StrictObjectOptions`; green when removed; an ADR id on a tombstone, a `.default()` VALUE, `history`/`guidance` outside a strictObject options position, `extraKeys` key names and an inferred local that merely MENTIONS `KeySetGuidance` all pass; test bodies out; the seen floor is PER BUCKET so one matcher rotting while the others carry the total still reds; and the two TYPE ANCHORS are pinned on the predicate itself — the annotation, `satisfies` and `as const satisfies` spellings all read as a strictObject options position while some other satisfied type does not, and the `*_STRICT_OPTIONS` NAME branch still fires where no type is written at all — which is the only place they can be told apart, since end to end they are redundant), the fourth population — customer-facing text BUILT INSIDE A FUNCTION (red on an id in an inline `error: () =>` callback, in a const the callback only dispatches to, inside a `message:` builder function, RETURNED from a tombstone-prescription builder, in a `: StrictObjectOptions` options factory, and in a plain `error:` string; ⛔ the body of an ordinary helper and a local inside a recognised factory stay unswept, because the climb crosses a function only when the FUNCTION sits in a recognised position; and `functionBuilt` carries its own blindness floor, since an unrecognised spelling produces no flag SILENTLY), the FIFTH population — prose built inside plain `function` DECLARATIONS (#13156: red on an id in a declaration consumed by `message:`, RETURNED to a `retiredKey()` argument, and in a const the declaration only dispatches to; its own `functionDeclared` bucket with its own floor, so the declaration clause rotting cannot hide behind the arrows; ⛔ an unconsumed declaration and one consumed only by an unrecognised call stay unswept — the clause is the fourth population\'s, one declaration form over, never an unconditional crawl), the Rule 3 boundary OUTPUT (names the position-based root AND the ledgered cross-package leg\'s root, exclusion and baseline, no longer claims siblings are unscanned, and lists every floored bucket — derived from the same constants the scans read), the CROSS-PACKAGE prose-id leg (#13297: a concatenation-split id in a plain helper is counted — total-string coverage, no position climb to rot; a `//` comment, a test body and the spec subtree are out; an id inside a template\'s embedded expression counts exactly once; a 6-digit colour never matches while the cross-repo spelling\'s id half does; the prefilter is a superset of the id regex on every counted site; and the ledger arithmetic answers all three verdicts from one measurement — exact baseline green, empty baseline all-growth, over-pinned baseline stale without invented growth) and the dispatch-gates declaration (every separator-less walked root declared as a subtree — `packages/**` included since #13297 — nothing declared this gate does not walk, the over-claim bounded to SKIP_PATHS) all hold.');
1818+
console.log('✓ check-doc-authoring self-test: scope wiring (.claude and the live docs/ corpus in, .claude/worktrees and docs/{audits,handoff,plans} out), detection, the dead-root hard error (red when a ROOT is renamed, green when restored), the empty-scan hard error (red when a root yields nothing and when the whole scan does, green when restored), the published-catalog internal-id rule (red on a planted id in prose, in a fenced comment and in the repo#NNNN spelling, green when removed; hex colours, version numbers, HTTP codes, array indices and the "#1" ordinal all pass; references/ reached, generated artifacts and the internal roots out; the `#<n>` placeholder passes while the concrete ids it replaced stay red, with no exemption to reach for), the spec customer-facing-text internal-id rule (red on an id planted on a LATER line of a concatenated message — the shape a line-oriented census cannot see, proven here — and in a template chain, a positional validator message, the repo#NNNN spelling, a nested strictObject `guidance` prescription, a HOISTED guidance const, a `KeySetGuidance` const consumed only CROSS-MODULE in both the annotated and the `as const satisfies` spelling, a HOISTED refusal message, a `retiredKey()` tombstone, `new Map` and `Object.freeze` guidance tables, `.describe()` prose, and the nested `guidance` of a whole options table written `satisfies StrictObjectOptions`; green when removed; an ADR id on a tombstone, a `.default()` VALUE, `history`/`guidance` outside a strictObject options position, `extraKeys` key names and an inferred local that merely MENTIONS `KeySetGuidance` all pass; test bodies out; the seen floor is PER BUCKET so one matcher rotting while the others carry the total still reds; and the two TYPE ANCHORS are pinned on the predicate itself — the annotation, `satisfies` and `as const satisfies` spellings all read as a strictObject options position while some other satisfied type does not, and the `*_STRICT_OPTIONS` NAME branch still fires where no type is written at all — which is the only place they can be told apart, since end to end they are redundant), the fourth population — customer-facing text BUILT INSIDE A FUNCTION (red on an id in an inline `error: () =>` callback, in a const the callback only dispatches to, inside a `message:` builder function, RETURNED from a tombstone-prescription builder, in a `: StrictObjectOptions` options factory, and in a plain `error:` string; ⛔ the body of an ordinary helper and a local inside a recognised factory stay unswept, because the climb crosses a function only when the FUNCTION sits in a recognised position; and `functionBuilt` carries its own blindness floor, since an unrecognised spelling produces no flag SILENTLY), the FIFTH population — prose built inside plain `function` DECLARATIONS (#13156: red on an id in a declaration consumed by `message:`, RETURNED to a `retiredKey()` argument, and in a const the declaration only dispatches to; its own `functionDeclared` bucket with its own floor, so the declaration clause rotting cannot hide behind the arrows; ⛔ an unconsumed declaration and one consumed only by an unrecognised call stay unswept — the clause is the fourth population\'s, one declaration form over, never an unconditional crawl), the GENERATED table — a prescription filed under each of a list of keys by `Object.fromEntries(keys.map(…))` rather than written as an object literal (red both HOISTED into a const spread into an options factory\'s `guidance` and generated INLINE at the `guidance:` key itself, green when the id is removed; ⛔ and a generated VALUE table reaching no sink stays unswept, because `.map()` is TRANSPARENT to the climb and never a position of its own), the Rule 3 boundary OUTPUT (names the position-based root AND the ledgered cross-package leg\'s root, exclusion and baseline, no longer claims siblings are unscanned, and lists every floored bucket — derived from the same constants the scans read), the CROSS-PACKAGE prose-id leg (#13297: a concatenation-split id in a plain helper is counted — total-string coverage, no position climb to rot; a `//` comment, a test body and the spec subtree are out; an id inside a template\'s embedded expression counts exactly once; a 6-digit colour never matches while the cross-repo spelling\'s id half does; the prefilter is a superset of the id regex on every counted site; and the ledger arithmetic answers all three verdicts from one measurement — exact baseline green, empty baseline all-growth, over-pinned baseline stale without invented growth) and the dispatch-gates declaration (every separator-less walked root declared as a subtree — `packages/**` included since #13297 — nothing declared this gate does not walk, the over-claim bounded to SKIP_PATHS) all hold.');
17831819
}
17841820

17851821
/**
@@ -2381,6 +2417,59 @@ function selfTestRule3(expect) {
23812417
expect('the dispatched-const red names the const it travelled through',
23822418
r.violations[0]?.where, 'via RULING_SENTENCE');
23832419

2420+
// RED #23 — a GENERATED guidance table: one prescription filed under each
2421+
// of a list of keys via `Object.fromEntries(keys.map(…))`, hoisted into a
2422+
// const and spread into an options FACTORY's `guidance`. This is
2423+
// `SEPARATOR_NAV_ITEM_GUIDANCE` (`ui/app.zod.ts`), reduced. Measured live:
2424+
// every literal of the surrounding nav options table was reachable and this
2425+
// whole table was not, because the climb died on `map` / `fromEntries`
2426+
// between the callback and its sink.
2427+
const GENERATED_TABLE = (prose) => [
2428+
"import { z } from 'zod';",
2429+
"import { strictObject } from '../shared/strict-object';",
2430+
"import type { StrictObjectOptions } from '../shared/strict-object';",
2431+
'const SEPARATOR_GUIDANCE: Readonly<Record<string, string>> = Object.fromEntries(',
2432+
" ['label', 'icon'].map((key) => [key, `\\`${key}\\` " + prose + '`]),',
2433+
');',
2434+
'const navItemSurface = (variant: string): StrictObjectOptions => ({',
2435+
' surface: `this \\`${variant}\\` navigation item`,',
2436+
" guidance: { ...(variant === 'separator' ? SEPARATOR_GUIDANCE : {}) },",
2437+
'});',
2438+
"export const S = strictObject(navItemSurface('separator'), { id: z.string() });",
2439+
].join('\n');
2440+
writeFileSync(target, GENERATED_TABLE('is not a separator key (#4286).'));
2441+
r = scan();
2442+
expect('an id in a GENERATED guidance table reaching an options factory is RED',
2443+
r.violations.length, 1);
2444+
expect('the generated-table red names the sink it travelled through',
2445+
r.violations[0]?.where, 'via SEPARATOR_GUIDANCE (built in a function)');
2446+
writeFileSync(target, GENERATED_TABLE('is not a separator key.'));
2447+
expect('...and green once the id is gone', scan().violations.length, 0);
2448+
2449+
// RED #24 — the same generation written INLINE at the `guidance:` key
2450+
// rather than hoisted, so it reaches its position with no sink const in the
2451+
// path at all. This is `ui/view.zod.ts`'s container-key prescription,
2452+
// reduced. Both spellings are live, and a fix that closed only one of them
2453+
// would leave the other silent with nothing to say so.
2454+
const INLINE_GENERATED = (prose) => [
2455+
"import { z } from 'zod';",
2456+
"import { strictObject } from '../shared/strict-object';",
2457+
'export const S = strictObject({',
2458+
" surface: 'this view container',",
2459+
' guidance: Object.fromEntries(',
2460+
" ['type', 'columns'].map((k) => [k, `\\`${k}\\` " + prose + '`]),',
2461+
' ),',
2462+
'}, { name: z.string() });',
2463+
].join('\n');
2464+
writeFileSync(target, INLINE_GENERATED('belongs to a single VIEW (#4286).'));
2465+
r = scan();
2466+
expect('an id in a guidance table generated INLINE at the key is RED',
2467+
r.violations.length, 1);
2468+
expect('the inline generated-table red names the position',
2469+
r.violations[0]?.where, 'strictObject guidance (built in a function)');
2470+
writeFileSync(target, INLINE_GENERATED('belongs to a single VIEW.'));
2471+
expect('...and green once the id is gone', scan().violations.length, 0);
2472+
23842473
// ── Precision: what must NEVER fire ─────────────────────────────────────
23852474
//
23862475
// ⛔ The clause is NOT "climb through function bodies". These three are the
@@ -2401,6 +2490,26 @@ function selfTestRule3(expect) {
24012490
].join('\n'));
24022491
expect('precision — an ordinary helper\'s body is NOT swept', scan().violations.length, 0);
24032492

2493+
// ⭐ The precision case the TRANSPARENT_CALLS widening owes: `.map()` is
2494+
// TRANSPARENT, never a position. The same `Object.fromEntries(keys.map(…))`
2495+
// generation whose prose is RED above must stay silent when what it builds
2496+
// is a VALUE table nothing customer-facing consumes — a `.map()` is one of
2497+
// the commonest expressions in this tree, and a widening that swept every
2498+
// string returned from one would report identifiers, slugs and enum members
2499+
// as refusal prose. The climb terminating at a recognised position is the
2500+
// only thing holding that line, so it is pinned rather than assumed.
2501+
writeFileSync(target, [
2502+
"import { z } from 'zod';",
2503+
"const SLUGS: Readonly<Record<string, string>> = Object.fromEntries(",
2504+
" ['draft', 'live'].map((k) => [k, `${k}-#4286`]),",
2505+
');',
2506+
"const TOKENS = ['a', 'b'].flatMap((k) => [`${k}#4286`]);",
2507+
'export const S = z.object({ a: z.string() });',
2508+
'export const T = { SLUGS, TOKENS };',
2509+
].join('\n'));
2510+
expect('precision — a generated VALUE table reaching no sink is NOT swept',
2511+
scan().violations.length, 0);
2512+
24042513
// The same boundary, declaration form (#13156). ⛔ The widening is NOT an
24052514
// unconditional crawl of function bodies: a plain `function` DECLARATION
24062515
// nothing customer-facing consumes stays unreachable — a `.default()`

0 commit comments

Comments
 (0)