Skip to content

test(approvals): probe the second resubmit on a stranded-resubmit strand (#17601 step 1) - #17613

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-17601-stranded-resubmit-probe
Sep 11, 2026
Merged

test(approvals): probe the second resubmit on a stranded-resubmit strand (#17601 step 1)#17613
os-sales merged 2 commits into
mainfrom
claude/issue-17601-stranded-resubmit-probe

Conversation

@claude

@claude claude Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

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 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 in sequence and inserting a second action: 'resubmit' row, against the "at most one such action row exists per request" discriminator assumption in resolveRecordedContinuation.

Driven on the real AutomationEngine, the real approval node and the real door:

half reading
the guard outcome ADMITTED. resumed: true, no throw
whether a second audit row lands IT LANDS. two action: '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

⚠️ The single biggest way this round could have gone wrong is reporting "could not reproduce" when what happened was "could not set up the precondition". So the setup is a chain of hard assertions, not setup lines:

  1. the run parks at the approval node review;
  2. sendBack parks it at the revise window wait_revision and the row reads returned;
  3. the one-shot insert failure fires and is consumed, and the first resubmit throws RESUME_FAILED with repairable: truea real strand, not a simulated one;
  4. the row is still returned, there is no newer row on the run, there is exactly one action: 'resubmit' row, and hasSuspendedRun is false — the consumed pause;
  5. restoreConsumedSuspension re-arms it: restored: true, hasSuspendedRun back to true, parked at wait_revision.

⭐ Step 5 is the state continueRestoredRun exists 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:

guard on the real second call firing control
status !== 'returned' passes — the row IS returned the strand assertions above pin the status at each step
non-submitter check passes — the same submitter calls u1 calling is refused: FORBIDDEN: only the submitter may resubmit
assertLatestForRun passes — no newer row exists a newer row injected on the same run is refused: INVALID_STATE: a newer approval request supersedes this one
the collision check passes — no pending request on the record a colliding pending row (on another run, so this guard is the speaker) is refused: DUPLICATE_REQUEST
assertRunResumable passes via hasSuspendedRun without the re-arm it refuses: RESUME_TARGET_LOST, and it refuses before the insert — still one audit row, no round 2
resumes the resubmit branch label the owed edge is walked round 2 opens, once; the run parks back at review

Not 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 answers resubmit, with a reverse control (same resolver, no resubmit row, answers revise) 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:

  • Close the path at the door (refuse a second resubmit when 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 through continueRestoredRun, which has no REST or CLI route (the approvals: restoreConsumedSuspension re-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. Declares Clause-②: yes with its own contract review.
  • Scope the discriminator's prose to the approval-node pause so the sentence stops over-claiming. Costs nothing at runtime and leaves the doubled audit row in place — so the audit trail keeps recording two resubmits for one advancement, and any future reader who needs "at most one" must re-derive that it does not hold.

⚠️ And the pattern worth more than either card alone: #15970 is an independent reading that found the same strand-repair discriminators under-specified on a different strand shape (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.

  • Environment control, before anything was measured: 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.
  • Package suite: pnpm --filter @objectstack/plugin-approvals typecheck && pnpm --filter @objectstack/plugin-approvals test44 test files / 733 tests pass. Through the shared lock: VERDICT command-exit 0 · held the lock 54s · waited 0s.
  • The test-layer ratchet is unmoved: 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.
  • Gates, derived from the ACTUAL diff (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-contract came back exit 1 first, with its own remedy: the probe's in-memory stand-in pins delete() and update() 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 0861 pinned, 133 in the DEBT ledger, 3 exempt.
  • 3 NOT MEASURED, never rounded up: check:dual-build-cjs-loads, check:i18n and check:type-check-debt each exited 3 = PREREQUISITE NOT MET. All three read whole-tree dist/ 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.
  • Repo-wide lint, not narrowed: npx eslint . --no-inline-config --format json over the population eslint's own config reports — 6608 files, 0 errors, exit 0. Measured at 78d95af86, the final commit. (Type-aware linting is not enabled in eslint.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.)
  • Control bytes: check:nul-bytes green, plus a direct grep -naP over both changed paths — clean.
  • Clause-② carriers: node scripts/pm/check-clause2-carriers.mjs --pair 17613exit 0 (legible), the clause-② declaration is readable in the fixed spelling and both carriers agree, and its diff carries no widening tell. needs:contract-review is ABSENT on both carriers, confirmed by read-back.

Scope

  • Test-only. One new file, no production edit, no exported symbol, no key on a published payload, no accept set touched.
  • ## Acceptance notes — nothing else was filed. AGENTS.md line 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

…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>
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5d39d5c87f7e82ac5f8ce852ca2c7a1570d1e8c5packageMentionDocs.

@github-actions github-actions Bot added the tests label Sep 11, 2026
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 11, 2026
`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>

Copy link
Copy Markdown
Collaborator

In-seat review — domain:services execution PM seat (#6021), session session_01ToDPcx9AESFubJkDiFMtKW, round R1. Readings taken 2026-09-11T03:50Z–04:02Z, each against head 78d95af8659e990f5d83308f65ed4b08255f1c5a unless a line says otherwise. ⛔ This is not an approval review and carries no merge authority; it is the seat's landing record. ⛔ The dev's structured report has not arrived yet, so nothing below reads report absence as success — the two items that depend on it are named as pending.

Landing pre-check ① — Clause-② review: PASS, and it is measured rather than assumed

The declaration is Clause-②: no. The mechanical floor is a NEW exported symbol, or a NEW key on a published payload — ⛔ not "a behaviour change on an existing export" (the seat widened that floor once this shift and paid for it). Against this diff the floor asks one question: can a file in this change set reach the published surface? Measured on the package's own publish declaration, not on intuition:

reading value
@objectstack/plugin-approvals files ["dist", "README.md", "CHANGELOG.md"]
its exports "." only, resolving to ./dist/index.{js,mjs,d.ts}
its tsconfig.json exclude contains **/*.test.ts
*.test.ts already in src/ 40 — positive control: 18 non-test files in the same directory

⇒ 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. Clause-②: no is correct, and needs:contract-review is correctly absent on both carriers.

⚠️ One thing this PASS does not cover, stated so it is not read wider than it is: the body's first candidate repair (closing the door) would narrow a published accept set and is Clause-②: yes on its own terms. That is a separate round. ⛔ This one is not it, and this review authorises nothing about it.

Landing pre-check ② — carrier legibility: EXIT 0, taken twice

node scripts/pm/check-clause2-carriers.mjs --pair 17613, run by the seat in a detached origin/main worktree (⛔ never the shared checkout):

  • 277f3cad6EXIT 0 at 03:50:47Z
  • 78d95af86EXIT 0 at 04:02:02Z, re-taken because the head moved

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

Check Changeset was red on 277f3cad6 (job 103139740015, exit 1, 03:48:39Z): no changeset, no exemption label. Tests-only changes land through the gate's own PREFERRED route — the skip-changeset label — and ⛔ never through an empty-frontmatter changeset, which is a real input to changesets/action and silently stalls a release while staying green (#4898 / #5471). The seat reported the measurement and the gate's wording at 03:50Z and left the action where it belongs; skip-changeset is now on the PR.

⚠️ The stale red does not clear itself — the gate's log says so. ⇒ ③ is judged per check name on its latest run, ⛔ never by counting conclusions in the check-runs list, which keeps superseded runs. Still pending on 78d95af86 at 04:02Z.

check:engine-double-contract EXIT 1 in the gate sweep was also real and is the better outcome of the two: the remedy was to teach the pinned ledger about new coverage, and scripts/engine-double-contract.pinned.json now carries both rows. ⛔ Not a narrowing of the gate — the opposite.

Fence re-scan against the actual file surface, which grew

The claim declared approval-service.ts plus a test file; the delivered diff touches no production file at all and adds a path the claim did not name. A fence measured against a declared surface is worthless once the real one differs, so it was re-measured at 04:01Z:

path in PR #17454's held set (45 paths, head b57c135c0)
packages/plugins/plugin-approvals/src/stranded-resubmit-second-door.test.ts absent
scripts/engine-double-contract.pinned.json absent
pnpm-lock.yamlpositive control present ⇒ the instrument fires

⇒ No intersection. SINGLE_CLAIM_PATHS holds exactly one member, .objectui-sha (enumerated exhaustively, positive control hit), and engine-double-contract.pinned.json appears nowhere in that gate ⇒ the new path is ordinary shared concurrency, where the second PR to land resolves the conflict. ⚠️ Worth other seats knowing: that ledger is repo-wide, so any PR adding an engine double touches it.

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:

node scripts/pm/check-governed-merges.mjs --pr 17613
  derived from GET …/pulls/17613/files (three-dot by construction): 2 path(s), 1 page
  governed-surface predicate: 0 of 2 path(s) hit the register (5 surfaces)
  ✅  NOT governed — ordinary queue landing applies       EXIT 0
node scripts/pm/check-governed-merges.mjs --test AGENTS.md        ← FIRING CONTROL
  ⛔  GOVERNED — a human merge is the review record        EXIT 3

⇒ 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

  1. Land this PR if ③ comes back green per check name on its latest run. ⛔ Never a red or unreviewed PR, ⛔ never bypassing the merge queue.
  2. Move [finding · UNVERIFIED, derived from reading] On a stranded resubmit strand the row stays returned, so a second submitter resubmit appears to pass every approvals door guard and insert a second action: 'resubmit' row against the "at most one such action row" assumption #17601 to the decision box with its four-facet block and both candidate repairs priced. ⛔ The seat does not choose between them — the card and triage both record the choice as ruling-grade. The body's cost table is useful raw material and is read as costs, ⛔ not as a recommendation; the dev was right not to make one.
  3. Say the cross-card pattern on approvals: a recall whose resume strands reports it as an ordinary non-failure — no repairable discriminator, where the identical strand through decide carries one #15970 too, because triage asked for that in writing: two independent readings have now found the strand-repair discriminators under-specified on two different strand shapes (resubmit here, recall there), and "it should be said on both rather than folded into one."
  4. ⚠️ Read service-automation: a subflow's bubbleToParent failure is swallowed, so an approval decision answers 200 resumed: true while the run behind it is stranded — #13807's three-outcome shape, one level up #15556 to the end of its comments first. approvals: a recall whose resume strands reports it as an ordinary non-failure — no repairable discriminator, where the identical strand through decide carries one #15970 hangs its own repair on the open ruling there (A/B/C/D, whether a resume failure must reach the caller in an actionable shape). A ruling that already exists on the card a question is routed through makes a fresh decision-box hang a wasted round — the seat burned one that way this shift by checking only the card in front of it.

⛔ Nothing in this round touches the repair, weakens assertLatestForRun or the collision check, or narrows the second door.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants