Repository navigation
runtime: two hand-built 400 refusals put off-catalog codes on details.fields[].code (unrecognized_keys, invalid_string), which ADR-0114's closed FieldErrorCode catalog forbids #24597
Description
Filing gate ①, class (b): a published closed catalog that a public door's answer breaks, measured. reach: an operator's POST /api/v1/automation/approval_flow/runs/run_7/cancel with body {"foo":1} answers 400 VALIDATION_FAILED with details.fields[0].code: "unrecognized_keys". That was measured through the route's domain handler by #24546's dev (report 6271088846, PR #24594). The sites are in packages/runtime, which is domain:cli by the lane table.
Filed by domain:cli seat 1 (seat post #6024) · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T, from that report's out_of_scope_findings[0]. ⛔ Not a claim. Grade, lane and area:* are triage's. Reader who acts: the domain:cli seat.
The contract
packages/spec/src/api/errors.zod.ts(FieldErrorCode, about:222–:292onorigin/main5a871e4e4) declares a closed catalog: "a service does not get to invent one — it adds a member here" (ADR-0114 D2).- The members for these two conditions are
unknown_field("a key the target does not declare"), which the resume door one route over already uses, andinvalid_format("a declared pattern/format the value does not match"). - The published
ApiErrorSchematypesdetailsloosely, so no gate sees an off-catalog code. A client that switches on the catalog gets a value it cannot name.
The sites (read on origin/main 5a871e4e4)
packages/runtime/src/domains/automation.ts:959: the run-lifecycle body refusal (cancel / restore-suspension unknown key) writescode: 'unrecognized_keys' as const, Zod's issue code.packages/runtime/src/domains/permission-set-clone.ts:194: the clone door's name rule writescode: 'invalid_string'(Zod's), where the catalog hasinvalid_format.- Census:
git grep -n "'unrecognized_keys'\|'invalid_string'" -- 'packages/runtime/src/**/*.ts' ':!*.test.ts'finds these two writers and one reader,packages/runtime/src/domains/packages.ts:888. That reader compares a Zod issue's own code, which is correct and not a wire write. - The done criterion below asks for the full census of hand-built
details.fieldswriters against the catalog, not only these two.
Precedent
#8124 (closed) removed the same leak from fieldsFromZodIssues (ADR-0114 D3). These two sites build their field rows by hand, so that fix did not reach them.
What done looks like
- Both sites write catalog members:
unknown_fieldandinvalid_format. The message text stays. - A census of every hand-built
details.fields[]writer inpackages/runtime, each checked againstFieldErrorCode. Every off-catalog one is fixed in the same PR, or listed with its reason. - A pin per site: the refusal's
details.fields[].codeparses underFieldErrorCodeSchema, with an ablation. - Whether changing a wire code narrows anything is the PR's call to state (
Clause-②line). No accept set moves; only the refusal's vocabulary does.
Dedupe
- MCP
search_issues"FieldErrorCode closed catalog off-catalog details.fields code unrecognized_keys invalid_string runtime": 2 hits, both closed and different (check:error-status-conformance leaves 36 real producers unresolved —StandardErrorCode.enum.CODEis one mechanical shape the constant index cannot read #15644, an error-status conformance gap; Field-level error codes are four vocabularies with no schema — needs its own catalog (ADR-0112 D6 follow-up) #3977, the catalog's founding card). - "ADR-0114 field error code Zod issue code leaks into refusal envelope fieldsFromZodIssues mapping": 36 hits. The family's closed precedent
fieldsFromZodIssuesleaks Zod's issue codes onto the wire'sfields[].code— the exact pass-through ADR-0114 D3 closed, still live in@objectstack/runtime#8124 is the nearest; no open card names these sites. - The control query reaches the family (
fieldsFromZodIssuesleaks Zod's issue codes onto the wire'sfields[].code— the exact pass-through ADR-0114 D3 closed, still live in@objectstack/runtime#8124, Field-level error codes are four vocabularies with no schema — needs its own catalog (ADR-0112 D6 follow-up) #3977). - Dedupe words: unrecognized_keys field code; FieldErrorCode closed catalog; invalid_string permission-set clone; ADR-0114 off-catalog details.fields code.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 19, 2026
Triage: p2 ·
bug·domain:cli·area:api·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-19T16:57ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch. Fresh read on
origin/mainc03b17d784.packages/runtime/src/domains/automation.ts:959hand-buildscode: 'unrecognized_keys'. The closedFieldErrorCodecatalog is atpackages/spec/src/api/errors.zod.ts:244.unknown_fieldand the catalog's format member). Nothing spec declares moves, so this is not in the protocol-first class.domain:cli(packages/runtime). Pin both sites with the catalog member, plus a closed-set check over the domain's hand-built refusals.buglabel is kept.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 6 ·
domain:cliseat 1 · p2 · 2026-10-19T22:25ZSession:
session_01SYEJqDL3NXxrJRBVqaMu9TAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24597-field-code-catalogWorktree:
objectstack-issue-24597Domain:
domain:cliSeat:
domain:cli#1(seat post #6024)File surface (read on
origin/mainbfc1539fe; line numbers move; stop on a breach and explain it in the report):packages/runtime/src/domains/automation.ts:959:code: 'unrecognized_keys'→unknown_field.packages/runtime/src/domains/permission-set-clone.ts:194:code: 'invalid_string'→invalid_format.6272582926asks for: every hand-builtdetails.fields[]writer inpackages/runtime, checked againstFieldErrorCode(packages/spec/src/api/errors.zod.ts, about:244). An off-catalog one is fixed here; otherwise it is listed with its reason.details.fields[].codeparses underFieldErrorCodeSchema, with an ablation, plus the closed-set check triage names over the domain's hand-built refusals. Apatchchangeset on@objectstack/runtime.packages/spec/**(the catalog is read, never extended here); messages, statuses and every other field of the envelopes.Container & model:
S,mode:subagent,model: default tier(no path-derived mandate)Clause-②: no
Basis for the Clause-② line: the refusals' field codes move onto the members the published catalog already declares. No accept set or status moves, and no catalog member is added.
Responsibility: this lane's own refusals (
packages/runtime). No platform path covers it. It reaches an operator's run-lifecycle body refusal (measured by Flow resume answers 200 and silently dropsvariableswhen the body also carriesinputs#24546's dev) and the clone door's name rule.Thread-read: 6272582926
Serial constraints cleared:
automation.tsorpermission-set-clone.ts, and nopm:dispatchedclaim names them (read in this act).--out PLAN/--apply --plan-file PLAN, which 18.0 refuses on every database (#24637, domain:cli row) #24651 (organization-ownership-boot-guard.ts), and rest: every GET /api/v1/data/OBJECT still runs a full zod safeParse of the object's definition (the API-exposure gate's by-name read is decorated with diagnostics) — 11.8% of a HotCRM read's CPU #24535 (PR perf(rest): GET /data/OBJECT no longer computes metadata diagnostics; the exposure gate reads getMetaItemForExecution #24666) are disjoint.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{
"issue": 24597,
"status": "done",
"branch": "claude/issue-24597-field-code-catalog",
"pr": "#24683",
"session": "session_01SYEJqDL3NXxrJRBVqaMu9T — mode:subagent, the parent seat's harness-stamped id (the relay route read it from the container's CLAUDE_CODE_REMOTE_SESSION_ID; it is also the Claude-Session trailer on 5fd5f5e)",
"premise_still_valid": true,
"summary": "The premise held at origin/main bfc1539. automation.ts:957 (refuseInvalidRunLifecycleBody, cancel / restore-suspension unknown key) wrote
unrecognized_keys, and permission-set-clone.ts:194 (readCloneBody name rule) wroteinvalid_string. Both now write catalog members,unknown_fieldandinvalid_format, each with a one-line ADR-0114 D2 comment. Measured through the real HttpDispatcher handler, a field-by-field diff of the before and after envelopes shows exactly one path changing per site, body.error.details.fields[0].code; status 400, error.code VALIDATION_FAILED, message, httpStatus and the row's field and message are byte-identical. Census: an AST walk of packages/runtime/src (tests excluded) finds 31 hand-built rows {field, code} and 48 helper calls. The two above were the only off-catalog rows; the other 29 are catalog members. endpoint-executor.ts:386 (required) is already pinned exactly in endpoint-executor.test.ts, and query-param.ts:81 takes acodeparameter typed FieldErrorCode, so tsc closes it. Not wire writers: the fieldsFromZodIssues rows (ADR-0114 D3 mapper), the two empty-row refusals in data.ts, and the relays of producer rows (actions.ts:913, flowDefinitionRefusal's pass-through, the sandbox). packages.ts:888 compares a Zod issue's own code to name misplaced install options, a read and not a wire write. New pin file packages/runtime/src/domains/field-error-code-catalog.test.ts has 41 tests. Part 1 (5 tests) pins each site: the 400 VALIDATION_FAILED envelope, exact {field, code} rows, each code parsing under FieldErrorCode, and no service call. Part 2 (36 tests) is the closed-set check: it drives every hand-built row under src/domains/ through the real dispatcher and parses each details.fields[].code under FieldErrorCode, naming the row's expected {field, code}. No gate script or lint rule was added. The changeset is a patch on @objectstack/runtime with a line-initialClause-②: no, and it names the wire change. The PR body carriesFixes #24597, the Clause-② line and its basis. Naming: the dispatch and claim sayFieldErrorCodeSchema, but the spec exports the Zod enum asFieldErrorCodeand noFieldErrorCodeSchemaexists, so the pins parse underFieldErrorCode. Conflict, stated rather than resolved silently: os-dev.md makes repo-wide scans (pnpm lint) andtest:repoCI-owned and never owed by the dev, while the dispatch asked for both. I ran both as asked (green); no standard clause forbids running them. The worktree was removed after the PR opened, without --force.","tests": "All at 5fd5f5e (the final and only commit), each through scripts/pm/os-verify-lock.sh (VERDICT command-exit lines quoted). (1) New file:
pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 src/domains/field-error-code-catalog.test.ts, 41 passed. (2) Fullpnpm --filter @objectstack/runtime test(vitest project local): 'Test Files 426 passed (426) · Tests 6712 passed | 19 skipped (6731)', VERDICT command-exit 0, held 545s (a shared-box reading). (3)pnpm --filter @objectstack/runtime test:repo(vitest project repo): 'Test Files 3 passed (3) · Tests 808 passed (808)', VERDICT command-exit 0. (4)pnpm --filter @objectstack/runtime typecheck(tsc --noEmit, then check:test-typecheck over tsconfig.test.json): VERDICT command-exit 0. 'check:test-typecheck: OK — 27 file(s) / 190 error(s) / 68 pinned signature(s) held'. The new file has no ledger entry, so it compiles with zero errors. (5) Before and after envelopes, measured through handleAutomation / handleSecurity by a scratch vitest printer that was deleted and never committed. Before:unrecognized_keyson cancel and restore-suspension,invalid_stringon clone. After:unknown_fieldandinvalid_format. The diff is one path per envelope. (6) Ablation, one leg per fixed site, after the fix was committed. The test imports ../http-dispatcher.js relatively, so the subject resolves to src/ and no dist/ build is in the path. The mutation went throughnode scripts/ablation-replace.mjsinside a wrapper with atrap restore EXIT INT TERMon absolute paths. automation leg:code: 'unknown_field' as const,becamecode: 'unrecognized_keys' as const,; anchor 1 to 0, replacement 0 to 1, blob 84d4f8b34e42 to 5ad01f696481; result '4 failed | 37 passed' (both Part 1 run-lifecycle pins and both Part 2 run-lifecycle unknown-key rows, each failing with: details.fields[].code "unrecognized_keys" ... is not a FieldErrorCode member); restored blob 84d4f8b34e42 == HEAD, git diff HEAD empty. clone leg:code: 'invalid_format'becamecode: 'invalid_string'; anchor 1 to 0, blob 820646d40ba1 to ffdb9582fd37; result '4 failed | 37 passed' (three Part 1 clone pins and the Part 2 name-rule row, same catalog-parse assertion); restored blob 820646d40ba1 == HEAD, git diff HEAD empty. Direction: red, as predicted; no other test moved. (7) Grep of other packages' tests for assertions onunrecognized_keys/invalid_stringfrom these two doors: none. Every hit reads a Zod issue's own code on a local parse (cli, lint, metadata-protocol, drivers, core logger, service-automation wait-node). In ../objectui (79cb8c0), clientValidation.ts:1020 reads a client-side Zod issue code, not these doors' wire rows.","gates": "Local subset, at 5fd5f5e:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 67 commands (4 changed paths against merge base bfc1539; 365 changed lines, under the 3000 threshold). Each ran byte for byte with its exit code captured before any pipe. First pass: 66 exit 0, andpnpm check:dual-build-cjs-loadsexited 3 ('PREREQUISITE NOT MET — this gate reads built output', 39 packages without dist/). After a fullturbo run build --filter=!@objectstack/docs --concurrency=2(VERDICT command-exit 0, 76 tasks, 75 cached) it exited 0: '111 published require entry point(s) across 69 package(s) load'. check:dts-closure (76 packages swept, against 32 before), check:sourcemap-no-sources-content, check:lean-entry-closure and check:published-files were re-run on the built tree, all exit 0. Reconciliation:node scripts/pm/dispatch-gates.mjs --ran ran.list, exit 0, 'Run reconciliation — 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.' Full union, as the dispatch asked:pnpm lint(eslint . --no-inline-config), LINT_EXIT=0, no output. check-platform-checklist --changed-from does not apply, because no exported symbol was renamed or removed. CI on PR #24683 at head 5fd5f5e, read when this report was written (37 check-runs), was not waited on. success: Auto Label, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Lint · scoped · tooling self-tests, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, filter. skipped: Build Docs, Console Pin Gate, Dogfood Regression Gate (shard matrix), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance (live PG + MySQL). in_progress: Build Core, Check Changeset, Governed Surface Queue Guard, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Test Core (1/6) through (6/6), Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace.","mcp_calls": "0 — no MCP GitHub tool was called. Reads went through single-card REST GETs (
gh api) on the card, its two comments, PR #24683 and its check-runs.","api_writes": "2 — both through the fleet-write relay, as objectstack-fleet[bot]. Each is one POST /repos/objectstack-ai/objectstack/dispatches. (1) pr_create, which executed POST /repos/objectstack-ai/objectstack/pulls (draft, #24683; read-back: 15814 bytes sent, 15814 stored, identical) and POST /repos//issues/24683/assignees (zhuangjianguo, listed). (2) this os-dev-report comment, which executed POST /repos//issues/24597/comments. Labels: none sent; skip-changeset does not apply because a patch changeset ships. git push (2, not REST): the empty-branch probe and 5fd5f5e.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted, not filed — the class stays open one level up. validationFailure(message, fields) in packages/types/src/validation-failure.ts types
fieldsas unknown[], which is how both rows got past tsc. Typing the rows'codeas FieldErrorCode would close the class at compile time for every caller in every package. That is a signature change to a published @objectstack/types export, outside this claim's file surface. It is recorded in PR #24683 Acceptance notes.","carrier: none (承接者:无) · noted, not filed — a read-only AST sweep of packages/*/src (tests excluded), for literal rows {field, code, message} whose string-literal code is not a FieldErrorCode member, found three hits in packages/metadata-protocol/src/protocol.ts:5772/5799/5810 (field_removed, field_type_change, field_required_no_default). Those are DESTRUCTIVE_CHANGE findings delivered as err.issues, not details.fields[], so they are not this catalog's position. The sweep sees only string-literal codes, so it is a supporting reading, not a proof. Recorded in PR #24683 Acceptance notes."
],
"deviations": "Two items in the local run go beyond os-dev.md's floor, both because the dispatch asked: test:repo, and pnpm lint as the full union. No origin/main merge was made, per the dispatch. A scratch envelope-printer test file was created in the worktree for the before/after measurement and deleted before the commit; it was never staged."
}
objectstack-fleet commentedon Oct 19, 2026
ACCEPT → PR #24683 comment
6279556446·domain:cliseat 1 · sessionsession_01SYEJqDL3NXxrJRBVqaMu9T· 2026-10-19T23:24Z · head5fd5f5e71. Landing waits on that head's checks.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Landed: PR #24683 →
3b8e2b145·domain:cliseat 1 ·zhuangjianguo· sessionsession_01SYEJqDL3NXxrJRBVqaMu9T· 2026-10-20T00:10Zmerged: true, merge commit3b8e2b145, merged 2026-10-20T00:09Z through the merge queueorigin/main3b8e2b145is an ancestor oforigin/main, with a single parent (squash). The 4 files and +363 / −2 match what was reviewed at5fd5f5e71gh-readonly-queue/main/*pr-24683*head remains6278722621→ report6279499710→ ACCEPT6279556446→ ready and auto-merge.FieldErrorCodecatalog members. The run-lifecycle body refusal (cancel, restore-suspension) carriesunknown_field, notunrecognized_keys. The permission-set clone name rule carriesinvalid_format, notinvalid_string.field-error-code-catalog.test.tspins both sites and runs every hand-built row underpackages/runtime/src/domains/through the catalog (41 tests).validationFailure(message, fields)inpackages/typestypesfieldsasunknown[]. A typed row would close the class at compile time. This is recorded for the next seat; not filed.completedon the merge.pm:dispatchedis removed in the stroke after this comment.Generated by Claude Code