diff --git a/.changeset/scope-resubmit-discriminator-invariant.md b/.changeset/scope-resubmit-discriminator-invariant.md new file mode 100644 index 0000000000..14866f7ff6 --- /dev/null +++ b/.changeset/scope-resubmit-discriminator-invariant.md @@ -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. diff --git a/packages/plugins/plugin-approvals/src/approval-service.ts b/packages/plugins/plugin-approvals/src/approval-service.ts index 0f4aacb530..162b1b825b 100644 --- a/packages/plugins/plugin-approvals/src/approval-service.ts +++ b/packages/plugins/plugin-approvals/src/approval-service.ts @@ -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, }); diff --git a/packages/plugins/plugin-approvals/src/stranded-resubmit-second-door.test.ts b/packages/plugins/plugin-approvals/src/stranded-resubmit-second-door.test.ts index fb3c789581..77ca46653c 100644 --- a/packages/plugins/plugin-approvals/src/stranded-resubmit-second-door.test.ts +++ b/packages/plugins/plugin-approvals/src/stranded-resubmit-second-door.test.ts @@ -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