test(approvals): probe the second resubmit on a stranded-resubmit strand (#17601 step 1) - #17613
Conversation
…trand #17601 recorded a hypothesis derived from reading and explicitly not executed: on the third strand shape `continueRestoredRun` repairs — a stranded `resubmit` paused at the `approval_revise` node — the row is left `returned` with no newer row, so a second `resubmit` by the same submitter was read as passing every door guard and writing a second `action: 'resubmit'` row, against the "at most one such action row per request" discriminator assumption in `resolveRecordedContinuation`. Driven on the real engine and the real door, it reproduces. Once an operator re-arms the consumed pause with `restoreConsumedSuspension` — the state `continueRestoredRun` exists to serve — the second call is admitted, resumes, and the request carries TWO `action: 'resubmit'` rows. The pin records the measurement with a firing control on each guard the card reads as "passes" (submitter-only, the collision check and the supersede check each shown refusing on that very row in that very state), an un-re-armed control showing `assertRunResumable` refusing before any write, and a bound: the discriminator's own read is a presence check, so two rows still decide `resubmit` — what is falsified is the stated invariant and the audit trail's one-row-per-advancement shape, not the edge the repair verb reads on today's code. No production code is changed. The repair choice is ruling-grade and the card routed it to the decision box. Claude-Session: https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
`pnpm check:engine-double-contract` named this directly: the probe's in-memory ObjectQL stand-in routes `delete()` and `update()` through ObjectQL's own dispatch predicates, so the gate holds the pins — but the ledger has to record them or it never protects the file. Produced by `node scripts/check-engine-double-contract.mjs --write`; the diff is two rows naming only the new test file, pure insertions, and the shrink-only baseline is untouched. Claude-Session: https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW Co-authored-by: Claude <noreply@anthropic.com>
|
In-seat review — Landing pre-check ① — Clause-② review: PASS, and it is measured rather than assumedThe declaration is
⇒ If a test file could reach the published surface, those 40 would already be on it. Nothing in this change set adds an exported symbol or a key on a published payload, and no accept set moves.
Landing pre-check ② — carrier legibility: EXIT 0, taken twice
The re-take is not ceremony: a PASS taken on a superseded head is recall, not a reading. Verdict both times: "the clause-② declaration is readable in the fixed spelling and both carriers agree, and its diff carries no widening tell." ⭐ Measured with the gate's own reader, ⛔ never a hand-rolled regex — a regex narrower than the gate reads an existing declaration as absent, and a false absence drives a write nobody needed. Landing pre-check ③ — checks: NOT YET SATISFIED, and the one red was real
⭐ Fence re-scan against the actual file surface, which grewThe claim declared
⇒ No intersection. Governed surface — NOT governed, driven rather than recalled⛔ A managed-surface PR may not be flipped ready, enqueued, or armed by any seat without maintainer authorisation, so this was measured before any landing step rather than inferred from the paths looking ordinary: ⇒ Ordinary queue landing applies. ⭐ The control matters: a predicate that answered "not governed" for everything would answer that here too. ⭐ What this seat owes once the report lands, stated now so it is not invented later
⛔ Nothing in this round touches the repair, weakens Generated by Claude Code |
Part of #17601 — this PR delivers step 1 only, the measurement. The repair half stays open: the card records that the choice between the two candidate repairs is ruling-grade and routed it to the decision box, and triage's gate says the same. ⛔ No production code is changed here, and this PR must not be read as endorsing the behaviour it records.
Clause-②: no
VERDICT: POSITIVE — the hypothesis reproduces, both halves
#17601 recorded a hypothesis derived from reading and explicitly not executed: on the third strand shape
continueRestoredRunrepairs — a strandedresubmitpaused at theapproval_revisenode — the row is leftreturnedwith no newer row, so a secondresubmitby the same submitter was read as passing every door guard in sequence and inserting a secondaction: 'resubmit'row, against the "at most one such action row exists per request" discriminator assumption inresolveRecordedContinuation.Driven on the real
AutomationEngine, the real approval node and the real door:resumed: true, no throwaction: 'resubmit'rows on one request⇒ The third clause of the stated invariant — "a resubmit opens the next round as a NEW row, so at most one such action row exists per request" — is false on this strand shape, for exactly the reason the strand exists: a resubmit whose resume strands opens no new round.
The precondition was reached, and every step of reaching it is asserted
review;sendBackparks it at the revise windowwait_revisionand the row readsreturned;resubmitthrowsRESUME_FAILEDwithrepairable: true— a real strand, not a simulated one;returned, there is no newer row on the run, there is exactly oneaction: 'resubmit'row, andhasSuspendedRunisfalse— the consumed pause;restoreConsumedSuspensionre-arms it:restored: true,hasSuspendedRunback totrue, parked atwait_revision.⭐ Step 5 is the state
continueRestoredRunexists to serve. It is not an exotic shape invented by the probe — it is the operator repair the platform documents.Guard by guard, each "passes" with a firing control of the identical shape
A guard that passed and a guard that was never consulted read the same from the outside, so each one is shown refusing on that very row, in that very state:
status !== 'returned'returnedu1calling is refused:FORBIDDEN: only the submitter may resubmitassertLatestForRunINVALID_STATE: a newer approval request supersedes this oneDUPLICATE_REQUESTassertRunResumablehasSuspendedRunRESUME_TARGET_LOST, and it refuses before the insert — still one audit row, no round 2resubmitbranch labelreviewNot one refusal wrote an audit row — asserted, because that is what makes the audit-row count the discriminating reading between the admitted call and the refused one.
⛔ The bound, measured too, so the finding is not read wider than it is
The discriminator's own READ is a presence check (
limit: 1), so two rows decide exactly as one does: the resolver is driven on the doubled row with the journal stripped and still answersresubmit, with a reverse control (same resolver, no resubmit row, answersrevise) proving that reading is not a constant.⇒ What is falsified is the stated invariant and the audit trail's one-row-per-advancement shape. ⛔ On today's code it is not the edge the repair verb reads. Stating it the other way round would over-claim.
Two further honest limits: the flow is not doubly advanced (round 2 opened exactly once, and the round-1 row is still
returned, untouched); and ⛔ no sweep for sibling "at most one" assumptions elsewhere in the file was run, so nothing here says the rest are safe.What each candidate repair would cost — and ⛔ neither is recommended here
The card reserved this choice, so these are costs, not a preference:
resubmitwhen one is already recorded). Narrows a published accept set: a submitter whose retry is legitimate today starts getting a refusal, and a strand that an operator re-armed would then be continuable only throughcontinueRestoredRun, which has no REST or CLI route (the approvals:restoreConsumedSuspensionre-arms the pause but the approvals door cannot continue it — a restored approval suspension can only be cancelled, never decided #15389 ruling refused one) — so the cost is an in-process-only recovery path for a door users reach today. DeclaresClause-②: yeswith its own contract review.recall). Two shapes now.Verification
Ablation is not owed here — no guard was added, so there is no new refusal whose ability to fail needs proving. What the probe owes instead is a firing control on each guard it reports as passing, and those are the table above, driven on the same row in the same state.
src/restored-approval-continuation.test.ts— the existing suite for this very mechanism — 9/9 pass in this checkout. A probe in an environment that cannot reach the pause measures nothing, and this is the reading that says it can.pnpm --filter @objectstack/plugin-approvals typecheck && pnpm --filter @objectstack/plugin-approvals test— 44 test files / 733 tests pass. Through the shared lock:VERDICT command-exit 0 · held the lock 54s · waited 0s.check:test-typecheck: OK — 8 file(s) / 324 error(s) / 27 pinned signature(s) held in test-typecheck-debt.json. The new file carries zero errors and is therefore unledgered, which is what that ratchet requires of a file it does not list.node scripts/pm/dispatch-gates.mjs --commands, re-derived after the second path landed: 55 then 63 families) and reconciled:✓ dispatch-gates --ran: 63 derived famil(ies) accounted for — 60 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3).pnpm check:engine-double-contractcame back exit 1 first, with its own remedy: the probe's in-memory stand-in pinsdelete()andupdate()to ObjectQL's dispatch predicates and the pinned ledger had to record them. Regenerated with--write(two rows naming only the new file, pure insertions, shrink-only baseline untouched) and it re-runs exit 0 —861 pinned, 133 in the DEBT ledger, 3 exempt.check:dual-build-cjs-loads,check:i18nandcheck:type-check-debteach exited 3 = PREREQUISITE NOT MET. All three read whole-treedist/and refuse without a repo-wide build closure; a test-only diff moves neither, and that closure is CI's run, not this PR's. ⛔ Not a pass and ⛔ not a finding.npx eslint . --no-inline-config --format jsonover the population eslint's own config reports — 6608 files, 0 errors, exit 0. Measured at78d95af86, the final commit. (Type-aware linting is not enabled ineslint.config.mjs, so no unlinted file's verdict could have moved either way — but the whole population was read, so that invariance is not load-bearing.)check:nul-bytesgreen, plus a directgrep -naPover both changed paths — clean.node scripts/pm/check-clause2-carriers.mjs --pair 17613→ exit 0 (legible),the clause-② declaration is readable in the fixed spelling and both carriers agree, and its diff carries no widening tell.needs:contract-reviewis ABSENT on both carriers, confirmed by read-back.Scope
## Acceptance notes— nothing else was filed.AGENTS.mdline 440-444's trailer pair is model-free on the commit.🤖 Generated with Claude Code
https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW
Generated by Claude Code