Skip to content

Commit f99dbcc

Browse files
claude[bot]claude
andauthored
fix(devx): check-error-status-conformance derives the nested { status, body } terminal and the err.code/err.status assignment form (#15656)
* fix(devx): check-error-status-conformance derives a { status, body } terminal with a nested body and the err.code/err.status assignment form (#15633) R5b matched the mapper's `{ status: N, body: { … } }` terminal with a body group that forbade nested braces, so every arm whose body carries a conditional spread was skipped — silently, which is the one outcome this gate's header rules out ("the runtime side is DERIVED, never listed … REPORTED as unresolved, never silently dropped"). The 409 `UNIQUE_VIOLATION` answer that `structuredCodeAnswer`'s `DuplicateRecordError` arm returns is exactly that shape, and two tests pin it as emitted while the deriver found no producer for it at all. The body span is now brace-BALANCED, walked on a projection with comments AND string/template/regex content blanked, so a brace that is text cannot open or close it. A second producer of the same code declares its envelope by ASSIGNMENT (`err.code = …; err.status = …`), a shape no rule read at all; R6 reads it, bounded to one identifier inside one block and a line window, and reports a pair it cannot resolve rather than dropping it. Self-test: 40 → 47 cases, two new batteries — the balanced span (the conditional-spread body derives, a body with no code fabricates nothing, a brace inside a string does not close the span early) and the assignment form (constants resolve, an unresolvable status is reported, and neither a different identifier nor a different block ever pairs). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk * fix(devx): ratchet the unpinned baseline down by the six rows the widened deriver pinned (#15633) The deriver change in the previous commit reads 135 further producer sites, and six codes the baseline still recorded as having NO derivable producer now have one: INVALID_FIELD, INVALID_FILTER, INVALID_QUERY, INVALID_SORT, OBJECT_NOT_FOUND and RECORD_NOT_ACCESSIBLE. Each row had become a false statement about the tree. Removed BY HAND, six rows and nothing else — deliberately not with `--update`, which regenerates the whole list from the current run and could admit a row nobody read. This is the SHRINK direction the gate prescribes in its own failure text ("ratchet the baseline down") and the file's own note requires ("a row that becomes pinned must be removed"); the ⛔ MAINTAINER-ONLY convention covers the baseline-EXPANDING remedy, which the script's self-test battery 11b pins as the asymmetry it is. Authorised by the PM seat `domain:devx @ objectstack` (#6023) on #15633. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent bc2debd commit f99dbcc

2 files changed

Lines changed: 230 additions & 14 deletions

File tree

scripts/check-error-status-conformance.mjs

Lines changed: 230 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
// `scripts/error-status-unpinned-baseline.json`; a NEW one fails the gate, and a
111111
// row that becomes pinned fails it too (ratchet down with `--update`).
112112
import { readdirSync, readFileSync, writeFileSync, statSync, existsSync } from 'node:fs';
113-
import { maskComments } from './js-comment-mask.mjs';
113+
import { maskComments, scanSource, blank } from './js-comment-mask.mjs';
114114
import { join, relative } from 'node:path';
115115
import { isEntrypoint } from './invoked-as.mjs';
116116

@@ -154,11 +154,13 @@ const SELF_TEST_BATTERIES = Object.freeze({
154154
'20 — the ungraded census: an entry the parser READ but for which no page': 3,
155155
'21 — nowPinned, the PRODUCER branch: a baselined code that GAINS a': 2,
156156
'22 — nowPinned, the DOC-REMOVED branch: the #9266/#9563 counterfactual,': 2,
157+
'23 — R5b: the body span is brace-BALANCED, and the braces it counts are': 3,
158+
'24 — R6, the ASSIGNMENT form, and the two bounds that keep it honest.': 4,
157159
});
158160

159161
// DELETING an entry silences that battery's floor exactly as effectively as
160162
// zeroing it, so the roster's own size is pinned too.
161-
const SELF_TEST_BATTERY_FLOOR = 23;
163+
const SELF_TEST_BATTERY_FLOOR = 25;
162164

163165
// The key an assertion is filed under when no battery is open. It is not a
164166
// declared battery, so it reds by the same set difference rather than silently
@@ -323,6 +325,106 @@ function classBodies(src) {
323325

324326
const lineOf = (src, idx) => src.slice(0, idx).split('\n').length;
325327

328+
/**
329+
* The two projections a rule may read, from ONE scan of the source.
330+
*
331+
* `src` comments blanked, string/template/regex CONTENT intact — what
332+
* every rule matches on, because a gate's signal (`code:
333+
* 'UNIQUE_VIOLATION'`) is itself a string literal.
334+
* `structural` comments AND literal content blanked — the ONLY projection a
335+
* BRACE WALK may read. A brace inside a string, a template or a
336+
* regex is not a delimiter, and counting one flips the parity of
337+
* every brace after it: the span then closes early (blinding) or
338+
* runs to end of file (fabricating). Both offsets survive the
339+
* mask, so a line number read off either is true of the original.
340+
*/
341+
function projections(raw) {
342+
const { comment, literal } = scanSource(raw);
343+
const both = new Uint8Array(raw.length);
344+
for (let k = 0; k < both.length; k++) both[k] = comment[k] || literal[k];
345+
return { src: blank(raw, comment), structural: blank(raw, both) };
346+
}
347+
348+
/**
349+
* The index of the `}` matching the `{` at `open`, walked on the STRUCTURAL
350+
* projection, or -1 when the source ends first.
351+
*/
352+
function matchingBrace(structural, open) {
353+
let depth = 0;
354+
for (let i = open; i < structural.length; i++) {
355+
if (structural[i] === '{') depth++;
356+
else if (structural[i] === '}' && --depth === 0) return i;
357+
}
358+
return -1;
359+
}
360+
361+
/**
362+
* True when `a` and `b` sit in the SAME block: no brace opened between them is
363+
* still open at `b`, and no brace closed between them was opened before `a`.
364+
* Read on the structural projection, so a brace in prose or in a string cannot
365+
* join two blocks or split one.
366+
*/
367+
function sameBlock(structural, a, b) {
368+
const [lo, hi] = a <= b ? [a, b] : [b, a];
369+
let depth = 0;
370+
for (let i = lo; i < hi; i++) {
371+
if (structural[i] === '{') depth++;
372+
else if (structural[i] === '}') { if (depth === 0) return false; depth--; }
373+
}
374+
return depth === 0;
375+
}
376+
377+
/**
378+
* How far apart the two halves of an ASSIGNMENT-form declaration may sit. The
379+
* bound is `sameBlock` first — a window alone would pair two unrelated
380+
* statements in two neighbouring functions — and this line count second, so a
381+
* long top-level block cannot marry a `code` assignment near its top to a
382+
* `status` assignment near its bottom. The real producer this rule was written
383+
* for writes them on consecutive lines.
384+
*/
385+
const ASSIGNMENT_PAIR_WINDOW_LINES = 20;
386+
387+
/**
388+
* `<ident>.code = <expr>;` paired with the same identifier's `<ident>.status =
389+
* <expr>;` (or `.statusCode`), nearest partner first, bounded by `sameBlock`
390+
* and `ASSIGNMENT_PAIR_WINDOW_LINES`.
391+
*
392+
* A pair is refused outright when another assignment to the SAME identifier's
393+
* `code` sits between the two halves — that identifier was restamped in
394+
* between, so which `code` the `status` belongs to is a guess, and this gate
395+
* reports rather than guesses.
396+
*
397+
* @returns {{ ident: string, code: string, status: string, line: number }[]}
398+
*/
399+
function assignmentPairs(src, structural) {
400+
const hits = [];
401+
for (const m of src.matchAll(/\b([A-Za-z_$][\w$]*)\.(code|status|statusCode)\s*=\s*([^;\n]+);/g)) {
402+
hits.push({
403+
ident: m[1],
404+
prop: m[2] === 'statusCode' ? 'status' : m[2],
405+
expr: m[3].trim(),
406+
at: m.index,
407+
line: lineOf(src, m.index),
408+
});
409+
}
410+
const out = [];
411+
for (const codeHit of hits) {
412+
if (codeHit.prop !== 'code') continue;
413+
let best;
414+
for (const other of hits) {
415+
if (other === codeHit || other.ident !== codeHit.ident) continue;
416+
if (other.prop !== 'status') continue;
417+
if (Math.abs(other.line - codeHit.line) > ASSIGNMENT_PAIR_WINDOW_LINES) continue;
418+
if (!sameBlock(structural, codeHit.at, other.at)) continue;
419+
const [lo, hi] = codeHit.at <= other.at ? [codeHit.at, other.at] : [other.at, codeHit.at];
420+
if (hits.some((h) => h.prop === 'code' && h.ident === codeHit.ident && h.at > lo && h.at < hi)) continue;
421+
if (!best || Math.abs(other.at - codeHit.at) < Math.abs(best.at - codeHit.at)) best = other;
422+
}
423+
if (best) out.push({ ident: codeHit.ident, code: codeHit.expr, status: best.expr, line: codeHit.line });
424+
}
425+
return out;
426+
}
427+
326428
/**
327429
* Every (code, status) pair the scanned sources prove reachable, with the
328430
* evidence that proves it.
@@ -346,7 +448,7 @@ export function deriveRuntimeStatuses(sources, index) {
346448
unresolved.push(`${where}: code=${String(code).trim()} status=${String(status).trim()}`);
347449

348450
for (const [path, raw] of sources) {
349-
const src = maskComments(raw);
451+
const { src, structural } = projections(raw);
350452
// R1 — error classes declaring their own code + status/statusCode.
351453
for (const { name, body } of classBodies(src)) {
352454
const codeM = /^[ \t]*(?:public\s+|protected\s+|private\s+)?readonly\s+code\s*(?::[^=\n]+)?=\s*([^;\n]+);/m.exec(body);
@@ -390,14 +492,41 @@ export function deriveRuntimeStatuses(sources, index) {
390492
if (!/^[A-Z][A-Z0-9_]*$/.test(code)) continue;
391493
record(code, status, `${path}:${lineOf(src, m.index)}: { code, status }`);
392494
}
393-
// R5b — `{ status: N, body: { …, code: 'X' } }`, the mapper's two sanitised
394-
// 5xx terminals, where the pair straddles a nested brace.
395-
for (const m of src.matchAll(/\bstatus\s*:\s*(\d{3})\s*,\s*body\s*:\s*\{([^{}]*)\}/g)) {
396-
const c = /\bcode\s*:\s*'([A-Z][A-Z0-9_]*)'/.exec(m[2]);
495+
// R5b — `{ status: N, body: { … } }`, the REST mapper's terminals, where the
496+
// pair straddles a nested brace. The body span is BRACE-BALANCED and walked
497+
// on `structural`.
498+
//
499+
// It was a brace-FREE character class, which read the two flat 5xx terminals
500+
// and skipped every arm whose body carries a conditional spread — the
501+
// `DuplicateRecordError` arm in `packages/rest/src/error-response.ts` builds
502+
// its 409 `UNIQUE_VIOLATION` body that way, so the code documented as 409 and
503+
// pinned by two tests derived NO producer at all. Skipped, not reported:
504+
// exactly the silent blindness this file's header rules out.
505+
for (const m of src.matchAll(/\bstatus\s*:\s*(\d{3})\s*,\s*body\s*:\s*\{/g)) {
506+
const open = m.index + m[0].length - 1;
507+
const close = matchingBrace(structural, open);
508+
if (close < 0) continue;
509+
const c = /\bcode\s*:\s*'([A-Z][A-Z0-9_]*)'/.exec(src.slice(open + 1, close));
397510
const status = resolveStatus(m[1], index);
398511
if (!c || status === undefined) continue;
399512
record(c[1], status, `${path}:${lineOf(src, m.index)}: { status, body }`);
400513
}
514+
// R6 — the ASSIGNMENT form: `err.code = …;` and `err.status = …;` stamped on
515+
// the same identifier. Every rule above reads a DECLARATION — a class
516+
// property, a call argument, an object literal — so a producer that builds
517+
// its envelope by assigning onto a plain `Error` matched nothing at all.
518+
// `driver-memory`'s `conflictRefusal` is exactly that shape, and its 409
519+
// was the second of this code's two invisible producers.
520+
for (const pair of assignmentPairs(src, structural)) {
521+
const where = `${path}:${pair.line}: assignment`;
522+
const code = resolveString(pair.code, index);
523+
const status = resolveStatus(pair.status, index);
524+
// A resolved code that is not SCREAMING_SNAKE is not an error code: some
525+
// other `.code`/`.status` pair, dropped the way R3/R4 drop theirs.
526+
if (code !== undefined && !/^[A-Z][A-Z0-9_]*$/.test(code)) continue;
527+
if (code === undefined || status === undefined) { refuse(where, pair.code, pair.status); continue; }
528+
record(code, status, where);
529+
}
401530
}
402531
return { emitted, unresolved, sites };
403532
}
@@ -1160,7 +1289,100 @@ function selfTest() {
11601289
nowPinnedDocRemovedMessage('TRANSACTION_FAILED').includes('doc entry was removed')
11611290
&& !nowPinnedDocRemovedMessage('TRANSACTION_FAILED').includes('a producer now declares'));
11621291

1163-
const CASES = 40;
1292+
// 23 — R5b: the body span is brace-BALANCED, and the braces it counts are
1293+
// the STRUCTURAL ones. The first case is the real shape this rule was
1294+
// blind to — `structuredCodeAnswer`'s `DuplicateRecordError` arm, whose
1295+
// 409 body carries conditional spreads, so a brace-free character class
1296+
// matched nothing and the code derived NO producer at all. The other two
1297+
// are the directions a widened span can go wrong in: fabricating a code
1298+
// out of a body that has none, and closing early on a brace that is text.
1299+
battery('23 — R5b: the body span is brace-BALANCED, and the braces it counts are');
1300+
const spread = runFixture({
1301+
files: {
1302+
'a/m.ts':
1303+
'function answer(error, field) {\n'
1304+
+ ' return {\n'
1305+
+ ' status: 409,\n'
1306+
+ ' body: {\n'
1307+
+ " error: 'A record with this value already exists',\n"
1308+
+ " code: 'UNIQUE_VIOLATION',\n"
1309+
+ " ...(typeof error?.message === 'string' ? { developerMessage: error.message } : {}),\n"
1310+
+ ' ...(field ? { field } : {}),\n'
1311+
+ ' },\n'
1312+
+ ' };\n'
1313+
+ '}\n',
1314+
},
1315+
handling: '', catalog: '', members: ['VALIDATION_ERROR'],
1316+
});
1317+
check('23 a conditional-spread body derives across its nested braces',
1318+
spread.emitted.get('UNIQUE_VIOLATION')?.has(409) === true, [...spread.emitted.keys()].join(','));
1319+
const noCode = runFixture({
1320+
files: { 'a/m.ts': "const answer = () => ({ status: 409, body: { error: 'conflict', details: { hint: 'retry' } } });" },
1321+
handling: '', catalog: '', members: ['VALIDATION_ERROR'],
1322+
});
1323+
check('23b a body with no code inside the span fabricates nothing',
1324+
noCode.emitted.size === 0, [...noCode.emitted.keys()].join(','));
1325+
const braceInText = runFixture({
1326+
files: { 'a/m.ts': "const answer = () => ({ status: 403, body: { error: 'unbalanced } brace', code: 'PERMISSION_DENIED' } });" },
1327+
handling: '', catalog: '', members: ['PERMISSION_DENIED'],
1328+
});
1329+
check('23c a brace inside a string does not close the span early',
1330+
braceInText.emitted.get('PERMISSION_DENIED')?.has(403) === true, [...braceInText.emitted.keys()].join(','));
1331+
1332+
// 24 — R6, the ASSIGNMENT form, and the two bounds that keep it honest.
1333+
// `driver-memory`'s `conflictRefusal` stamps its ADR-0112 envelope by
1334+
// assigning onto a plain `Error`; every rule before R6 reads a
1335+
// DECLARATION, so that producer matched nothing. The bounds are the
1336+
// whole risk of reading assignments: two halves that never belonged to
1337+
// each other pair into a status nobody declared.
1338+
battery('24 — R6, the ASSIGNMENT form, and the two bounds that keep it honest.');
1339+
const stamped = runFixture({
1340+
files: {
1341+
'a/c.ts': "export const REFUSAL_CODE = 'TIMEOUT';\nexport const REFUSAL_STATUS = 504;",
1342+
'a/e.ts':
1343+
'function refuse(message) {\n const err = new Error(message);\n'
1344+
+ ' err.code = REFUSAL_CODE;\n err.status = REFUSAL_STATUS;\n return err;\n}',
1345+
},
1346+
handling: '#### `TIMEOUT`\n**HTTP Status:** 504 \n', catalog: '', members: ['TIMEOUT'],
1347+
});
1348+
check('24 the assignment form derives through the constant index',
1349+
stamped.reconciledPairs === 1 && stamped.unresolved.length === 0, JSON.stringify(stamped.unresolved));
1350+
const opaqueStatus = runFixture({
1351+
files: {
1352+
'a/e.ts':
1353+
'function refuse(message) {\n const err = new Error(message);\n'
1354+
+ " err.code = 'TIMEOUT';\n err.status = statusFor(message);\n return err;\n}",
1355+
},
1356+
handling: '', catalog: '', members: ['TIMEOUT'],
1357+
});
1358+
check('24b an assignment pair whose status will not resolve is REPORTED, not dropped',
1359+
opaqueStatus.unresolved.length === 1 && opaqueStatus.unresolved[0].includes('assignment')
1360+
&& !opaqueStatus.emitted.has('TIMEOUT'),
1361+
JSON.stringify(opaqueStatus.unresolved));
1362+
const twoObjects = runFixture({
1363+
files: {
1364+
'a/e.ts':
1365+
"function pair() {\n const a = new Error('a');\n const b = new Error('b');\n"
1366+
+ " a.code = 'TIMEOUT';\n b.status = 504;\n return [a, b];\n}",
1367+
},
1368+
handling: '', catalog: '', members: ['TIMEOUT'],
1369+
});
1370+
check('24c a code and a status on DIFFERENT identifiers never pair',
1371+
!twoObjects.emitted.has('TIMEOUT') && twoObjects.unresolved.length === 0,
1372+
JSON.stringify([...twoObjects.emitted.keys(), ...twoObjects.unresolved]));
1373+
const twoBlocks = runFixture({
1374+
files: {
1375+
'a/e.ts':
1376+
"function one() {\n const err = new Error('x');\n err.code = 'TIMEOUT';\n return err;\n}\n"
1377+
+ "function two() {\n const err = new Error('y');\n err.status = 504;\n return err;\n}",
1378+
},
1379+
handling: '', catalog: '', members: ['TIMEOUT'],
1380+
});
1381+
check('24d the same identifier in two BLOCKS never pairs across the boundary',
1382+
!twoBlocks.emitted.has('TIMEOUT') && twoBlocks.unresolved.length === 0,
1383+
JSON.stringify([...twoBlocks.emitted.keys(), ...twoBlocks.unresolved]));
1384+
1385+
const CASES = 47;
11641386
// ── The floor: every declared battery RAN, and ran its cases (#13489) ───
11651387
//
11661388
// Evaluated after every battery has had its chance and BEFORE the verdict, so

scripts/error-status-unpinned-baseline.json

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,15 @@
1212
"INSUFFICIENT_PRIVILEGES",
1313
"INTEGRATION_ERROR",
1414
"INVALID_CREDENTIALS",
15-
"INVALID_FIELD",
16-
"INVALID_FILTER",
1715
"INVALID_FORMAT",
18-
"INVALID_QUERY",
1916
"INVALID_REFERENCE",
20-
"INVALID_SORT",
2117
"INVALID_TOKEN",
2218
"IP_RESTRICTED",
2319
"LICENSE_REQUIRED",
2420
"LOCK_CONFLICT",
2521
"MAX_RECORDS_EXCEEDED",
2622
"MFA_REQUIRED",
27-
"OBJECT_NOT_FOUND",
2823
"QUOTA_EXCEEDED",
29-
"RECORD_NOT_ACCESSIBLE",
3024
"SESSION_EXPIRED",
3125
"TIME_RESTRICTED",
3226
"VALUE_OUT_OF_RANGE",

0 commit comments

Comments
 (0)