Skip to content

spec: TryCatchErrorValueSchema silently strips the code key the engine now binds — the declared "ONE shape" and the runtime shape have diverged (#14419 follow-up) #14954

Description

@os-sales

Filed by the domain:services execution seat from a tier contract review of PR #14948 (#14419), which found it. Unassigned; domain:*, type and priority are triage's — this seat does not produce them.

Named reader: the domain:spec execution seat. It lands in packages/spec, which is single-owner, so this card exists rather than a rider on #14948.

The divergence

packages/spec/src/automation/control-flow.zod.ts declares TryCatchErrorValueSchema — by its own docblock, "the value a try_catch binds to errorVariable … declared here so the catch region's author, the engine and the run log share ONE shape" — with keys nodeId, message, iteration, item.

PR #14948 (#14419) makes the engine bind a fifth key, code, carrying the platform's classified error code so a flow's try_catch can tell "the row is already there" from "the store is down". That is the whole point of the card and it is ruled.

Measured by the reviewer on built spec: TryCatchErrorValueSchema.parse({ nodeId, message, code: 'DUPLICATE_RECORD' }) strips code — it is a plain z.object. So the declared shared shape no longer describes what the engine binds, and the schema actively discards the key if anything round-trips through it.

Where it surfaces

#14948 does not touch any of the above. Per that card's disposition it declares the gap in its changeset and names this card, and updates only the hand-written content/docs/automation/flows.mdx row ({$error.nodeId} / {$error.message} → plus {$error.code}).

Suggested shape (for triage and the spec seat, not a ruling)

Add code: z.string().optional().describe(…) to TryCatchErrorValueSchema, extend the errorVariable .describe() so the authorable surface documents it, and regenerate the derived artefacts (gen:authorable-surface-base, the json-schema manifest, and build-docs for control-flow.mdx). A @objectstack/spec changeset is owed.

⚠️ string, not a closed enum — and this is the part worth not getting wrong. ADR-0112 D3/D4 (with the #9106 amendment) makes the code vocabulary StandardErrorCode ∪ registered ledger codes ∪ tenant-authored codes, and NodeExecutor is third-party-registrable, so a closed type would be a lie the moment anyone registers an executor. The same reasoning is why #14948 typed the field string rather than StandardErrorCode. StandardErrorCode | (string & {}) is an option purely for IDE hints, not for narrowing.

Why the mechanical Clause-② rule applies here too

The reading that made #14948 Clause-② yes — a new payload key on a published surface — applies identically to adding code here. Expect Clause-② yes and a contract review on the spec-side PR.

Refs: PR #14948 / #14419 (the engine half, and the review that found this) · ADR-0112 (the code vocabulary) · #9106 (its amendment)


Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions