Repository navigation
feat(spec,automation): FlowRuntimeState.unboundCause classifies an unbound flow row beside its reason - #23854
Conversation
…bound row beside its reason FlowRuntimeState gains an optional, closed unboundCause (FLOW_UNBOUND_CAUSES: deployment_policy, disabled_subflow, missing_trigger, binding_failure), stamped by AutomationEngine.describeUnboundReason on exactly the rows that carry reason, so a consumer classifies without parsing the sentence. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PU9ijL7gdKme3dFxWKcBE6
…r unboundCause Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PU9ijL7gdKme3dFxWKcBE6
📓 Docs Drift CheckThis PR changes 2 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 142 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 faf408523e023382f18de65b456799189eb85add && git checkout faf408523e023382f18de65b456799189eb85add
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1eac7ad751e833115544be405993efced6a726d3 7ba22e988d571270ebb9faabd54ff1486e759bdf && git checkout -B drift-repro 1eac7ad751e833115544be405993efced6a726d3 && git merge --no-ff 7ba22e988d571270ebb9faabd54ff1486e759bdf
node scripts/docs-audit/affected-docs.mjs --json 1eac7ad751e833115544be405993efced6a726d3
|
席位核对 · ACCEPT · PR #23854 @
|
Contract reviewWritten: 2026-10-16T09:38Z Served-tier: Inputs: card #23842 (body, claim Gate verdicts, read 2026-10-16T09:35Z: 41 check-runs on the head, 35 ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
Fixes #23842
Clause-②: yes
What
FlowRuntimeState(packages/spec/src/contracts/automation-service.ts) gains an optional, closedunboundCausebesidereason. Its values are published as the tupleFLOW_UNBOUND_CAUSES, with the derived typeFlowUnboundCause, so a consumer can enumerate them. This follows theAPPROVAL_STATUSES/ApprovalStatusconvention in the same contracts folder.reasonstays the rendered sentence. Its docblock still says "do not parse it", and now points tounboundCausefor anyone who needs to classify the row.AutomationEngine.describeUnboundReason(packages/services/service-automation/src/engine.ts) is the single producer ofreason. Both the status door andgetTriggerBindingAudit()read it. Each of its branches now returns{ reason, cause }, andgetFlowRuntimeStates()spreads the two together, so on every row they are either both present or both absent. Neither the sentences nor the audit's shape changed.The measured set of causes (4, not the card's example 2)
I read every branch of
describeUnboundReasonat3fed1eb00. It composes four distinct causes, each with its own owner and remedy:deployment_policypolicyDisabledFlows(the recordedScheduledWorkPolicyrefusal)disabled_subflowsubflowDeclinedFlows(ADR-0126 §7.3 arming gate)missing_triggerrequires: ['triggers']/ the trigger packagebinding_failureNone of them is merged with another, because no two share a remedy.
missing_triggerandbinding_failureboth have an engineering owner, but one is fixed by adding a dependency and the other by reading a stack trace. There is no catch-allother: the branch set is closed.Wire mirrors
GET /automation/_status(packages/runtime/src/domains/automation.ts) passesgetFlowRuntimeStates()rows through verbatim and is typed against the contract, so it needs no change. No zod schema inpackages/spec/src/apimirrorsFlowRuntimeState: I searched fortriggerTypeand_statusand found only a docblock mention. I regenerated theapi-surface/contracts.jsonandexport-origins/contracts.jsonbaselines withgen:api-surfaceandgen:export-origins.check:generatedexit 0.Tests (head
7ba22e988d)engine.test.ts: the status row fordeployment_policy,binding_failurefor a bind that throws, andmissing_triggerin the two-doors test.subflow-guard-every-door.test.ts:disabled_subflow, and that the cause clears when the subflow is re-enabled and the caller arms.reasonorunboundCause.FlowRuntimeState['unboundCause']to the tuple union. A runtime test checks that the values are unique and snake_case with noother, and that the docblock still forbids parsingreasonand points atunboundCause.pnpm --filter @objectstack/service-automation exec vitest run: 186 files / 2501 tests passed. Specautomation-service.test.ts: 18 passed.typecheckfor both packages: exit 0.return { reason: policyReason, cause: 'deployment_policy' };withreturn { reason: policyReason } as any;throughscripts/ablation-replace.mjs(anchor x1 → x0, blob changed).engine.test.tswent red on 2 pins: "OFF: the status row names the policy…" and "the two doors answer the SAME sentence…". After the restore, the blob equals HEAD (4c735c08…) andgit diff HEADis empty. No build was needed, because the suite imports./engine.jsfrom source.Gates
dispatch-gates --commandson this diff gives 90 derived families.--ranreconciliation: 89 run, 1 NOT MEASURED. Every family I ran exited 0.check:plugin-teardown-shape --self-testexited 3 at first on the shallow clone and exited 0 aftergit fetch --unshallow. NOT MEASURED:check:dual-build-cjs-loads, which exited 3 because it needs a fullpnpm build. That is left to CI.check:dev-prereqs: exit 0.Acceptance notes
getTriggerBindingAudit()entries still carry onlyreason. Addingcausethere would widen a second surface the card does not name, so I did not.unboundCause === 'deployment_policy'once this ships in a published@objectstack/spec. The seat files that card. objectui is not touched here.🤖 Generated with Claude Code
Generated by Claude Code