@@ -3562,6 +3562,116 @@ export function isTestFilePath(path) {
35623562 return /\.(test|spec)\.(ts|tsx|mts|cts)$/.test(path);
35633563}
35643564
3565+ /**
3566+ * Does this file carry a value shaped like an ADR-0112 error/notice CODE — the
3567+ * content trigger for `check:dispatcher-error-vocabulary` (#12850)?
3568+ *
3569+ * ## Why this one is judged from CONTENT, when every other kind reads a PATH
3570+ *
3571+ * The four predicates around this one answer questions about a path: is it a
3572+ * test file, does its package own an extract config, is it a gate script, is it
3573+ * in the root program. This one cannot, and the reason is recorded elsewhere in
3574+ * the tree rather than argued here. The vocabulary gate computes its own
3575+ * population by walking a bare top-level root, and `scripts/pm/bare-root-
3576+ * worklist.mjs` already carries the verdict for that spelling: REFUSE-WIDE,
3577+ * "non-test sources plus manifests, 1898 of 4903 (39%) — same trade" as the
3578+ * sibling it is grouped with, whose note spells the trade out — a declaration
3579+ * that "would name this gate for every card in the repo that touches a
3580+ * package".
3581+ *
3582+ * So there is no path prefix to give this entry. Inventing one would not merely
3583+ * be imprecise: it would mirror, inside this file, a population the ledger next
3584+ * door has already refused to spell — and mirroring a fact another file states
3585+ * is the drift this whole script is written against. A trigger that looks right
3586+ * and covers a third of the tree is strictly worse than today's honest silence,
3587+ * because a lead that fires on every card is one a reader learns to skip.
3588+ *
3589+ * What IS derivable from a diff is the thing the gate actually bites on: a code
3590+ * value entering the tree. That is content, so this predicate reads content.
3591+ *
3592+ * ## What it matches, and why it is deliberately broader than the gate
3593+ *
3594+ * Two limbs, both anchored on the shapes an error code is written in here:
3595+ *
3596+ * - STAMP POSITION — the token `code` bound to a quoted literal or to a
3597+ * SCREAMING_SNAKE identifier, through `:` or `=`, optional or not, with an
3598+ * optional `typeof` between. That last part is not decoration: the specimen
3599+ * that cost the CI round trip is `code: typeof CONVERSION_NOTICE_CODE`, and
3600+ * a matcher without it misses the very case this entry exists for.
3601+ * - CONSTANT BINDING — a SCREAMING_SNAKE binding whose value is a quoted
3602+ * SCREAMING_SNAKE string, which is how this repo declares a code before any
3603+ * `code` token is anywhere near it (`const CONVERSION_NOTICE_CODE =
3604+ * 'OS_METADATA_CONVERTED'`). The specimen file matches on both limbs; a
3605+ * tree sweep found no file that only the second reaches, so neither limb is
3606+ * carrying the other.
3607+ *
3608+ * ⛔ This is NOT a copy of the gate's own `SHAPES` table and must never become
3609+ * one. A copy would be a second spelling of a fact that file owns, and it would
3610+ * go stale in the SILENT direction the day `SHAPES` grows an indirection — that
3611+ * table has grown twice for exactly that reason. Being broader than `SHAPES` is
3612+ * what makes staleness impossible in the expensive direction: a shape added
3613+ * there is already inside this predicate's wider net.
3614+ *
3615+ * ## The false-positive trade, stated so nobody assumes narrowing is free
3616+ *
3617+ * This predicate over-matches on purpose. It fires for a file that merely
3618+ * CONTAINS a code, not only one that adds a new one, and it does not ask
3619+ * whether the value is registered — both would need the gate's own resolver,
3620+ * which is the thing this must not import. Cost of a false positive: one extra
3621+ * gate run, and this gate needs no build and answers for the whole tree in one
3622+ * pass. Cost of a false negative, measured on #12843: a full CI round trip,
3623+ * because the derived union reads green locally and the gate reds in `Lint &
3624+ * Repo Gates`. The two costs are not close, so the wide side is the correct
3625+ * one. ⚠ Narrowing it later is therefore not a tidy-up — it is a trade against
3626+ * a measured price, and it needs the same kind of measurement to justify.
3627+ *
3628+ * Measured on this tree when written, over 7169 tracked files: 196 of the 2281
3629+ * non-test TypeScript files match (8.6%, 2.7% of the tree) — two orders of
3630+ * discrimination away from the 39% a path spelling would have named. 194 of
3631+ * those 196 are inside the gate's own scanned population; the other two are one
3632+ * file each under the app and example roots, which the gate does not scan. Two
3633+ * wasted runs across the whole tree is the entire cost of leaving the
3634+ * population half out, and leaving it out is what keeps this file from spelling
3635+ * a pathy literal it would then match cards through — see the note on `why`
3636+ * prose in the table's docblock.
3637+ *
3638+ * ## What it cannot see, said out loud rather than discovered later
3639+ *
3640+ * A file that does not exist has no content, so this returns false for one —
3641+ * and at DISPATCH time the card's file surface is a hypothesis, which is where
3642+ * a brand-new file carrying a brand-new code lives. The trigger therefore fires
3643+ * for the dev's re-derivation off the merge base (where the file is real and
3644+ * where the missed gate actually costs the round trip) and stays quiet for the
3645+ * PM's hypothetical surface. That asymmetry is the honest one: firing on a path
3646+ * whose content nobody can read would be the path-shaped trigger this entry
3647+ * exists to refuse, wearing a different name. ⛔ Do not close it by falling back
3648+ * to the path half.
3649+ */
3650+ const CODE_STAMP_POSITION = /\bcode\s*\??\s*[:=]\s*(?:typeof\s+)?(?:['"`]|[A-Z][A-Z0-9_]*\b)/;
3651+ const CODE_CONSTANT_BINDING =
3652+ /\b(?:const|readonly|static|let)\s+[A-Z][A-Z0-9_]*\s*(?::[^=;\n]+)?=\s*['"`][A-Z][A-Z0-9_]*['"`]/;
3653+
3654+ /** The file's text, or null when there is nothing on disk to read. */
3655+ function readTrackedSource(path) {
3656+ try {
3657+ return readFileSync(join(ROOT, path), 'utf8');
3658+ } catch {
3659+ return null;
3660+ }
3661+ }
3662+
3663+ export function stampsAnErrorCodeLiteral(path, readSource = readTrackedSource) {
3664+ if (!/\.[cm]?tsx?$/.test(path) || /\.d\.[cm]?ts$/.test(path)) return false;
3665+ if (isTestFilePath(path)) return false;
3666+ const source = readSource(path);
3667+ if (source === null || source === undefined) return false;
3668+ // Comments are masked for the reason the gate masks them: a code DISCUSSED in
3669+ // prose is not a code stamped in source. This narrows nothing the gate would
3670+ // have reported, so it costs no recall in the expensive direction.
3671+ const masked = maskComments(source);
3672+ return CODE_STAMP_POSITION.test(masked) || CODE_CONSTANT_BINDING.test(masked);
3673+ }
3674+
35653675/**
35663676 * Is this path inside the ROOT package's tsc program — the population behind
35673677 * the `@objectstack/spec-monorepo` entry of `check:type-check-debt`?
@@ -4109,6 +4219,15 @@ export function reachesMetadataFormModule(path, modulePaths) {
41094219 * day the entry stops firing by itself (its `matches` reads the flags), so
41104220 * delete it only once the opt-out is the permanent shape rather than a
41114221 * transient one.
4222+ * - error-code entry: when the vocabulary gate's own source declares the
4223+ * population it walks in a form this derivation can read — which today
4224+ * means the bare-root ledger row for it moving off REFUSE-WIDE to a
4225+ * recorded subtree spelling — the ordinary path match names it and this
4226+ * entry is redundant. ⛔ Growing the gate's own SHAPES table does NOT
4227+ * qualify: more stamp positions make the gate see more, and change nothing
4228+ * about whether a dispatch brief can NAME it. ⛔ Nor does this predicate
4229+ * going quiet on a given card: it reads content, so silence about a file
4230+ * nobody can read yet is not evidence in either direction.
41124231 * - root-program entry: when the gate's own source names its root population
41134232 * in a form this derivation can read — a positive literal, or a generated
41144233 * manifest of the resolved program — the ordinary path match names it and
@@ -4213,6 +4332,16 @@ export const CHANGE_KIND_GATES = [
42134332 },
42144333 ],
42154334 },
4335+ {
4336+ kind: 'adds or edits a file carrying an ADR-0112 error or notice CODE (judged from CONTENT — no path derivation can name this gate)',
4337+ matches: stampsAnErrorCodeLiteral,
4338+ gates: [
4339+ {
4340+ name: 'check:dispatcher-error-vocabulary',
4341+ why: 'it sweeps the non-test TypeScript sources under the package root for every site that stamps an error code, and reports each value the registered vocabulary (StandardErrorCode joined with ERROR_CODE_LEDGER) does not contain — so a code arriving through a quoted literal, a SCREAMING_SNAKE constant, a typeof reference to one, or a template moves it. This is the gate no path derivation can name: it computes its own population from a bare top-level root, which the bare-root ledger records as REFUSE-WIDE at 39% of the tracked tree, so it scores the same quiet silence for every card and #12843 paid a CI round trip for that silence. It needs NO build — a source scan, one pass, whole tree, and it names the file and line. Repair by REGISTERING the code where the vocabulary is declared, never by widening a consumer to tolerate it; reconciliation runs BOTH ways, so a table row whose site is gone fails too, and a pending-registration row whose code has since been registered fails as the discharge it is. ⚠ This lead is deliberately WIDE — it fires on a file that merely carries a code-shaped value, not only one that adds a new one — because the wasted run is one cheap gate and the miss is a CI round trip',
4342+ },
4343+ ],
4344+ },
42164345];
42174346
42184347/**
@@ -7548,6 +7677,86 @@ function selfTest() {
75487677 t('the i18n section also names check:i18n-stale-fill, runnably', i18nHit.some((l) => l.includes('- pnpm check:i18n-stale-fill —')));
75497678 t('a path outside every owning package emits no i18n section', !changeKindLines(['packages/objectql/src/engine.ts'], resolved).some((l) => l.includes('check:i18n')));
75507679
7680+ // ── The error-code CONTENT kind (#12850) ─────────────────────────────────
7681+ //
7682+ // The only entry in this table judged from a file's CONTENT rather than its
7683+ // path, so its cases are shaped differently: the limbs are driven through an
7684+ // INJECTED reader (offline, no tree), and the tree itself is used only for
7685+ // the two properties a fixture cannot pin — that the predicate still reaches
7686+ // the real specimen, and that it still DISCRIMINATES.
7687+ const codeSrc = (text) => (_path) => text;
7688+ const stamps = (text, path = 'packages/x/src/a.ts') => stampsAnErrorCodeLiteral(path, codeSrc(text));
7689+ t('a quoted code literal in a stamp position is a hit', stamps("const e = { code: 'NOT_CREATABLE' };"));
7690+ t('a SCREAMING_SNAKE constant in a stamp position is a hit', stamps('const e = { code: NOT_CREATABLE };'));
7691+ t('an assigned code is a hit', stamps("err.code = 'FLOW_FAILED';"));
7692+ t('an optional code FIELD TYPE is a hit', stamps("interface E { code?: 'FLOW_FAILED' }"));
7693+ // The specimen shape from #12843, spelled out: without the `typeof` limb this
7694+ // case is the one that fails, and it is the exact form that cost the round
7695+ // trip — a literal `code` type reached through a named constant.
7696+ t('a typeof reference to a code constant is a hit — the #12843 shape',
7697+ stamps('interface N { code: typeof CONVERSION_NOTICE_CODE; }'));
7698+ // The declaration half of that same shape, which carries no `code` token at
7699+ // all and is therefore invisible to every `code`-anchored limb.
7700+ t('a SCREAMING_SNAKE constant bound to a SCREAMING_SNAKE string is a hit',
7701+ stamps("export const CONVERSION_NOTICE_CODE = 'OS_METADATA_CONVERTED' as const;"));
7702+ t('a file with neither shape is not a hit', !stamps('export function add(a: number, b: number) { return a + b; }'));
7703+ // Masking is load-bearing in the cheap direction only: a code the gate would
7704+ // never report because it is not in source cannot cost a run here either.
7705+ t('a code discussed only in a comment is not a hit', !stamps("// code: 'NOT_CREATABLE' is stamped elsewhere\nexport const x = 1;"));
7706+ t('a lowercase constant binding is not a hit', !stamps("const notACode = 'lowercase';"));
7707+ // Population: the gate does not read tests, declaration files or non-TS, so
7708+ // neither does the lead. Each is driven with content that WOULD hit, so the
7709+ // case fails if the population half stops being consulted.
7710+ t('a test file carrying a stamp is not a hit', !stamps("const e = { code: 'X_Y' };", 'packages/x/src/a.test.ts'));
7711+ t('a d.ts carrying a stamp is not a hit', !stamps("const e = { code: 'X_Y' };", 'packages/x/src/a.d.ts'));
7712+ t('a non-TS file carrying a stamp is not a hit', !stamps("const e = { code: 'X_Y' };", 'packages/x/src/a.md'));
7713+ // The unreadable branch, pinned as its own case because it is the one this
7714+ // entry deliberately does NOT close: at dispatch time the card's surface is a
7715+ // hypothesis, and a file with no content on disk answers false rather than
7716+ // falling back to a path match. A regression here would be silent.
7717+ t('a path with nothing to read is not a hit, and does not throw', !stampsAnErrorCodeLiteral('packages/x/src/a.ts', () => null));
7718+ t('…and the live reader answers the same way for a path the tree does not have',
7719+ !stampsAnErrorCodeLiteral('packages/there-is-no-such-package/src/a.ts'));
7720+
7721+ // Anti-vacuity, against the REAL tree: the shape that cost #12843 a CI round
7722+ // trip must still be reached. Spelled rather than discovered because it IS
7723+ // the specimen — a derived probe would answer about some other file.
7724+ const CODE_SPECIMEN = 'packages/spec/src/conversions/types.ts';
7725+ t('the live tree still carries the #12843 specimen shape, and the predicate reaches it',
7726+ stampsAnErrorCodeLiteral(CODE_SPECIMEN));
7727+ // The discrimination pin, and the one case that holds this card's ruling
7728+ // mechanically: a content trigger is only worth having while it names the
7729+ // gate for SOME cards and not for most. The path spelling this entry refuses
7730+ // would have scored 39%; if a future widening pushes this predicate up there,
7731+ // the entry has become the thing it was written against and this case fails.
7732+ const codeCorpus = trackedFiles().filter((f) => /\.[cm]?tsx?$/.test(f) && !/\.d\.[cm]?ts$/.test(f) && !isTestFilePath(f));
7733+ const codeHits = codeCorpus.filter((f) => stampsAnErrorCodeLiteral(f));
7734+ t(`the content trigger discriminates: ${codeHits.length} of ${codeCorpus.length} non-test TS files (neither vacuous nor tree-wide)`,
7735+ codeCorpus.length > 500 && codeHits.length > 20 && codeHits.length < codeCorpus.length / 4);
7736+
7737+ // The rendered section, driven through THIS entry alone so the count is a
7738+ // statement about the entry rather than about which other kinds happen to
7739+ // fire for the specimen path.
7740+ const codeEntry = CHANGE_KIND_GATES.filter((k) => k.gates.some((g) => g.name === 'check:dispatcher-error-vocabulary'));
7741+ t('exactly one entry in the table names the vocabulary gate', codeEntry.length === 1);
7742+ const codeKind = changeKindLines([CODE_SPECIMEN], resolved, codeEntry);
7743+ t('a code-carrying path emits the convention section', codeKind.length === 2 && codeKind[0].includes('judged from CONTENT'));
7744+ t('and it names the vocabulary gate runnably, anchored on the delimiter',
7745+ codeKind.some((l) => l.includes('- pnpm check:dispatcher-error-vocabulary —')));
7746+ const codeLine = codeKind.find((l) => l.includes('- pnpm check:dispatcher-error-vocabulary —')) ?? '';
7747+ // The `why` owes the three halves a dev cannot re-derive from the command:
7748+ // why no path derivation names it, what the repair direction is, and that it
7749+ // needs no build (unlike the two ratchets in this same table).
7750+ t('the vocabulary line states why no path derivation reaches it', /REFUSE-WIDE/.test(codeLine));
7751+ t('…and pushes the repair to registration rather than to a tolerant consumer',
7752+ /REGISTERING/.test(codeLine) && /never by widening a consumer/.test(codeLine));
7753+ t('…and says it needs no build, unlike the ratchets in this table', /needs NO build/.test(codeLine));
7754+ // The card's second ruling, pinned: the over-broad direction is the chosen
7755+ // one and the trade is written where the next reader will meet it. A silent
7756+ // narrowing that drops this sentence fails here.
7757+ t('…and writes the false-positive trade down, so nobody assumes narrowing is free',
7758+ /deliberately WIDE/.test(codeLine) && /CI round trip/.test(codeLine));
7759+
75517760 // ── The metadata-form edge (#9116) ────────────────────────────────────────
75527761 //
75537762 // The bundles' OTHER producer, and the half no owning-package test can reach:
@@ -7998,6 +8207,12 @@ function selfTest() {
79988207 'check:cross-package-test-inputs is a live family (#10542 moved it here from a path derivation that could name it at 49.6% precision at best)',
79998208 liveFamilies.has('check:cross-package-test-inputs'),
80008209 );
8210+ // #12850's entry, pinned here for the same reason and with one of its own:
8211+ // this is the only gate in the table reached by a CONTENT predicate, so the
8212+ // census guard above is the only thing standing between a rename and a lead
8213+ // that renders STALE on a card nobody re-reads.
8214+ t('check:dispatcher-error-vocabulary is a live family, so naming it is not a guess',
8215+ liveFamilies.has('check:dispatcher-error-vocabulary'));
80018216
80028217 // ── The test-file entry's deletion criterion, MEASURED (#11199) ───────────
80038218 //
0 commit comments