Skip to content

feat(spec): TryCatchErrorValueSchema declares the optional open-string code key the try_catch engine binds - #15672

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-14954-try-catch-error-code-key
Sep 5, 2026
Merged

feat(spec): TryCatchErrorValueSchema declares the optional open-string code key the try_catch engine binds#15672
os-project-manager merged 2 commits into
mainfrom
claude/issue-14954-try-catch-error-code-key

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #14954

What

packages/spec/src/automation/control-flow.zod.tsTryCatchErrorValueSchema gains an optional code: z.string() key, and the try_catch config's errorVariable describe names it, so the ONE shape the catch region's author, the engine and the run log share again equals what the engine binds. PR #14948 (#14419) added that fifth key on the engine side; the schema was a plain z.object that did not declare it, so any round-trip through the declared shape silently STRIPPED it, and the generated reference page documented four keys where the runtime binds five.

Typed as an OPEN string, deliberately not StandardErrorCode and not the ledger union. The card's warning, quoted from its body: "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 closed-at-every-door rule governs ApiErrorSchema.code at an HTTP door; this value is bound in-process and never crosses one. The docblock on the schema says the same, so the next author is told which decision they are standing on.

The describe text, verbatim

code (on TryCatchErrorValue):

Platform-classified error code (ADR-0112) of the failure that ended the try region, e.g. create_record's DUPLICATE_RECORD; present only when the failing node's own result carried one, so a catch region branching on $error.code treats "unset" as "no classified code", never as "nothing failed". An open string, not a closed enum: the vocabulary is StandardErrorCode plus registered ledger codes plus tenant-authored codes, and third-party node executors bind their own

errorVariable (on TryCatchConfig):

Variable holding the caught error in the catch region — a TryCatchErrorValue: nodeId, message, code when the failing node carried a platform-classified error code (ADR-0112 — branch on $error.code to tell "the row is already there" from "the store is down"), and iteration / item when the failure happened inside a loop body

Generated followers — regenerated, never hand-edited

Derived, not guessed: pnpm --filter @objectstack/spec build (runs gen:schema), then pnpm --filter @objectstack/spec check:generated to name what is stale, then check:generated --fix, which regenerates only what the sweep proved stale.

  • packages/spec/authorable-surface/automation.json — rewritten by gen:schema during the build: +1 entry, automation/TryCatchErrorValue:code.
  • content/docs/references/automation/control-flow.mdx — the ONE artifact check:generated named stale (✗ check:docs, "1 of 15 artifact(s) stale"); regenerated by gen:docs through --fix: the errorVariable row changes and a code row joins the TryCatchErrorValue table.
  • Measured unchanged by the same sweep (each ): api-surface/ (no new export — TryCatchErrorValue (type) / TryCatchErrorValueSchema (const) were already listed), json-schema.manifest/ (the schema set is unchanged), export-origins/, declaration-map/, spec-changes.json, docs/protocol-upgrade-guide.md, authorable-surface.base.json (the anchor: it lags by design, was not hand-adjusted, and check:authorable-surface is green).

Strictness-ledger reading

check:strictness-ledger tracks per-file strip-site COUNTS and each row's Class verdict, not a schema's key set. control-flow.zod.ts keeps exactly one strip site — TryCatchErrorValueSchema, class wire, the "+1 at #13681" row — and an additive optional key moves neither the count nor the class, so the generated counts artifact is unchanged and the gate is green (check:generated prints ✓ check:strictness-ledger; the standalone run exits 0). The hand-written row's key list records what #13681 added and stays true as history; not edited. ADR-0087: an additive optional key retires nothing, so no entry is owed; node scripts/check-adr-0087-registration.mjs --base origin/main exits 0 on this changeset (that gate judges declared-breaking changesets only).

Changeset

.changeset/try-catch-error-value-code-key.md@objectstack/spec: minor. Semver derivation in one line: a new optional key on a published schema widens the accept set and the exported TryCatchErrorValue type without retiring or renaming anything ⇒ minor (not patch — it is a contract widening rather than a behaviour repair; not major — nothing that parsed before stops parsing).

Verification — every reading below is on 214ae5211, this PR's head (git rev-parse --short HEAD = 214ae5211)

Heavy runs went through scripts/pm/os-verify-lock.sh; verdict lines are quoted from the tools' own output, exit codes captured before any pipe.

  • Spec unit tests — pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/automation/control-flow.test.tsTest Files 1 passed (1) · Tests 45 passed (45) (lock VERDICT command-exit 0). Four new pins: a round-trip preserves code (asserted as whole-object equality, since a plain z.object strips silently); code is optional and the key is ABSENT, not undefined, when no classified code was carried, and composes with iteration / item; the value is an OPEN string — ACME_RATE_LIMITED and DUPLICATE parse, while code: 42 is refused at path ['code'] with invalid_type; the describe text reaches the JSON schema (z.toJSONSchema) on both the value and errorVariable.
  • pnpm --filter @objectstack/spec typecheck → exit 0 (tsc --noEmit, check:scripts-typecheck, check:test-typecheck: OK); tsc -p tsconfig.test.json --listFilesOnly reaches src/automation/control-flow.test.ts (1 hit), so the test layer's verdict covers the edited test file.
  • Consumer package, downstream direction (the package that binds the value): dependency closure built first — pnpm turbo run build --filter="@objectstack/service-automation^..."Tasks: 20 successful, 20 total — then pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2Test Files 107 passed (107) · Tests 1290 passed (1290) (lock VERDICT command-exit 0). That package's vitest config aliases only @objectstack/platform-objects to src; @objectstack/spec resolves through the workspace link to the REBUILT dist, so the automation: create_record collapses the engine's DUPLICATE_RECORD envelope to a string, so a flow's try_catch / fault edge still cannot tell "already there" from "the store is down" #14419 pins in create-record-duplicate-code.test.ts (expect(seen[0]?.code).toBe('DUPLICATE_RECORD'), the loop-row case and the stale-code cases) ran against the new declaration. pnpm --filter @objectstack/service-automation typecheck → exit 0.
  • Reverse verification, so the consumer readings are of the rebuilt .d.ts and not a cache: a scratch file under packages/services/service-automation/src assigning { nodeId, message, code: 42 } to TryCatchErrorValueerror TS2322: Type 'number' is not assignable to type 'string' (exit 2, exactly one error), while the positive control on the next line, code: 'DUPLICATE_RECORD', compiled. packages/spec/dist/automation/index.d.ts:1247 reads code: z.ZodOptional wrapping z.ZodString. The scratch file was deleted; git status is clean.
  • Gate families — derived off the ACTUAL diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths; it reported "5 path(s) vs merge base 7087f99 of 'origin/main' and HEAD"): 93 commands, each run on its own with the exit captured before any pipe. 91 exit 0, including every family the dispatch named (check:api-surface, check:authorable-surface, check:browser-reachable-entries, check:dual-source-exports, check:entry-nameability, check:export-origins, check:docs, check:empty-state, check:changeset-gate-self-tests, check:corpus-claim-drift, check:cross-package-test-inputs, check:doc-anchors, check:doc-authoring, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check:doc-formula-expressions, check:doc-security-posture) plus, among those the derivation added, check:nul-bytes, check:strictness-ledger, check:generated, check:merge-driver, check:test-source-alias, the changeset trio (check-empty-changeset, check-changeset-no-major, check-adr-0087-registration) and check:skill-examples (✅ 257 prose examples type-check across 3 surface(s), after building @objectstack/client and @objectstack/client-react so its SDK surface could measure). Two are NOT MEASURED locally, by their own verdict text, and belong to CI: check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/, 49 unbuilt packages) and check:type-check-debt (exit 3, Build the closure first … This is NOT a pass and NOT a finding). One transient, stated rather than hidden: check:docs-audit-scope went red once (affected-docs self-test, 1 of 568 cases) while an aborted first check:skill-examples run had left its extracted-example build files in the tree; it is green on a pristine 7087f99c compare worktree and green on re-run after the clean check:skill-examples pass, with 0 ignored .ts files remaining under packages/.
  • pnpm lint (repo-wide eslint) was not run locally and no narrowing measurement is claimed for it — CI owns that run.

Hand-written docs

content/docs/automation/flows.mdx:1133 already documents {$error.code} (from PR #14948) in terms consistent with the describe text above — "absent otherwise, so a handler branching on it should treat "unset" as "no classified code", not as "nothing failed"" — so nothing there is false; not edited.

Out of scope, filed

🤖 Generated with Claude Code


Generated by Claude Code

…g code key the try_catch engine binds

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
…ence for the new code key

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/automation.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/automation.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 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 — 129 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 58f30d9ef152f07d668bc472c58512cd0c99aeafpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 147c76f0a98a3fc189ede53b2612cf31b3c371d0 — the merge of head 214ae52115974b574704006f4cce19cf064c582e into base 58f30d9ef152f07d668bc472c58512cd0c99aeaf, 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 147c76f0a98a3fc189ede53b2612cf31b3c371d0 && git checkout 147c76f0a98a3fc189ede53b2612cf31b3c371d0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 58f30d9ef152f07d668bc472c58512cd0c99aeaf 214ae52115974b574704006f4cce19cf064c582e && git checkout -B drift-repro 58f30d9ef152f07d668bc472c58512cd0c99aeaf && git merge --no-ff 214ae52115974b574704006f4cce19cf064c582e

node scripts/docs-audit/affected-docs.mjs --json 58f30d9ef152f07d668bc472c58512cd0c99aeaf

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 5, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review September 5, 2026 03:11
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit 581d8f8 Sep 5, 2026
43 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-14954-try-catch-error-code-key branch September 5, 2026 03:38
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/s tests tooling

Projects

None yet

2 participants