Skip to content

fix(devx): check-error-status-conformance derives the nested { status, body } terminal and the err.code/err.status assignment form - #15656

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-15633-error-status-deriver
Sep 5, 2026
Merged

fix(devx): check-error-status-conformance derives the nested { status, body } terminal and the err.code/err.status assignment form#15656
baozhoutao merged 2 commits into
mainfrom
claude/issue-15633-error-status-deriver

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15633

Unblocks #15362 / PR #15634 — that docs diff is correct as written and needs no change; it was red only because this gate's deriver could not see either of the two producers of the code it documents.

What was blind, and why

check:error-status-conformance derives the runtime side rather than listing it. Its header states the invariant: "The runtime side is DERIVED, never listed … Where an identifier cannot be resolved the declaration is REPORTED as unresolved, never silently dropped — a deriver that goes quietly blind is the same failure one layer down." Two producers of UNIQUE_VIOLATION were neither derived nor reported.

R5b matched the REST mapper's terminal with a body group written as a negated character class that excluded both brace characters, so it read only bodies with no nesting at all. The real arm — structuredCodeAnswer's DuplicateRecordError branch at packages/rest/src/error-response.ts:996 — builds its 409 body with conditional spreads, so nested braces sit between status: 409 and code: 'UNIQUE_VIOLATION'. Skipped, silently.

The assignment form had no rule at all. Every rule read a declaration — a class property, a call argument, an object literal — so packages/drivers/driver-memory/src/memory-unique-constraint.ts:504-505, which stamps its ADR-0112 envelope with err.code = UNIQUE_VIOLATION_CODE; err.status = UNIQUE_VIOLATION_STATUS;, matched nothing.

The answer itself is pinned by tests on both doors — packages/rest/src/rest-duplicate-record-arm.test.ts §1 asserts status 409 and body.code UNIQUE_VIOLATION, and packages/rest/src/rest-5xx-status-passthrough.test.ts:379 asserts the same through another door. The code IS emitted; only the deriver was blind.

The change

cd26dd16bscripts/check-error-status-conformance.mjs:

  • R5b's body span is brace-BALANCED. The walk reads a structural projection of the source — comments and string / template / regex content blanked, offsets preserved — built from one scanSource() call in scripts/js-comment-mask.mjs. A brace that is text can therefore neither close the span early (blinding) nor let it run past its terminal (fabricating).
  • R6, new: the assignment form. IDENT.code = expr paired with the same identifier's IDENT.status / IDENT.statusCode, both resolved through the existing constant index. Bounded twice: sameBlock() on the structural projection (no brace opened between them is still open at the partner, none closed between them was opened before the first), and a 20-line window on top, so a long top-level block cannot marry a code near its top to a status near its bottom. A pair that resolves on neither half is reported as unresolved, per the header's rule, rather than dropped. Site recorded as path:line: assignment.

87ad25178scripts/error-status-unpinned-baseline.json: six rows removed by hand, six deletions and nothing else.

The six baseline rows, and why they had to go

The widened deriver reads 135 further true producer sites (144 → 279). Six codes the baseline still recorded as having no derivable producer now have one, so each row had become a false statement about the tree:

removed row a producer it now derives
INVALID_FIELD packages/rest/src/error-response.ts:1146, :1652 · packages/metadata-protocol/src/protocol.ts:8779
INVALID_FILTER packages/rest/src/query-multiplicity.ts:250 · packages/spec/src/data/filter-comparand-shape.ts:254
INVALID_QUERY packages/metadata-protocol/src/protocol.ts:3321 · packages/spec/src/ui/view-grouping-query.ts:322
INVALID_SORT packages/objectql/src/engine.ts:997 · packages/metadata-protocol/src/protocol.ts:3246
OBJECT_NOT_FOUND packages/rest/src/error-response.ts:1123, :1772
RECORD_NOT_ACCESSIBLE packages/rest/src/error-response.ts:1086 · packages/plugins/plugin-audit/src/comment-access-hooks.ts:186

This is the shrink direction. The gate's own failure text prescribes it ("ratchet the baseline down"), the file's own note requires it ("a row that becomes pinned must be removed"), and the script's self-test battery 11b pins the asymmetry deliberately: neither ratchet-DOWN message carries the ⛔ MAINTAINER-ONLY marker, which only the baseline-EXPANDING remedy does. ⛔ --update was not run — it regenerates the whole list from the current run and could admit a row nobody read. Authorised on #15633 by the PM seat domain:devx @ objectstack (#6023) after the measurement below was reported.

Derived site lists, before and after

R5b before — the only two terminals it could read:

packages/rest/src/error-response.ts:126   DATABASE_ERROR  @500
packages/rest/src/error-response.ts:179   INTERNAL_ERROR  @500

R5b after — those two unchanged, at the same statuses, plus 46 more. The ones that matter here:

packages/rest/src/error-response.ts:996    UNIQUE_VIOLATION            @409   ← the card's subject
packages/rest/src/error-response.ts:1539   UNIQUE_VIOLATION            @409
packages/rest/src/error-response.ts:893    DELETE_RESTRICTED           @409
packages/rest/src/error-response.ts:922    CONCURRENT_UPDATE           @409
packages/rest/src/error-response.ts:1020   ERR_DATASOURCE_UNAVAILABLE  @503
packages/rest/src/error-response.ts:1086   RECORD_NOT_ACCESSIBLE       @403
packages/rest/src/error-response.ts:1123   OBJECT_NOT_FOUND            @404
packages/rest/src/error-response.ts:1146   INVALID_FIELD               @400
packages/rest/src/rest-server.ts:355       OBJECT_API_DISABLED         @404
packages/rest/src/rest-server.ts:364       OBJECT_API_METHOD_NOT_ALLOWED @405

R6 (all new):

packages/drivers/driver-memory/src/memory-unique-constraint.ts:504  UNIQUE_VIOLATION       @409   ← the card's second producer
packages/metadata-protocol/src/protocol.ts:894                      INVALID_FILTER         @400
packages/objectql/src/engine.ts:997                                 INVALID_SORT           @400
packages/plugins/plugin-audit/src/comment-access-hooks.ts:186       RECORD_NOT_ACCESSIBLE  @403
packages/rest/src/query-multiplicity.ts:250                         INVALID_FILTER         @400
packages/spec/src/data/filter-comparand-shape.ts:254                INVALID_FILTER         @400

Every site the old deriver had is still derived at the same status — the diff of the full (code, status, site) dump is additions only, which is what makes the six-row shrink a measurement rather than a side effect. Direction A stayed clean throughout: zero emittedNotDocumented and zero documentedNotReachable findings, so every newly derived status was already documented.

Verification

Exit codes captured before any pipe; every verdict below is the gate's own printed line. Union re-run on the final head 87ad25178.

command exit verdict line
pnpm check:error-status-conformance 0 ✓ check-error-status-conformance --self-test: 47 cases pass (was 40) and ✓ every derivable runtime status is documented, and every documented status is reachable.
pnpm check:ratchet-remedy-authority 0 OK check-ratchet-remedy-authority: 205 scripts swept …; 14 mark the expanding remedy ⛔ MAINTAINER-ONLY — the marker in this file is intact and untouched
pnpm check:nul-bytes 0 check-nul-bytes: OK (scanned 7571 text file(s) … no raw ASCII control bytes).
pnpm check:pm-dispatch-gates 0 ✓ dispatch-gates self-test: 1445 cases pass.
node scripts/check-comment-mask-corpus.mjs 0 the corpus gate over the very scanner R5b's brace walk now reads
pnpm check:entry-guard, check:parse-guard, check:cross-package-test-inputs, check:driver-memory-census, + the rest of the derived family 0 29 commands from node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack (change set: the two paths above, three-dot vs merge base d30ccb9bd)

Scope line, main vs this branch:

main   d30ccb9bd   144 producer site(s) derived | reconciled: 19 code(s), 20 pair(s)
                   unpinned: 32 documented code(s) with no derivable producer (baselined: 32)

branch 87ad25178   279 producer site(s) derived | reconciled: 25 code(s), 26 pair(s)
                   unpinned: 26 documented code(s) with no derivable producer (baselined: 26)

NOT MEASURED, by the derivation tool's own account: the three families whose argv carries a CI-only variable (check-cross-package-test-inputs --union-into, check-shard-attestation, check-test-completeness) and the 36 artifact-roster families it scores silent for every card. Whole-repo pnpm lint was not named by the derivation and was not run — narrowing declared; CI runs the full farm regardless.

The three legs that prove this unblocks #15634

Measured in throwaway detached worktrees, removed afterwards. The docs change was never committed here.

(a) this branch aloneEXIT=0:

unpinned: 26 documented code(s) with no derivable producer (baselined: 26).
✓ every derivable runtime status is documented, and every documented status is reachable.

(b) this branch + PR #15634's docs diff (git diff origin/main...origin/claude/issue-15362-error-handling-unique-violation | git apply, 1 file, 14 insertions / 14 deletions), re-run on the final head 87ad25178EXIT=0:

scope: 52 code(s) reconciled = 50 StandardErrorCode member(s) + 2 ledger code(s) a doc page
  publishes a status for (INVALID_REQUEST, UNIQUE_VIOLATION); 2243 source files scanned;
  279 producer site(s) derived
reconciled: 26 code(s) with a derived producer, 27 (code, status) pair(s) matched against the docs.
unpinned: 26 documented code(s) with no derivable producer (baselined: 26).
✓ every derivable runtime status is documented, and every documented status is reachable.

(c) control leg — the docs diff with the deriver change but WITHOUT the baseline shrinkEXIT=1, six findings and nothing else, all one shape:

✗ INVALID_FIELD: baselined as unpinned, but a producer now declares its status — ratchet the baseline down with --update.
… INVALID_FILTER, INVALID_QUERY, INVALID_SORT, OBJECT_NOT_FOUND, RECORD_NOT_ACCESSIBLE

Leg (c) is what makes the shrink accountable: it is the only thing standing between the deriver change and a green #15634, and it produces no other finding.

Ablations

Fix committed first, so every restore leg points at a commit that really holds it. Each mutation is trap-guarded (EXIT INT TERM), uses absolute paths resolved from git rev-parse --show-toplevel, restores with git checkout HEAD -- PATH (never the bare form, which reads from a polluted index), and is proved on disk by an anchor-count check plus a blob-hash comparison before the run. HEAD blob of the gate script: 759d420098b76d4bd9229465ef797e42f4b84476.

1 — revert R5b's balanced span to the brace-free character class. Mutation confirmed on disk: the deleted anchor const close = matchingBrace counts 0, the injected marker counts 1, blob 4188246bcb10d2884305aa731f5329726259d8a8. Result — self-test EXIT=1, 2/47 case(s) failed:

x self-test: 23 a conditional-spread body derives across its nested braces
x self-test: 23c a brace inside a string does not close the span early

2 — remove R6 (its call site replaced by an empty iterable). Mutation confirmed on disk: the injected marker counts 1, and the one line still matching assignmentPairs(src, structural) is the function definition; blob 465cc194884157b1b09fed4418cdd97c52233883. Result — self-test EXIT=1, 2/47 case(s) failed:

x self-test: 24 the assignment form derives through the constant index
x self-test: 24b an assignment pair whose status will not resolve is REPORTED, not dropped

Both restore legs verified the way the mutation legs were: blob back to 759d4200… and git diff HEAD --name-only empty.

Self-test

40 → 47 cases, two new batteries, roster floor 23 → 25. No existing case weakened.

  • 23 — R5b: the body span is brace-BALANCED, and the braces it counts are (3) — a conditional-spread body derives across its nested braces; a body with no code inside the span fabricates nothing; a brace inside a string does not close the span early.
  • 24 — R6, the ASSIGNMENT form, and the two bounds that keep it honest. (4) — constants resolve through the index; an unresolvable status is REPORTED, not dropped; a code and a status on different identifiers never pair; the same identifier in two blocks never pairs across the boundary.

Scope notes


🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

…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
…ened 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
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

Reviewed against the three-dot diff at 87ad25178 (2 files, +230/−14), not the report.

What the diff does, checked line by line

  • scripts/check-error-status-conformance.mjs: one scanSource call now yields two projections — src (comments blanked, literal content intact, what the rules match on) and structural (comments AND literal content blanked, the only projection a brace walk reads). R5b's body span is brace-balanced via matchingBrace(structural, open) instead of the brace-free [^{}]* class, so the DuplicateRecordError arm's conditional-spread 409 body derives. New R6 assignmentPairs(src, structural) pairs <ident>.code = …; with the same identifier's .status/.statusCode assignment, nearest partner first, bounded by sameBlock and a 20-line window, refused when the identifier's code is restamped between the halves, reported (not dropped) when it will not resolve, and dropped as not-an-error-code when the resolved code is not SCREAMING_SNAKE. Self-test 40→47 (batteries 23: three R5b cases incl. brace-in-string; 24: four R6 cases incl. different identifiers and different blocks never pairing); roster floor 23→25.
  • scripts/error-status-unpinned-baseline.json: exactly six deletions, zero insertions — INVALID_FIELD, INVALID_FILTER, INVALID_QUERY, INVALID_SORT, OBJECT_NOT_FOUND, RECORD_NOT_ACCESSIBLE — the shrink authorised by this seat's option-A ruling on check:error-status-conformance cannot derive the UNIQUE_VIOLATION producer — R5b's body group forbids nested braces, so documenting the code's real 409 fails the gate #15633 after the measurement was reported; --update not run.

Measured by this seat on the PR head (throwaway worktree, PM checkout's node_modules; plain node, no dist involved):

node scripts/check-error-status-conformance.mjs --self-test   EXIT=0   ✓ 47 cases pass
node scripts/check-error-status-conformance.mjs               EXIT=0
  scope: 51 code(s) reconciled … 2243 source files scanned; 279 producer site(s) derived
  reconciled: 25 code(s) with a derived producer, 26 (code, status) pair(s) matched against the docs.
  unpinned: 26 documented code(s) with no derivable producer (baselined: 26).
  ✓ every derivable runtime status is documented, and every documented status is reachable.
git diff origin/main..HEAD --stat -- scripts/error-status-unpinned-baseline.json   → 6 deletions(-)

Main for comparison derives 144 sites / baselined 32. The 50 unresolved declarations the deriver now reports rather than drops are #15644's card (filed by the dev).

Fences held: the two files only; no overlap with any other open PR; git merge-tree origin/main clean; skip-changeset (nothing published).

Flipping ready + enabling auto-merge. Fixes #15633 closes on merge; on merge, #15634 (#15362) gets origin/main merged in and re-runs CI per that card's ruling.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants