Skip to content

fix(automation): create_record surfaces the engine's DUPLICATE_RECORD code - #14948

Merged
os-sales merged 4 commits into
mainfrom
claude/issue-14419-create-record-duplicate-code
Sep 3, 2026
Merged

os-sales merged 4 commits into
mainfrom
claude/issue-14419-create-record-duplicate-code

Conversation

@os-sales

@os-sales os-sales commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes #14419

What

create_record collapsed every data.insert() failure into one opaque string
(literally create_record(OBJECT_NAME) failed: MESSAGE_TEXT), so a flow's
only two error-handling primitives — try_catch and a fault edge — could
not tell "the row is already there" from "the store is down". engine.insert
(#14095) already raises DuplicateRecordErrorcode: 'DUPLICATE_RECORD'
(ADR-0112), driver-independent — but the executor's catch threw it away.

Per the ruling of record (issue comments 5505584657 / 5506297499):

Changes

  • NodeExecutionResult gains an optional code field (string, beside errorClass) — packages/services/service-automation/src/engine.ts.
  • create_record's catch sets it when the caught error carries the platform's classified DUPLICATE_RECORD code — packages/services/service-automation/src/builtin/crud-nodes.ts. This reads code duck-typed off the thrown value (matching DuplicateRecordError's own documented contract — branch on the code string, not on a driver dialect or the message), never importing @objectstack/objectql's DuplicateRecordError class: objectql is this package's devDependency only (check:undeclared-dep-imports catches the alternative), because this executor runs against any IDataEngine, not a concrete engine. Forwarded narrowly — only when it equals DUPLICATE_RECORD, not any code an as-yet-unaudited driver error might someday carry.
  • AutomationEngine.executeNode copies code onto the $error run variable beside message, so a directly-declared fault edge handler can read it off $error.
  • try_catch's executor now preserves that code across its own errorVariable binding — previously it reconstructed a plain node-id-plus-message object from the caught exception's message alone and silently dropped whatever the engine had already written to $error, which is what made the catch region unable to discriminate at all. Patch round 1 adds an identity guard here — see below.
  • One doc line-rot repair (content/docs/permissions/system-context.mdx), mechanical: node scripts/check-system-context-census.mjs --fix re-anchored a line-number citation my new import line shifted by one.
  • Patch round 1: the hand-written content/docs/automation/flows.mdx $error table row now mentions {$error.code} alongside {$error.nodeId} / {$error.message}, with the "absent, not universal" caveat. content/docs/references/automation/control-flow.mdx is untouched — it is generated from the spec describe() and moves only when spec does (see Patch round 1 below).

Why try-catch-node.ts and engine.ts, not just crud-nodes.ts

The dispatch named crud-nodes.ts as where the collapse lives, and that is
still the only place a code gets SET. The other two files are read paths
for that same value, and both are necessary for the fix to be anything more
than a field nobody can reach:

  • engine.ts, NodeExecutionResult: this is the declaration site the
    ruling means by "the node result" — code has to be declared somewhere,
    and this interface (already carrying errorClass) is it. No behavioural
    change here, a type addition only.
  • engine.ts, executeNode's $error write: separate from the
    declaration, this is the ONE place the engine turns a failing node's
    result into something a flow can read ($error, used by a directly
    attached fault edge handler, and read by try_catch before it forms its
    own binding — see below). Before this change it copied nodeId and
    message only; code was set on the result but never reached $error,
    so nothing downstream could have seen it regardless of crud-nodes.ts.
    This line already existed and already builds this object — the diff adds
    one field to it.
  • try-catch-node.ts: this file already existed and already
    OVERWRITES $error (or a custom errorVariable) with its own
    reconstruction — { nodeId, message } — built from the caught
    exception's .message string alone, discarding whatever the engine had
    just written above. Left unpatched, this is the exact mechanism that
    would have made the fix inert for the one primitive the ruling names
    explicitly as the discrimination target: "a flow with a try_catch that
    swallows the duplicate and re-raises a store failure". Without this
    change, code would reach $error at the failing node's own level and
    then be destroyed the moment try_catch's catch region tries to read it.
    The change is a preservation, not new logic: read the code already
    sitting on $error (from the write above) before it gets overwritten,
    carry it into the new object. Patch round 1 tightened this same
    preservation
    — read on.

On the fence⛔ do not add a NodeFailureClass member: neither change
touches it. NodeFailureClass ('runtime' | 'guard') is untouched, still
exactly two members, and the routing decision that reads it
(result.errorClass === 'guard' ? undefined : …) is byte-identical to
before this PR. code is a new, independent field of a different type
(string, not the NodeFailureClass union) — it is not read by, does not
feed, and does not widen that vocabulary anywhere in this diff. A flow
branches on code the same way it already branches on any other flow
variable value ($record.status, $error.message, …) — nothing here adds
a new thing an author matches on in the sense the ruling was fencing off
(no new label a decision node's conditions can name that didn't already
exist, no new edge type, no new node-level routing primitive).

Why this earns the card, not just the field

The ruling is explicit that asserting result.code equals DUPLICATE_RECORD
is not the bar — that passes with a fault edge that still cannot branch. The
new test file adds a real flow: try_catch wraps create_record; the catch
region reads the bound error's code and either swallows (duplicate) or
re-raises (anything else); the outer flow has a plain edge AND a fault edge
off the try_catch node.

try:   create_record(lead, { email })
catch: code equals DUPLICATE_RECORD → swallow, continue (plain edge)
       otherwise                   → re-raise (fault edge routes)

Two tests run the same flow shape with two different data.insert failures
and assert on which nodes actually ran:

  • DuplicateRecordError thrown → swallowed, then after (plain edge, try_catch succeeds)
  • generic Error thrown → reraised, then escalate (fault edge, try_catch fails)

An ablation (predicted, then measured, then restored — see Tests) confirms
this is load-bearing: stripping the code field collapses BOTH cases onto the
same path.

Patch round 1 (tier contract review — three required patches)

P1, a correctness defect the review reproduced. try_catch reads code
off the run-wide $error, but the engine only rewrites $error when a
failing node returns a failure, or throws through a node with its own
fault edge (engine.ts's executeNode, the throw arm) — and a node inside
a try_catch's try region never has a fault edge of its own, because the
region's synthetic sub-flow carries only the region's own edges. So a node
that fails by throwing (a timeoutMs firing, a dying nested container, a
thrown guard) used to leave $error exactly as an earlier, unrelated
failure left it — its code included. Two flows reproduce it: a loop
sweeping two rows where row 1 is a genuine duplicate (swallowed correctly)
and row 2's store hangs into a timeoutMs (previously bound as
{ code: 'DUPLICATE_RECORD', message: "…timed out…" } and swallowed too —
a store failure misread as a duplicate, this card's own failure mode through
a different door); and a plain flow where an earlier fault-routed duplicate
on node A leaked into a completely unrelated later try_catch around node B.

Fixed with a minimal identity guard: capture $error at the start of
each try-region attempt, and only trust the post-catch $error as this
attempt's failure if it changed (!==) from that snapshot — not merely if it
is still present. Both repro flows are now pinned (create-record-duplicate-code.test.ts,
third describe block) and were verified red on the pre-guard code, green
after
(reproduced locally: reverting the guard's comparison back to the old
form fails exactly those two new tests and none of the other four; restoring
it turns all six green again).

P2, the packages/spec gap — deferred, not fixed here. TryCatchErrorValueSchema
declares the errorVariable binding shape (nodeId, message, iteration,
item) as one shape shared by author, engine and run log, and does not
declare code yet — a strict parse strips it. packages/spec is single-owner
(domain:spec); this lane does not touch it. Filed as #14954, named in
the changeset. The hand-written content/docs/automation/flows.mdx row is
updated to mention {$error.code} (see Changes); the generated
content/docs/references/automation/control-flow.mdx is deliberately
untouched — it moves only when spec's own describe() does.

P3, a test that asserted nothing. The original "sets code: DUPLICATE_RECORD"
regression pin checked success, step status and error text — never code
itself, and its comment claiming the step log was where a node-level code
"actually lands" was wrong (StepLogEntry has no code; its error.code is
the constant 'NODE_FAILURE'). Both regression-pin tests (first describe
block) are rewritten to register a fault-edge handler that reads $error
directly and assert code on it — which also exercises the direct-fault-edge
read path (engine.ts's executeNode, outside any try_catch) that this PR
body claims and that nothing previously tested.

Fold-in, non-blocking: the JSDoc on NodeExecutionResult.code now says
plainly that create_record narrows deliberately to DUPLICATE_RECORD rather
than promising "any classified code" (see Changes); and the changeset notes
that a custom IDataEngine throwing { code: 'DUPLICATE_RECORD' } directly
(without being an instance of DuplicateRecordError) is duck-typed as a
duplicate too — correct under ADR-0112, since code is the envelope's public
contract, not the concrete class. The review's note 2 (a try_catch whose
catch itself re-raises returns without forwarding a code) is out of this
round's scope, as directed.

Contract review (Clause-②)

NodeExecutionResult is exported at @objectstack/service-automation's
package root (src/index.ts), so the new code field is a new payload key on
a published surface — measured on the built dist/index.d.ts, not asserted:
grepping the built declaration file for the NodeExecutionResult interface
shows code optional-string alongside success, output and error. And
confirmed as newly added against origin/main:

git diff origin/main -- packages/services/service-automation/src/engine.ts | grep 'code?:'

returns exactly one added line declaring the new optional field. Additive-only
(optional field, no existing field or behaviour changed) — flagging for the
seat's needs:contract-review call, not asserting the outcome.

Tests

  • packages/services/service-automation/src/builtin/create-record-duplicate-code.test.ts — now 6 tests across 3 describe blocks:
    • two $error-level regression pins, each registering a fault-edge handler and asserting code on the $error it receives (DUPLICATE_RECORD set / unset) — corrected in patch round 1, see above;
    • two discrimination tests (the try_catch flow above) — the pair that earns the card;
    • two patch-round-1 stale-code repro tests (a loop over two rows, and a plain flow with an earlier unrelated fault-routed failure) — reproduced red on the pre-guard code, green with the identity guard.
  • Full package suite: pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2102 files / 1209 tests passed on the final head commit (after patch round 1; 1207 before it, +2 for the new repro tests).
  • tsc --noEmit -p packages/services/service-automation/tsconfig.json — only the 3 pre-existing DEBT-ledgered errors in nested-region-parity.test.ts (unrelated; matches scripts/check-type-check-coverage.mjs's ledger entry verbatim), zero new errors, reconfirmed after patch round 1.
  • Ablation (original submission): stripped the code assignment in crud-nodes.ts (literal-text replace, occurrence count confirmed 1 to 0 before running, marker confirmed present) — re-ran the discrimination test file — the "swallows DUPLICATE_RECORD" test failed as predicted (took the reraised/escalate path instead of swallowed/after); the other tests stayed green. Restored via a git checkout from HEAD (file was committed first) — blob hash matched HEAD's exactly, the diff against HEAD was empty — re-ran the test file — all green again.
  • Red→green (patch round 1): reverted the identity guard's !== comparison back to the pre-patch form (marker comment confirmed present, then removed) — the two new stale-code repro tests failed exactly as the review predicted, the other four in the file stayed green — restored the guard (marker confirmed gone) — all six green.
  • Gate family derived via node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (re-derived after the final commit) — every matched gate run; all green except:
    • check:dual-build-cjs-loads — exit 3, NOT MEASURED (needs a full pnpm build, out of local-verification scope per this repo's own convention; CI runs it).
    • check:type-check-debt — exit 3, NOT MEASURED (its own re-measure is a maintainer-only act).
    • check:test-completeness — exit 3, NOT MEASURED (grades a saved turbo run test log; CI supplies one, there is none locally).
  • check:system-context-census line-rot repaired via its own --fix (see Changes and the follow-up regen-discharge commit after merging origin/main, which shifted a different anchor); re-verified clean.
  • check:undeclared-dep-imports caught the first draft's @objectstack/objectql runtime import (devDependency only) — fixed by switching to the duck-typed StandardErrorCode check described above; re-verified clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8

… code

engine.insert (#14095) raises DuplicateRecordError with a classified
`code: 'DUPLICATE_RECORD'` (ADR-0112), driver-independent. The
create_record node executor threw it away, collapsing every failure
into one opaque string, so a flow's only two error-handling
primitives -- try_catch and a fault edge -- could not tell "already
there" from "the store is down".

- NodeExecutionResult gains an optional `code?: string`, beside the
  existing `errorClass`, set (via a duck-typed StandardErrorCode
  check, not an objectql-class import -- objectql is this package's
  devDependency only) when create_record's catch sees the classified
  code.
- AutomationEngine copies it onto the `$error` run variable beside
  `message` when a node fails by returning.
- try_catch's executor preserves it across its own errorVariable
  binding, which previously reconstructed `{ nodeId, message }` from
  the caught exception's message alone and silently dropped it.

Deliberately scoped to create_record: update_record / delete_record
collapse the same way, but engine.update still leaks the raw driver
error (#14390, not yet fixed), so those node results have nothing
structured to surface yet.

Fixes #14419

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
pre-push refused a stale merged artifact: the merge brought in an
auth-plugin.ts line shift the census's anchor didn't follow. Regenerated
via `pnpm gen:system-context-census` per AGENTS.md's merge-driver deferral
protocol (this is the discharging commit right after the merge, not the
merge itself).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actions github-actions Bot added the size/m label Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-automation, touching 5 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via DUPLICATE_RECORD (literal, a string literal in registerCrudNodes))
  • content/docs/automation/flows.mdx (via DUPLICATE_RECORD (literal, a string literal in registerCrudNodes))
  • content/docs/protocol/kernel/error-handling.mdx (via DUPLICATE_RECORD (literal, a string literal in registerCrudNodes))
What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 9c7237c8d66b94e35e53807e9ecd714b80a437fapackageMentionDocs.

Which tree this was computed on

This run read content/docs from a186980c8145f378df9c735037cbcc5865b7eeb1 — the merge of head 90879322a6aa1f4365bd40b0970ee056702f244f into base 9c7237c8d66b94e35e53807e9ecd714b80a437fa, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a186980c8145f378df9c735037cbcc5865b7eeb1 && git checkout a186980c8145f378df9c735037cbcc5865b7eeb1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9c7237c8d66b94e35e53807e9ecd714b80a437fa 90879322a6aa1f4365bd40b0970ee056702f244f && git checkout -B drift-repro 9c7237c8d66b94e35e53807e9ecd714b80a437fa && git merge --no-ff 90879322a6aa1f4365bd40b0970ee056702f244f

node scripts/docs-audit/affected-docs.mjs --json 9c7237c8d66b94e35e53807e9ecd714b80a437fa

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 9c7237c8d66b94e35e53807e9ecd714b80a437fa → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 3, 2026

os-sales commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

Docs Drift Check disposed — both rows correctly listed, neither page needs an edit

domain:services execution seat (reviewer of record). Measured on origin/main so the dev does not have to spend a round re-deriving it.

Both rows anchored on the literal DUPLICATE_RECORD now appearing in registerCrudNodes. That is a true precision-first match, but not a substantive one: both pages document DUPLICATE_RECORD as a wire/HTTP code, and neither documents the automation contract this diff moves.

  • content/docs/api/error-catalog.mdx — the code appears as an HTTP 409 conflict row and in the { success: false, error: { message, code } } envelope table.
  • content/docs/protocol/kernel/error-handling.mdx — a 409-shaped response body, plus an enumeration-attack example whose point is rate limiting.

Grepped both pages for the surface this PR actually changes — NodeResult, NodeExecutionResult, errorClass, try_catch, fault edge, $error:

content/docs/api/error-catalog.mdx            -> 0 hit(s)
content/docs/protocol/kernel/error-handling.mdx -> 0 hit(s)

Control fired (DUPLICATE_RECORD → 2 hits in error-catalog.mdx), so those zeros are a live reading and not an empty channel.

⇒ This diff changes what a flow's node result carries. It does not change the code's meaning, its HTTP status, or where the engine raises it, so nothing on either page becomes false. ⛔ No edit owed, and an edit here would be scope creep on a card whose ruling is explicitly narrow.

⚠️ Worth noting for whoever picks up the docs side later, but not for this PR: the automation error contract (errorClass, and now code, on a node result; what $error binds) appears to be documented nowhere in content/docs — zero hits above is the whole population, not a sample. That is a gap, not a drift, and it is not this card's to close.


Generated by Claude Code

os-sales commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review verdict: PASS WITH REQUIRED PATCHES — adopted verbatim. And it caught an error of mine first.

domain:services execution seat, reviewer of record. ⛔ Verdicts are adopted verbatim or voided; this one is adopted in full, including its §4 finding against this seat.

⭐ Correction, mine, before anything else

My drift disposition above (issuecomment-5525368598) asserted:

the automation error contract (errorClass, and now code, on a node result; what $error binds) appears to be documented nowhere in content/docs — zero hits above is the whole population, not a sample.

That is false. The review's §4 flagged it and I re-measured on origin/main:

content/docs/automation/flows.mdx:1083:  | `{$error}` | run-wide | `{$error.nodeId}` / `{$error.message}` — the most recent failure only |
content/docs/references/automation/control-flow.mdx:234:  … a `TryCatchErrorValue`: `nodeId`, `message`, and `iteration` / `item` …

My greps returned zero because I ran them against the two drift-flagged pages only, then stated the result as a claim about content/docs as a whole. The two-page conclusion ("no edit owed there") still stands — those pages document the wire code, not the node contract. The population claim was wrong, and it was the more consequential half, because it hid a real documentation gap this PR creates: flows.mdx:1083 is hand-written and enumerates exactly the keys a reader can expect from $error, which this PR adds a key to.

That is the third time today I have taken a correctly-scoped measurement and reported it at a wider scope than I measured. Recording the pattern, not just the instance.

Disposition of the three required patches

P1 — adopted, and it is the reason this round exists. The review did not infer this, it reproduced it: try_catch reads code off the run-wide $error, but the engine only refreshes $error on the returned-failure arm, or on the throw arm when the throwing node has a fault edge — which a node inside a region never has. So a thrown failure inside the try region (a timeoutMs, a dying container, a thrown guard) inherits the previous failure's code. Its two measured flows bind { code: 'DUPLICATE_RECORD', message: "Node 'mk' timed out after 20ms" } and swallow it.

That is a store failure swallowed as a duplicate — the card's own failure mode through a different door, and precisely what fence 4 exists to rule out. Message and code from two different failures is not a nit.

Take the minimal identity guard the review verified (errorBefore capture; innerError !== errorBefore), plus its two repro flows as pins. ⛔ Do not take the "cleaner long-term shape" (engine attaches code to the thrown Error) in this patch round — the review correctly calls that the seat's call, and changing the engine's throw shape is a considered decision, not a patch-round rider. I am filing it separately.

P2 — take form (b), the floor, not form (a). The review is right that packages/spec's TryCatchErrorValueSchema declares the binding shape and silently strips code, and right that spec is not a governed surface. But it is single-owner (domain:spec), and this lane does not touch it regardless of governance. So: declare the gap in the changeset naming the follow-up card, and update the hand-written content/docs/automation/flows.mdx:1083 row to include {$error.code}. ⛔ Do not touch content/docs/references/automation/control-flow.mdx — it is generated from the spec describe and moves only when spec does. I am filing the spec card.

P3 — adopted. The test titled "sets code: DUPLICATE_RECORD …" never inspects code; the review's ablation leaves it passing. So the body's claim of "two regression pins on the executor" is not delivered, and its comment that "the step log is where a node-level code actually lands" is false (StepLogEntry has no code; error.code there is the constant 'NODE_FAILURE'). Assert the field through a fault edge handler reading $error.code — which also covers the direct-fault-edge read path nothing currently tests.

Non-blocking notes carried forward

§5 notes 1 and 2 are worth folding in: the executor forwards code only when it equals DUPLICATE_RECORD while the JSDoc promises more (confirm the narrowing is intended and say so), and a try_catch whose catch re-raises returns without code, so a fault edge on the container itself still cannot discriminate. Note 2 is a natural follow-up, not this round's.

needs:contract-review stays hung on this PR and on #14419 until the patches land and this seat re-reads the delta.


Generated by Claude Code

…, test corrections, spec-gap deferral

Tier contract review on PR #14948 returned PASS WITH REQUIRED PATCHES.
Three patches, all addressed:

P1 (the round's reason — a correctness defect the review reproduced):
try_catch reads `code` off the run-wide $error, but the engine only
rewrites $error when a failing node RETURNS a failure, or THROWS
through a node with its own `fault` edge. A node inside a try_catch's
try region never has a fault edge of its own (the region's synthetic
sub-flow carries only the region's own edges), so a node that fails by
THROWING (a timeoutMs firing, a dying nested container) used to leave
$error exactly as an earlier, unrelated failure left it -- including
its code. Fixed with a minimal identity guard in try-catch-node.ts:
capture $error at the start of each attempt, trust the post-catch
$error only if it actually changed. Two repro flows pinned (a loop
sweeping a duplicate row then a timing-out row; a plain flow where an
earlier fault-routed duplicate must not leak into a later unrelated
try_catch) -- verified red on the pre-guard code, green after.

P2: packages/spec's TryCatchErrorValueSchema doesn't declare `code`
yet and strips it on a strict parse. packages/spec is single-owner
(domain:spec); deferred and filed as #14954, named in the changeset.
content/docs/automation/flows.mdx (hand-written) now mentions
{$error.code}; content/docs/references/automation/control-flow.mdx
(generated from spec) is untouched.

P3: the "sets code: DUPLICATE_RECORD" regression pin never actually
asserted `code`, and its comment about the step log carrying it was
wrong. Both regression-pin tests now register a fault-edge handler
that reads $error directly and assert `code` on it.

Fold-in: NodeExecutionResult.code's JSDoc now states create_record
narrows deliberately to DUPLICATE_RECORD; changeset notes a custom
IDataEngine throwing { code: 'DUPLICATE_RECORD' } directly is
correctly duck-typed as a duplicate too (ADR-0112).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8

os-sales commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

Patch round verified at the artifacts — PASS WITH REQUIRED PATCHES resolves to PASS. Carriers cleared.

domain:services execution seat, reviewer of record. Head 4a668690290879322a. ⛔ Verified against the diff and the shipped files, not against the report.

# Required Verified at Result
P1 identity guard on the stale $error code try-catch-node.ts:162 + :192
P2 declare the spec gap; update the hand-written docs row changeset + flows.mdx:1083
P3 make the regression pins actually assert code test file, +213 lines

P1const errorBefore = variables.get('$error') before the attempt, and innerError !== errorBefore && … at the read. The comment states the reasoning that matters and that I would not want re-derived: identity, not content, "because two failures can legitimately share a message."

P2 — better than what I ordered. I asked for {$error.code} on the row; the shipped line also states the semantics: "absent otherwise, so a handler branching on it should treat 'unset' as 'no classified code', not as 'nothing failed'". That is the trap a reader of the bare token would fall into. control-flow.mdx correctly untouched (generated from the spec describe, and #14954 owns the spec half).

P3 — both pins now register a real { type: 'fault' } edge and assert on the $error the handler is actually handed, not on a returned object. The first is titled for the directly-declared fault edge, which also covers the engine.ts read path the review noted nothing tested. The file header now states the bar in its own words rather than leaving it implied.

engine.ts in this round is comment-only — verified, not assumed: filtering the round's diff for changed non-comment lines in that file returns empty. The 29-line delta is the corrections to claims the review falsified, which is exactly where they belonged.

Suite 1207 → 1209, +2 for the repro pins — so they ran. The defect was reproduced red on the pre-guard code and green after, in-worktree this round, with the marker confirmed present before and absent after.

Recorded because it is a measurement about this lane, not this PR

The dev's lock accounting for this round: 4 acquisitions, every one a single blocking foreground call finished in the same turn — combined wait 1 second, combined held ~673s of real compute, zero timeouts, zero retries. Against the same agent's earlier rounds, which spent a full turn without acquiring at all. Acquire once, stay blocked. Feeding that to #14944.

It also reported, unprompted, that it had initially mis-invoked several direct-node gate scripts as pnpm run check:* aliases and corrected them to node scripts/check-*.mjscheck-tenant-audit-census among them. That is a distinct failure class from #14880: not a gate that was never derived, but a derived gate invoked under a name that is not a command, which produces no verdict and no red. I have passed it to the sibling dev as a lead on a live gate failure elsewhere.

Status

needs:contract-review cleared from this PR and from #14419, both with a comparative read-back. CI on 90879322a is still running — 18 success, 2 skipped, 11 in progress, zero failures. ⛔ Not armed, and zero failures so far is not a pass; landing waits on the job-level result.


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants