Repository navigation
feat(spec): FLOW_FAILED carries the refused write's inner refusal — AutomationResult.userMessage / innerRefusal and one FlowFailureDetailsSchema for the four doors - #25037
Conversation
…utomationResult.userMessage/innerRefusal and FlowFailureDetailsSchema Ruling B' on #24914 (decision batch #379 item 5). Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…ck relaxed by ruling B' Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
… reference docs and strictness counts Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 149 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6bf75f27ef21ad1b6f7bb571ec9ceda5de665fd6 && git checkout 6bf75f27ef21ad1b6f7bb571ec9ceda5de665fd6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3b3830ca223eaea7875babb155e47eebbcd0300f 22786c992e928af6c43ca807998408adb52add4e && git checkout -B drift-repro 3b3830ca223eaea7875babb155e47eebbcd0300f && git merge --no-ff 22786c992e928af6c43ca807998408adb52add4e
node scripts/docs-audit/affected-docs.mjs --json 3b3830ca223eaea7875babb155e47eebbcd0300f
|
Contract reviewServed-tier: ① Derived judgmentsRead against card #24996 (ruling B′ on #24914, decision
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Seat re-read of the check-runs at landing: all 42 runs on |
Closes #24996
Clause-②: yes (widening)
The contract half of ruling B′ on #24914: the maintainer's 「同意」 in decision batch #379, item 5. With it, the refusal a flow write node meets reaches the client structurally, the way a record save's refusal does. This PR changes only
packages/spec, plus one runtime docblock (named below). Emission is #24914's job: the crud nodes, the engine carriage and the four doors. The console half is an objectui card. Until those land, nothing on the wire changes.The ruling's bullets (verbatim from #24996, "What the ruling binds")
What changed
AutomationResult(packages/spec/src/contracts/automation-service.ts) gains two optional members:userMessage?: string, with exactlyApiErrorSchema.userMessage's semantics: presence IS the marking, it is set only when the inner refusal's producer opted in, and it never replaceserror;innerRefusal?: FlowInnerRefusal.TriggerFlowResponseSchema.datamirrors both members. The compile-time pinTriggerFlowResponse['data']=AutomationResult(Twopackages/specroute response schemas declare adataNARROWER than the contract their route relays —AnalyticsResultResponseSchemaandTriggerFlowResponseSchema#13078) holds.FlowInnerRefusalSchema(new,@objectstack/spec/api) is{ code, message }, withcode: ErrorCode.ErrorCodeis the same closed ADR-0112 set thatApiErrorSchema.codeparses against, so "wire spelling" is enforced, not just described:UNIQUE_VIOLATIONparses, and a driver spelling such asER_DUP_ENTRYis refused.FlowFailureDetailsSchema(new) is theerror.detailsof every door's400 FLOW_FAILEDarm, declared once:errorMessage?,summary?andinnerRefusal?. It is a plain non-strict object, theResumeFailureDetailsSchemapattern. On the resume door,runId/status/repairablesit beside these keys in the same object, and each schema parses its own members.errorMessageor the runsummary— the/actionsdoor has no channel for them #9585 ceiling. On/actions,error.detailswas capped at "exactly two keys … no new schema keys". The ruling relaxes that cap by one named key: the declared set is now the three keys above, at all four doors. The ceiling is enforced in runtime, not in spec:FlowActionRunDetailsdocblock (packages/runtime/src/action-execution.ts), which this PR updates to cite ruling B′ (comment only, no type change);packages/runtime/src/actions-flow-dispatch-status.test.ts. That pin is left unchanged on purpose: nothing emitsinnerRefusalyet, so it still describes the wire exactly. A screen flow whose write node is refused shows the end user the engine's raw node text, in English, with the node id and the object API name ("Node 'create_account' failed: create_record(crm_account) failed: …") #24914 relaxes it in the same change that emits the key.error.messageat every door,error.code: FLOW_FAILED, andAutomationResult.code(still the closed union, no new ledger code). No wording pin moves.check:generated --fix, which rewrote only what it proved stale):api-surface/,export-origins/,declaration-map/,authorable-surface/,json-schema.manifest/,content/docs/references/**, and the strictness-ledger counts (api/446 → 448, the two new object sites).The member's name:
innerRefusalFlowActionRefusal,classifyFlowRefusal,FlowRefusalCode,refusal: trueonApiErrorSchema.innerprefix keeps it distinct fromrefusalMessage, which belongs to anendnode's deliberateoutcome: 'refused', a different thing.cause: it collides withError.causechaining semantics and says nothing about the shape;writeRefusal: too narrow, since the same{ code, message }shape fits a read node refused by permission;error: already the diagnostic string onAutomationResult.Contract review owed
This PR owes a
CONTRACT_REVIEW_TIERcontract review:Clause-②: yes (widening), a new public schema pair, and a ruled relaxation of the #9585 ceiling. Two choices are left to the reviewer explicitly:innerRefusal.codeis the closedErrorCode, not an open string. A producer must map to the wire spelling, and an app-specific hook code has nodeclaredCodearm inside the member.FlowFailureDetailsSchemadeclares the two existing artefacts (errorMessage,summary) beside the new key, so the ceiling is a declared set instead of prose. Before this PR, those artefacts existed on the wire but were not declared in spec.Tests
packages/spec/src/api/automation-api.zod.test.ts, new suiteFlowFailureDetailsSchema — one inner-refusal shape across the four FLOW_FAILED doors:/actions, endpoint), an envelope whosedetails.innerRefusalis{ code: 'UNIQUE_VIOLATION', message }and which has a top-leveluserMessagevalidates throughBaseResponseSchemaandFlowFailureDetailsSchema. On the resume door it also validates throughResumeFailureDetailsSchema;['code'];FlowFailureInnerRefusalIsTheContracts, holdsFlowFailureDetails['innerRefusal']equal toAutomationResult['innerRefusal'].Local results, all at
22786c992e(git rev-parse --short HEAD, the final commit):pnpm --filter @objectstack/spec test: 703 files / 21382 passed, 1 todo.pnpm --filter @objectstack/spec test:repo: 72 files / 1236 passed.pnpm --filter @objectstack/spec typecheck: exit 0. That coverstsc --noEmit, the scripts typecheck and the test-typecheck ratchet, which compiles the new type-level pin.pnpm --filter @objectstack/spec check:generated --fix: 5 artifacts were proven stale and regenerated, then re-checked green.actions-flow-dispatch-status.test.ts: 19 passed, unchanged, as expected.runtimetsc --noEmit: exit 0.node scripts/pm/dispatch-gates.mjs --commandsderived 113 commands, plus the 6 roster families that sit under a touched directory. 118 of these 119 are exit 0. A first pass printed PREREQUISITE NOT MET for six of them; those were re-run green after the dependency closure was built.check:dual-build-cjs-loadsis NOT MEASURED: it needs a full workspace build and is left to CI'sBuild Core.pnpm lint(repo-wide eslint) was not run locally and is CI's.Acceptance notes
AutomationResultinpackages/spec/src/automation/flow.zod.ts:1085. Those lines areFlowSchema's authoredsuccessMessage/errorMessage, which are flow-definition keys.AutomationResultis the TypeScript interface inpackages/spec/src/contracts/automation-service.ts, and its Zod mirror isTriggerFlowResponseSchema.data. The members land there, andflow.zod.tsis untouched: a runtime-produceduserMessageis not something a flow author writes. The claim's file surface namedflow.zod.ts, socontracts/automation-service.ts(and the one runtime docblock) are additions to that surface, declared here.pnpm --filter @objectstack/runtime typecheckis exit 1 locally, in itscheck:test-typecheckhalf only. It reports one type error insrc/organization-ownership-boot-guard.turso-remote.kernel.test.ts, a file this PR does not touch. This PR's runtime diff is a docblock-only edit, so it cannot move a type verdict. The cause is unverified: possibly a stale dependencydiston this shared box, possibly drift already on main. CI'sTypeScript Type Checkis the authority on it.contract.zod.tsis untouched: the A flow ACTION that fails cannot carry the author'serrorMessageor the runsummary— the/actionsdoor has no channel for them #9585 ceiling does not live there.ApiErrorSchema.userMessagealready carries the envelope's top-level channel.Generated by Claude Code