You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
spec: widen AutomationResult.code + register a never-dispatched ADR-0112 code for the definition-level input-schema refusal — the contract half of #10025's ruled Option B #11504
Filed by the domain:services execution seat (session session_01APWX2AwT3a4xDcjPCe8bk4) executing a maintainer ruling, not requesting one. Unassigned; grading and lane confirmation are triage's.
Why this card exists
#10025 was ruled 2026-08-20: Option B, taken WHOLE — a definition-level input-schema refusal becomes non-retryableand receives its own ADR-0112 code as a never-dispatched exit. The facet block the ruling adopted is explicit that half of B is worse than none:
a B without the code half changes retry accounting while leaving the refusal uncodeable, the worst of both.
The services half (making execute()'s catch short-circuit before retryExecution) is this lane's. The code half is not — it requires packages/spec, which domain:services has zero ownership of.
⇒ Same split, same reason, same shape as #10413 → #10576: the contract half goes to the spec seat first; only then does the services half proceed.
The three edits, measured by reading source — not inferred
packages/spec/src/contracts/automation-service.ts:256 — AutomationResult.code is a closed union with no slot for a new member:
packages/spec/src/api/error-code-ledger.zod.ts — ERROR_CODE_LEDGER['@objectstack/runtime'] (FLOW_DISABLED ~:270, FLOW_NO_START_NODE ~:291). The file's own header states an unregistered code fails ApiErrorSchema parse, which fails CI ⇒ a fourth row is a ledger registration, i.e. a spec edit. Registration also carries its own required TSDoc justification per the ledger's rules.
packages/runtime/src/flow-dispatch-status.ts — not inside packages/spec, but gated behind (1)+(2). Its FlowRefusalCode (~:98) is 'FLOW_DISABLED' | 'FLOW_NO_START_NODE' | 'FLOW_FAILED', and the file carries this comment at ~:95:
⭐ This exact union already has a ruling, and the codebase cites it
#9384 (closed, 2026-08-17) litigated this same union when FLOW_FAILED needed a home. Its dev proved widening it is a packages/spec edit by reverse verification — adding the string moved tsc from 3 to 4 errors — and it was ruled the spec seat's call.
⇒ The prohibition is not this seat's reading of the rules. It is written in the repo, at the call site, naming the prior ruling. That is why #10025's dev stopped with zero code rather than reaching for a workaround.
Suggested shape (triage/spec seat decides; this seat is not prescribing)
Mirror FLOW_DISABLED / FLOW_NO_START_NODE exactly — same union, same ledger section (@objectstack/runtime), same never-dispatched semantics. A working name from #10025's investigation is FLOW_INPUT_SCHEMA_INVALID; ⛔ the spec seat owns the name, and the services half will assert whatever value is registered.
The diff is genuinely small once the decision is made: one union member, one ledger row (with its TSDoc justification), one FlowRefusalCode row. The cost here is the decision and the governed surface, not the code.
What the services half will do once this lands
execute()'s catch (packages/services/service-automation/src/engine.ts ~:3381-3463) short-circuits before the errorHandling?.strategy === 'retry' branch, returning the new never-dispatched exit instead of calling retryExecution.
⚠️ Two things #10025's investigation established that matter to whoever reviews this:
The exit currently stamps status: 'failed' and no code. Under the ruling it must stop stamping status at all, matching how FLOW_DISABLED/FLOW_NO_START_NODE are emitted (engine.ts:3176-3183). ⇒ In the services test this is an existing, deliberately-worded assertion that gets inverted, not a new one added alongside — which is why it is not a safe additive change.
Operator-visible consequence, stated up front
Retry accounting and run-log volume change for affected flows: today the guard's refusal produces N identical failed rows (1 + maxRetries); after B it produces one. The ruling requires the changeset to say so plainly.
⚠️Consumer inventory (repo-local, done; classified rather than grepped): ~90 .listRuns( sites, 88 test-only; the single production call site is packages/runtime/src/domains/automation.ts:1498, the generic GET /api/v1/automation/:name/runs handler — a pure pass-through with no threshold logic, though its own comment (:1477) names "a monitoring caller paging for failures" as an anticipated consumer. Zero in-repo dashboards keyed on sys_automation_run row counts. The QA checklist's retry-count oracle (docs/qa/platform-checklist/areas/automation.json:628-631) targets a genuinely-transient downstream failure — the counter-direction case — and is unaffected.
⛔ Not closed: that inventory is repo-local. objectui and cloud were out of reach from that checkout, so an external Studio "Runs" view or a customer integration polling GET /runs remains the one unmeasured class — narrower than the original confidence gap (one concrete door, not an unknown), but real.
Refs: #10025 (the ruled card, blocked on this) · #9384 (the prior ruling on this exact union, cited in-repo) · #9889 (the parity floor that stays underneath) · #10413 → #10576 (the precedent for this split) · ADR-0112
Filed by the
domain:servicesexecution seat (sessionsession_01APWX2AwT3a4xDcjPCe8bk4) executing a maintainer ruling, not requesting one. Unassigned; grading and lane confirmation are triage's.Why this card exists
#10025 was ruled 2026-08-20: Option B, taken WHOLE — a definition-level input-schema refusal becomes non-retryableand receives its own ADR-0112
codeas a never-dispatched exit. The facet block the ruling adopted is explicit that half of B is worse than none:The services half (making
execute()'s catch short-circuit beforeretryExecution) is this lane's. Thecodehalf is not — it requirespackages/spec, whichdomain:serviceshas zero ownership of.⇒ Same split, same reason, same shape as #10413 → #10576: the contract half goes to the spec seat first; only then does the services half proceed.
The three edits, measured by reading source — not inferred
packages/spec/src/contracts/automation-service.ts:256—AutomationResult.codeis a closed union with no slot for a new member:packages/spec/src/api/error-code-ledger.zod.ts—ERROR_CODE_LEDGER['@objectstack/runtime'](FLOW_DISABLED~:270,FLOW_NO_START_NODE~:291). The file's own header states an unregistered code failsApiErrorSchemaparse, which fails CI ⇒ a fourth row is a ledger registration, i.e. a spec edit. Registration also carries its own required TSDoc justification per the ledger's rules.packages/runtime/src/flow-dispatch-status.ts— not insidepackages/spec, but gated behind (1)+(2). ItsFlowRefusalCode(~:98) is'FLOW_DISABLED' | 'FLOW_NO_START_NODE' | 'FLOW_FAILED', and the file carries this comment at ~:95:⭐ This exact union already has a ruling, and the codebase cites it
#9384 (closed, 2026-08-17) litigated this same union when
FLOW_FAILEDneeded a home. Its dev proved widening it is apackages/specedit by reverse verification — adding the string movedtscfrom 3 to 4 errors — and it was ruled the spec seat's call.⇒ The prohibition is not this seat's reading of the rules. It is written in the repo, at the call site, naming the prior ruling. That is why #10025's dev stopped with zero code rather than reaching for a workaround.
Suggested shape (triage/spec seat decides; this seat is not prescribing)
Mirror
FLOW_DISABLED/FLOW_NO_START_NODEexactly — same union, same ledger section (@objectstack/runtime), same never-dispatched semantics. A working name from #10025's investigation isFLOW_INPUT_SCHEMA_INVALID; ⛔ the spec seat owns the name, and the services half will assert whatever value is registered.The diff is genuinely small once the decision is made: one union member, one ledger row (with its TSDoc justification), one
FlowRefusalCoderow. The cost here is the decision and the governed surface, not the code.What the services half will do once this lands
execute()'s catch (packages/services/service-automation/src/engine.ts~:3381-3463) short-circuits before theerrorHandling?.strategy === 'retry'branch, returning the new never-dispatched exit instead of callingretryExecution.validateNodeInputSchemasitself is NOT touched. It stays the bug(service-automation): node input-schema validation guards only the FIRST attempt — a retry attempt runs the nodes attempt 1 refused to run #9889 chokepoint, called identically fromexecute()(:3319) andexecuteWithoutRetry()(:6616). Its own doc comment cites six prior drift incidents arguing against touching that call site — the fix point is downstream of the guard, not the guard.status: 'failed'and nocode. Under the ruling it must stop stampingstatusat all, matching howFLOW_DISABLED/FLOW_NO_START_NODEare emitted (engine.ts:3176-3183). ⇒ In the services test this is an existing, deliberately-worded assertion that gets inverted, not a new one added alongside — which is why it is not a safe additive change.Operator-visible consequence, stated up front
Retry accounting and run-log volume change for affected flows: today the guard's refusal produces N identical failed rows (1 +
maxRetries); after B it produces one. The ruling requires the changeset to say so plainly..listRuns(sites,88 test-only; the single production call site ispackages/runtime/src/domains/automation.ts:1498, the genericGET /api/v1/automation/:name/runshandler — a pure pass-through with no threshold logic, though its own comment (:1477) names "a monitoring caller paging for failures" as an anticipated consumer. Zero in-repo dashboards keyed onsys_automation_runrow counts. The QA checklist's retry-count oracle (docs/qa/platform-checklist/areas/automation.json:628-631) targets a genuinely-transient downstream failure — the counter-direction case — and is unaffected.⛔ Not closed: that inventory is repo-local.
objectuiandcloudwere out of reach from that checkout, so an external Studio "Runs" view or a customer integration pollingGET /runsremains the one unmeasured class — narrower than the original confidence gap (one concrete door, not an unknown), but real.Refs: #10025 (the ruled card, blocked on this) · #9384 (the prior ruling on this exact union, cited in-repo) · #9889 (the parity floor that stays underneath) · #10413 → #10576 (the precedent for this split) · ADR-0112