Skip to content

Commit ce244dd

Browse files
claude[bot]claude
andauthored
fix(devx): read NAME.enum.MEMBER so 36 real producers leave the unresolved census (#15736)
`check:error-status-conformance` derives the runtime side of the code/status reconciliation out of source, and REPORTS every declaration it cannot read rather than dropping it. 36 of the 50 declarations in that census were one mechanical shape: a producer stamping `err.code = StandardErrorCode.enum.X` beside a literal status. `buildConstantIndex` recorded nothing at all for a `z.enum([...])` declaration, and `lookup()` walks at most one dot, so the whole family resolved to `undefined` — 36 genuine producers, every one of them with its status already a literal, sat outside the derivation. Index a `z.enum([...])` declaration as `NAME.enum.MEMBER -> 'MEMBER'`, one entry per literal (a Zod enum member's value equals its own name by construction), and let `lookup()` walk that ONE extra segment. A name the array does not list stays absent from the index and its declaration is reported unresolved, never invented; dotted paths in general are still refused, deliberately. Self-test battery 25 pins the shape in all three directions (member resolves, non-member reported, `.enum.` is the only way in); the roster floor and case count move with it: 47 cases -> 50, floor 25 -> 26. Measured on this tree, before -> after: producer sites derived 279 -> 315 (+36, exactly the 36 `.enum.` rows) unresolved declarations 50 -> 14 (0 of them `.enum.` now) reconciled 25 codes / 26 pairs -> unchanged unpinned census 26 (baselined 26) -> unchanged The baseline shrink the card anticipated is EMPTY on today's main, and that is a reading rather than an omission: the 36 sites introduce zero NEW (code, status) pairs. All six codes they name (VALIDATION_ERROR, INVALID_FIELD, INVALID_FILTER, INVALID_QUERY, NOT_IMPLEMENTED, DATABASE_ERROR) already had a derived producer via another rule, and none of the six appears in `scripts/error-status-unpinned-baseline.json`. No row becomes `nowPinned`, so the baseline is untouched. Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk Co-authored-by: Claude <noreply@anthropic.com>
1 parent d57333b commit ce244dd

1 file changed

Lines changed: 85 additions & 8 deletions

File tree

scripts/check-error-status-conformance.mjs

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,12 @@ const SELF_TEST_BATTERIES = Object.freeze({
156156
'22 — nowPinned, the DOC-REMOVED branch: the #9266/#9563 counterfactual,': 2,
157157
'23 — R5b: the body span is brace-BALANCED, and the braces it counts are': 3,
158158
'24 — R6, the ASSIGNMENT form, and the two bounds that keep it honest.': 4,
159+
'25 — `z.enum([...])` members: the ONE extra segment `lookup` walks, and the': 3,
159160
});
160161

161162
// DELETING an entry silences that battery's floor exactly as effectively as
162163
// zeroing it, so the roster's own size is pinned too.
163-
const SELF_TEST_BATTERY_FLOOR = 25;
164+
const SELF_TEST_BATTERY_FLOOR = 26;
164165

165166
// The key an assertion is filed under when no battery is open. It is not a
166167
// declared battery, so it reds by the same set difference rather than silently
@@ -193,9 +194,9 @@ const RATCHET_EXPANSION_OFFER = /admit it into\s+scripts\/error-status-unpinned-
193194
// ───────────────────────────────────────────────────────────────────────────
194195

195196
/**
196-
* Index every `const NAME = <literal>` and `const OBJ = { key: <literal> }` in
197-
* the scanned sources, so a declaration written as `readonly code = SOME_CODE`
198-
* still resolves.
197+
* Index every `const NAME = <literal>`, `const OBJ = { key: <literal> }` and
198+
* `const NAME = z.enum([<literal>, ...])` in the scanned sources, so a
199+
* declaration written as `readonly code = SOME_CODE` still resolves.
199200
*
200201
* A name bound to two different literals in two files is recorded as AMBIGUOUS
201202
* and refused at resolution time. Guessing there would let the gate assert a
@@ -230,11 +231,35 @@ export function buildConstantIndex(sources) {
230231
// (`external-errors.ts`), so not reading it left the deriver blind to a whole
231232
// error family and reporting three declarations it could have resolved.
232233
const COMPUTED_ENTRY = /\[\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?)\s*\]\s*:\s*(?:'([^'\n]*)'|"([^"\n]*)"|(\d{3}))/g;
234+
// `export const NAME = z.enum(['A', 'B'])` — a Zod enum's members are reached
235+
// as `NAME.enum.MEMBER`, and each member's VALUE equals its own name by
236+
// construction, so the array IS the value table. Indexed one entry per
237+
// literal: a member the array does not list stays absent from the index, and
238+
// the declaration reaching for it is REPORTED unresolved rather than
239+
// invented — the same refusal `ambiguous` draws, for the same reason.
240+
//
241+
// `StandardErrorCode` (`packages/spec/src/api/errors.zod.ts`) is the
242+
// declaration this exists for. Without it the whole `z.enum` family recorded
243+
// NOTHING, so 36 real producers across the drivers, objectql, formula and
244+
// service-analytics — every one of them writing a literal status beside
245+
// `code = StandardErrorCode.enum.SOME_CODE` — resolved to `undefined` and
246+
// spent every run in the `unresolved` census.
247+
const ZOD_ENUM = new RegExp(
248+
`^\\s*(?:export\\s+)?const\\s+([A-Za-z_$][\\w$]*)\\s*(?::[^=\\n]+)?=\\s*z\\.enum\\(\\s*\\[([^\\]]*)\\]`,
249+
'gm',
250+
);
251+
const ENUM_MEMBER = /'([^'\n]+)'|"([^"\n]+)"/g;
233252

234253
const clean = [...sources.values()].map((src) => maskComments(src));
235254
const objectBodies = [];
236255
for (const src of clean) {
237256
for (const m of src.matchAll(SCALAR)) put(m[1], m[2] ?? m[3] ?? Number(m[4]));
257+
for (const m of src.matchAll(ZOD_ENUM)) {
258+
for (const lit of m[2].matchAll(ENUM_MEMBER)) {
259+
const member = lit[1] ?? lit[2];
260+
put(`${m[1]}.enum.${member}`, member);
261+
}
262+
}
238263
for (const m of src.matchAll(OBJECT)) {
239264
objectBodies.push([m[1], m[2]]);
240265
for (const kv of m[2].matchAll(LITERAL_ENTRY)) {
@@ -270,7 +295,12 @@ export function resolveStatus(expr, index) {
270295
}
271296

272297
function lookup(e, index) {
273-
if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?$/.test(e)) {
298+
// `NAME`, `OBJ.key`, and ONE three-segment shape: `NAME.enum.MEMBER`, the way
299+
// a `z.enum([...])` member is addressed. It is spelled out rather than folded
300+
// into a general dotted-path walk on purpose — an arbitrary path would let
301+
// this resolve members of objects the index never read, which is the
302+
// guessing this file refuses. Anything else stays unresolved and is REPORTED.
303+
if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?$/.test(e) || /^[A-Za-z_$][\w$]*\.enum\.[A-Za-z_$][\w$]*$/.test(e)) {
274304
return index.ambiguous.has(e) ? undefined : index.values.get(e);
275305
}
276306
// MAP[OBJ.key] / MAP[NAME] — resolve the subscript, then the member.
@@ -1382,7 +1412,54 @@ function selfTest() {
13821412
!twoBlocks.emitted.has('TIMEOUT') && twoBlocks.unresolved.length === 0,
13831413
JSON.stringify([...twoBlocks.emitted.keys(), ...twoBlocks.unresolved]));
13841414

1385-
const CASES = 47;
1415+
// 25 — `z.enum([...])` members: the ONE extra segment `lookup` walks, and the
1416+
// refusal that keeps it from becoming a guess. `StandardErrorCode`
1417+
// (`packages/spec/src/api/errors.zod.ts`) is written this way and 36 real
1418+
// producers across the drivers, objectql, formula and service-analytics
1419+
// spell their code as `StandardErrorCode.enum.SOME_CODE` — a shape the
1420+
// index recorded nothing for, so every one of them sat in `unresolved`
1421+
// beside a status that was already a literal.
1422+
battery('25 — `z.enum([...])` members: the ONE extra segment `lookup` walks, and the');
1423+
const ZOD_ENUM_DECL = "export const StandardErrorCode = z.enum([\n 'TIMEOUT',\n 'VALIDATION_ERROR',\n]);";
1424+
const zodEnum = runFixture({
1425+
files: {
1426+
'a/codes.ts': ZOD_ENUM_DECL,
1427+
'a/e.ts':
1428+
'function refuse(message) {\n const err = new Error(message);\n'
1429+
+ ' err.code = StandardErrorCode.enum.TIMEOUT;\n err.status = 504;\n return err;\n}',
1430+
},
1431+
handling: '#### `TIMEOUT`\n**HTTP Status:** 504 \n', catalog: '', members: ['TIMEOUT'],
1432+
});
1433+
check('25 a z.enum member resolves to its own name',
1434+
zodEnum.reconciledPairs === 1 && zodEnum.unresolved.length === 0
1435+
&& zodEnum.emitted.get('TIMEOUT')?.has(504) === true,
1436+
JSON.stringify([zodEnum.reconciledPairs, zodEnum.unresolved]));
1437+
const notAMember = runFixture({
1438+
files: {
1439+
'a/codes.ts': ZOD_ENUM_DECL,
1440+
'a/e.ts':
1441+
'function refuse(message) {\n const err = new Error(message);\n'
1442+
+ ' err.code = StandardErrorCode.enum.NOT_A_MEMBER;\n err.status = 504;\n return err;\n}',
1443+
},
1444+
handling: '', catalog: '', members: ['TIMEOUT'],
1445+
});
1446+
check('25b a name the enum array does not list is REPORTED, never invented',
1447+
notAMember.unresolved.length === 1
1448+
&& notAMember.unresolved[0].includes('StandardErrorCode.enum.NOT_A_MEMBER')
1449+
&& notAMember.emitted.size === 0,
1450+
JSON.stringify([notAMember.unresolved, [...notAMember.emitted.keys()]]));
1451+
// The member is reachable through `.enum.` and NOTHING else: `lookup` walks
1452+
// that one shape, not dotted paths in general, so a two-segment reach at the
1453+
// same name stays undefined rather than resolving off a neighbouring entry.
1454+
const enumIndex = buildConstantIndex(new Map([['a/codes.ts', ZOD_ENUM_DECL]]));
1455+
check('25c the member is addressable through `.enum.` and nowhere else',
1456+
resolveString('StandardErrorCode.enum.TIMEOUT', enumIndex) === 'TIMEOUT'
1457+
&& resolveString('StandardErrorCode.TIMEOUT', enumIndex) === undefined
1458+
&& resolveString('StandardErrorCode.enum.NOT_A_MEMBER', enumIndex) === undefined,
1459+
JSON.stringify([resolveString('StandardErrorCode.enum.TIMEOUT', enumIndex),
1460+
resolveString('StandardErrorCode.TIMEOUT', enumIndex)]));
1461+
1462+
const CASES = 50;
13861463
// ── The floor: every declared battery RAN, and ran its cases (#13489) ───
13871464
//
13881465
// Evaluated after every battery has had its chance and BEFORE the verdict, so
@@ -1441,8 +1518,8 @@ function selfTest() {
14411518
+ 'narrated envelopes in comments are not producers, unresolvable declarations are reported, a LEDGER code '
14421519
+ 'the docs publish a status for is reconciled in both directions while one no page publishes stays out of '
14431520
+ 'the vocabulary, an entry heading in an unrecognised shape is reported instead of silently dropped, the '
1444-
+ 'baseline-expanding remedy stays maintainer-only, and a baselined code leaving the unpinned census is '
1445-
+ 'named a producer or a removed doc entry — never the wrong one of the two.',
1521+
+ 'baseline-expanding remedy stays maintainer-only, a baselined code leaving the unpinned census is '
1522+
+ 'named a producer or a removed doc entry — never the wrong one of the two — and a `z.enum([...])` member resolves through `NAME.enum.MEMBER` while a name that array does not list stays unresolved.',
14461523
);
14471524
selfTestReachedVerdict = true;
14481525
process.exit(0);

0 commit comments

Comments
 (0)