Skip to content

Commit 0257092

Browse files
committed
docs(spec): ApprovalDecisionResult records what the stranded-decision throw carries
The contract already declared the posture: a decision that finalises a flow-bound request and cannot resume its own run throws rather than answering `resumed: false`, because a recorded decision whose flow never advances is #4420's zombie half-state. What it never said is that the throw is now readable. The maintainer ruling #13807 (2026-09-04, decision batch #37) owes both halves — "throws with the decision and run identified; the fields are the published way to read it" — and only the first half was written down. The `resumed` docblock now names the second: the 500-class `RESUME_FAILED` carries `finalized`, `decision`, `runId` and `repairable` on its ERROR body as `StrandedDecisionDetails` (`@objectstack/types`, attached by `strandedDecisionFailure`, read back by `strandedDecisionDetails`, merged into the response by the REST approvals door), and the status code does not move because a durable decision over a run that will not advance is still a failure. Prose only. No member is added: the four facts ride the ERROR, so putting them on this success shape would declare a shape that never carries them, and the docblock says so explicitly. It also keeps them distinct from `resumeFailure`, which reports the #16472 family's different event — a resume failure told behind an answer that still succeeded. The changeset is owed on the second half of the two-part test, not the first: `src/contracts/approval-service.ts` is outside `packages/spec`'s `files[]`, but the changed docblock text is emitted into `dist/contracts/index.d.ts` and `index.d.mts`, which `files[]` publishes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
1 parent 50dc214 commit 0257092

2 files changed

Lines changed: 31 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`ApprovalDecisionResult`'s contract docblock now records what the stranded-decision throw carries — execution item 2 of the #13807 ruling (maintainer 2026-09-04, decision batch #37).
6+
7+
The posture itself was already declared: a decision that finalises a flow-bound request and cannot resume its own run throws rather than answering `resumed: false`, because a recorded decision whose flow never advances is #4420's zombie half-state. What the docblock did not say is that the throw is now *readable*. The ruling's item 2 owes both halves — "throws with the decision and run identified; the fields are the published way to read it" — and only the first half was written down.
8+
9+
- **Prose only. No member moves.** `finalized`, `decision`, `runId` and `repairable` ride the ERROR body of the 500-class `RESUME_FAILED`, as `StrandedDecisionDetails` (`@objectstack/types`, attached by `strandedDecisionFailure`, read back by `strandedDecisionDetails`, merged into the response by the REST approvals door). Adding them to `ApprovalDecisionResult` would declare a success shape that never carries them, so the docblock names them where they actually live and says so explicitly.
10+
- **The status code does not move either.** A durable decision over a run that will not advance is still a failure; what the ruling changed is that a caller holding the status code alone no longer has to read a bare 500 as "the decision did not happen" when the row is terminal.
11+
- **Kept distinct from `resumeFailure`.** That member is the #16472 family's different event — a resume failure told *behind an answer that still succeeded* — and the new paragraph says so, so the two carriers are not read as one.

packages/spec/src/contracts/approval-service.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,26 @@ export interface ApprovalDecisionResult {
735735
* whose parent then stranded (#15556) still answers `true`, and the
736736
* parent's strand is told on {@link resumeFailure} — carried on the
737737
* success answer per the #16472 ruling, never thrown.
738+
*
739+
* What that throw carries is published, not prose only (#13807, maintainer
740+
* ruling 2026-09-04, decision batch #37). The status code does not move — a
741+
* durable decision over a run that will not advance is still a failure —
742+
* but the 500-class `RESUME_FAILED` it raises names, on its ERROR body, the
743+
* four facts a caller needs: `finalized` (the decision stands), `decision`,
744+
* `runId`, and `repairable` — the engine's own `'stranded'` discriminator
745+
* carried through, never inferred from the message text. That envelope is
746+
* `StrandedDecisionDetails` (`@objectstack/types`), attached by
747+
* `strandedDecisionFailure` and read back by `strandedDecisionDetails`; the
748+
* REST approvals door merges it into the `RESUME_FAILED` response body.
749+
* Those four facts are the published way to read the posture this member
750+
* declares — a caller holding only the status code reads a bare 500 as "the
751+
* decision did not happen", and the row IS terminal.
752+
*
753+
* ⛔ They are not members of this result and must never be added to it:
754+
* they ride the ERROR, so declaring them here would declare a success shape
755+
* that never carries them. ⛔ Nor are they {@link resumeFailure}, which
756+
* reports the other event of the #16472 ruling — a resume failure behind an
757+
* answer that still succeeded.
738758
*/
739759
resumed?: boolean;
740760
/**

0 commit comments

Comments
 (0)