Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/scope-resubmit-discriminator-invariant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@objectstack/plugin-approvals': patch
---

Correct the `resolveRecordedContinuation` discriminator's stated invariant in
`approval-service.ts` to what was measured. The comment claimed the
`action: 'resubmit'` audit row was "at most one per request"; a `resubmit` whose
own resume strands opens no next round, so the row stays `returned` and a second
`resubmit` after `restoreConsumedSuspension` lands a second such row. The
comment now records that more than one row can exist, states why the read is
correct anyway (it is a presence check with `limit: 1`, deciding identically on
one row or two), and points at the pin that measured it.

Prose only — no behaviour change, no door narrowed, no guard touched. The audit
trail's one-row-per-advancement shape is accepted residue; requiring one row per
advancement is a separate change.
28 changes: 24 additions & 4 deletions packages/plugins/plugin-approvals/src/approval-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4874,11 +4874,31 @@ export class ApprovalService implements IApprovalService {
// fallback (#4414) warns and evaluates every out-edge, so the flow
// proceeds with `{decision:'revise'}` where `{resubmitted:true}` was owed.
//
// The discriminator is exact and structural: `action: 'resubmit'` has
// exactly ONE writer in this file (`resubmit`), it is inserted before
// that resume, and a resubmit opens the next round as a NEW row — so at
// most one such action row exists per request, and its presence means
// The discriminator's first two clauses are exact and structural:
// `action: 'resubmit'` has exactly ONE writer in this file (`resubmit`),
// and it is inserted before that resume. Its presence therefore means
// the last continuation this row issued was the resubmit.
//
// ⚠️ What does NOT hold is the third clause this comment used to claim —
// "a resubmit opens the next round as a NEW row, so at most one such
// action row exists per request". A resubmit whose own resume STRANDS
// opens no next round at all, so the row stays `returned`; once an
// operator re-arms the pause with `restoreConsumedSuspension`, a second
// `resubmit` by the same submitter passes every door guard and writes a
// SECOND `action: 'resubmit'` row. ⇒ MORE THAN ONE such row CAN exist
// for one request. Measured guard by guard, on that very row in that
// very state, in `stranded-resubmit-second-door.test.ts` (#17601 probe,
// PR #17613) — read it there rather than re-deriving it from here.
//
// The read below is correct anyway, for a reason that clause never
// needed: it is a PRESENCE check (`limit: 1`), so it decides identically
// on one row or two — the pin's MEASUREMENT C drives this resolver on
// the doubled row and it still answers `resubmit`. What the doubling
// costs is the audit trail's one-row-per-advancement shape, ⛔ not the
// edge picked here, and that cost is ACCEPTED RESIDUE under the #17601
// ruling of 2026-09-11 (option B: scope this prose to what was measured,
// narrow no door). Requiring one row per advancement is a new card, ⛔
// not a local fix here.
const resubmitted = await this.engine.find('sys_approval_action', {
where: { request_id: requestId, action: 'resubmit' }, limit: 1, context: SYSTEM_CTX,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
* this behaviour, THIS PIN is the thing to update, and its update is the
* repair's evidence.
*
* ⭐ THAT RULING LANDED (#17601, 2026-09-11): option B — the discriminator's
* prose in `approval-service.ts` was scoped to this measurement and NO door was
* narrowed, so the doubling measured below is ACCEPTED RESIDUE and every
* assertion in this file stands exactly as it was.
*
* ## The hypothesis, and what it tested
*
* `resolveRecordedContinuation` discriminates the two continuation issuers of a
Expand Down
Loading