Skip to content

feat(spec): declare continueRestoredRun on the IApprovalService contract - #17567

Merged
os-bill merged 5 commits into
mainfrom
claude/issue-15389-approval-service-continue-restored
Sep 11, 2026
Merged

os-bill merged 5 commits into
mainfrom
claude/issue-15389-approval-service-continue-restored

Conversation

@os-bill

@os-bill os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Fixes #15389

Clause-②: yes

Declares the operator repair verb continueRestoredRun on the spec's IApprovalService contract, as an optional member. This is the contract half of the card; PR #15966 landed the class member on plugin-approvals as Part of and is not re-implemented here.

The ruling this executes

Maintainer, 2026-09-09, verbatim 「#106 同意」 on decision batch #106 whose item 3 was A:

continueRestoredRun is declared on the spec's IApprovalService contract as an optional member, with the shape and docblock discipline of IAutomationService.restoreConsumedSuspension / cancelRun (#16495 — including the 「a repair verb that will refuse is worse than promising nothing」 note).

B (class-only) and C (a new REST/CLI door) were refused explicitly. Neither is re-opened here: no route is added, and the shape is copied rather than invented.

What is in the diff

  • packages/spec/src/contracts/approval-service.ts — the optional member, its structural result declared inline, and a docblock following the [spec] Declare cancelRun and restoreConsumedSuspension on IAutomationService — #13953's ruled contract half, step (1), never filed #16495 pair: what it replays and what it deliberately does not, why it takes no ExecutionContext, that no door is declared, and the optionality rule ending in the promising-nothing note.
  • packages/spec/src/contracts/approval-service.test.ts — the conformance pin, shaped as the sibling of automation-service.test.ts's [#16495] block: exported type-level identity aliases for the parameters and the result, the optionality pin, a signature-carry pin, a @ts-expect-error pin for the required source, and a docblock-discipline pin.
  • packages/plugins/plugin-approvals/src/approval-service.ts — comment only. Its posture paragraph claimed this verb has "no entry in the spec ApprovalService contract" and that restoreConsumedSuspension "appears in no contract". The first becomes false with this change; the second became false with [spec] Declare cancelRun and restoreConsumedSuspension on IAutomationService — #13953's ruled contract half, step (1), never filed #16495. Corrected to state the ruled posture instead: both verbs are declared, optional, and a caller reaching either through the contract must probe for presence and refuse fail-closed.
  • .changeset/spec-approval-continue-restored-contract.md@objectstack/spec: minor.

Two decisions inside the ruled shape, stated rather than buried

The result is declared inline, not exported. restoreConsumedSuspension does the same (#16495 route (i)): the implementation's named ApprovalContinuationResult stays with plugin-approvals. packages/spec/api-surface/contracts.json is unchanged as a result — the published export roster does not widen, only the interface member does.

source IS enumerated here, unlike restoreConsumedSuspension's refusal, and the docblock says why. refusal is the engine's own open vocabulary, which the contract would have to keep in step with. source is a closed, binary property of the replay itself (journal = the literal signal re-issued, reconstructed = rebuilt from the recorded outcome) and it is the caller's branch point: a caller that cannot tell an exact replay from an inferred one cannot say what it trusts. A third provenance would be a spec card.

The ruling's conditional pin — measured, not assumed

The ruling named scripts/engine-double-contract.pinned.json if that file governs the double. It does not. That ledger is keyed by test-file path and pins fake ObjectQL engine write-verb dispatches (delete / update); it lists no packages/spec file at all and no method names. The new pin declares no engine double. Measured rather than read: pnpm check:engine-double-contract exits 0 with the ledger RETAINED at 778 rows, unchanged.

Changeset, confirmed against the published roster

The ruling measured @objectstack/spec: minor; this round confirms it and agrees. @objectstack/spec's files[] ships dist, and the member reaches the published declaration — grep -c continueRestoredRun packages/spec/dist/contracts/index.d.ts = 1 after the build (0 before), with restoreConsumedSuspension as the lit control in the same file. Additive and optional, so minor, not major: the only implementer still conforms untouched.

Verification

Every exit code captured right after a single redirected command, never through a pipe; heavy runs serialized through scripts/pm/os-verify-lock.sh (slot dev-15389) and read from its own VERDICT command-exit line.

run result
pnpm --filter @objectstack/spec test VERDICT command-exit 0 — 471 files passed, 13370 tests
pnpm --filter @objectstack/spec typecheck exit 0 — check:test-typecheck: OK, ledger held at 54 files / 259 errors / 144 pinned signatures, nothing added
pnpm --filter @objectstack/plugin-approvals typecheck exit 0 — the implements IApprovalService conformance
pnpm --filter @objectstack/plugin-approvals test VERDICT command-exit 0 — 43 files passed, 730 tests
pnpm exec turbo run build --concurrency=2 VERDICT command-exit 0 — 74 successful, 74 total
eslint . --no-inline-config exit 0 — 6591 files linted, 0 problems (whole repo, not a narrowed run)

Gates. Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the actual change set, never hand-built: 85 commands, all 85 run, 85 exit 0. Three first answered a PREREQUISITE REFUSAL on an unbuilt workspace (check:dual-build-cjs-loads, check:i18n, check:type-check-debt, all exit 3, which the scripts themselves spell "NOT a pass: nothing was measured") and all three exit 0 after the full build; none is reported as a pass while it was a refusal. Reconciled with --ran carrying every exit code: "85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN", a DERIVED zero rather than a claimed one. The six artifact-roster families whose roster directory contains one of my paths — where the derivation says silence is evidence in neither direction — were read rather than assumed: all six exit 0.

Phantom-check guard. The @ts-expect-error pin is only a pin if a program compiles the file. tsc -p packages/spec/tsconfig.test.json --listFiles names contracts/approval-service.test.ts, with contracts/automation-service.test.ts (the #16495 pin) as the lit control in the same output.

Ablation. Direction predicted before each leg. Each anchor asserted to occur exactly once as an exact substring before anything was written; each mutation proved on disk by a git hash-object delta plus marker counts (anchor 0, injected 1), never inferred from an editor's exit code; each restored by git checkout HEAD -- ABSOLUTE_PATH under trap ... EXIT INT TERM, proved by blob equality against HEAD and an empty git diff HEAD, with an empty hash treated as failure. All legs ran against the committed tree, so the restore leg points at an implementation HEAD actually carries.

  • A1 — drop source from the contract (predicted red). check:test-typecheck exit 1: "1 problem(s) · 4 type error(s)" in the pin file — the identity alias as TS2344: Type 'false' does not satisfy the constraint 'true', and TS2578: Unused '@ts-expect-error' directive, which is the direct proof that the @ts-expect-error is live. Restored, blob back at HEAD.
  • A2 — narrow the contract's union to source: 'journal' (predicted red; this is the conformance link, and plugin-approvals resolves @objectstack/spec through exports to dist/, so the leg REBUILDS). Build VERDICT command-exit 0, ablation-dist-preflight @objectstack/spec confirms the mutation reached the built artifact, then tsc --noEmit on plugin-approvals exit 1: error TS2416: Property 'continueRestoredRun' in type 'ApprovalService' is not assignable to the same property in base type 'IApprovalService'. The restore leg rebuilt too, and the preflight --absent confirms the mutation is gone from all 216 built files while the real union is present again in 2.
  • A3 — delete the promising-nothing note from the docblock (predicted red). vitest exit 1, exactly one test red — the docblock pin — with the other 5 green, so it discriminates that sentence specifically. Restored, blob back at HEAD.

Green baseline re-run after the last leg: 6 passed, tree clean (git status --porcelain 0 lines, git diff HEAD 0 lines).

Not measured

  • CI itself. Per the dispatch contract the report is delivered at draft-PR time and CI convergence is the PM's to read.
  • The families the derivation places outside the runnable total and names as not cleared by any path: the 46 artifact-roster families (bar the six read above), the 11 declared wide-population families, the 5 path-scheduled CI jobs and the always-runs tail. Their absence from the 85 is not a clearance.

验收备注

Nothing was filed. One boundary observation, recorded and not filed: packages/plugins/plugin-approvals/CHANGELOG.md carries the same "no entry in the spec ApprovalService contract" sentence, now superseded. It is left alone deliberately — a CHANGELOG entry is the record of what a released version did and was true when written; rewriting it would rewrite history rather than correct prose. 承接者: the next release-notes pass, which reads changesets rather than that line.


Generated by Claude Code

os-bill and others added 2 commits September 10, 2026 22:06
…ontract

The approvals half of the operator repair pair. `restoreConsumedSuspension`
re-arms a consumed pause and deliberately does not replay the resume signal;
for an approval suspension nothing could re-issue the continuation, because
every front door guards on a `pending` request and the row is terminal. The
issuer exists as a class member on `plugin-approvals`; declaring it optional
on the contract puts the same verb family on the contract rather than half on
a contract and half on a class.

Shape and docblock discipline follow `IAutomationService.cancelRun` /
`restoreConsumedSuspension`: optional, structural result declared inline, and
the note that promising a repair verb that will refuse is worse than promising
nothing. No REST or CLI route is declared.

Also corrects the class docblock's posture paragraph, which claimed this verb
has no contract entry and that `restoreConsumedSuspension` appears in no
contract — both false as of this change and of #16495 respectively.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>
The assertion matched `the continuation must be` contiguously; the docblock
wraps between `the` and `continuation`, so the pin was red for the wrapping
rather than for the prose it exists to hold.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 10, 2026
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-approvals, @objectstack/spec, touching 4 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via IApprovalService (symbol, a top-level interface))
  • content/docs/automation/flows.mdx (via ApprovalService (symbol, a top-level class), /:name/runs/:runId/restore-suspension (route, a path literal in ApprovalService))
What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 135 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 abc4b83ce8b01d4e5af3560a0547421f6f5ca98fpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 2ae4128d7d6823a45254f2c9292f53e1f6d34507 — the merge of head a6ced94691c3ea0e07a3c059e09200f0ed7c7e89 into base abc4b83ce8b01d4e5af3560a0547421f6f5ca98f, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2ae4128d7d6823a45254f2c9292f53e1f6d34507 && git checkout 2ae4128d7d6823a45254f2c9292f53e1f6d34507
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin abc4b83ce8b01d4e5af3560a0547421f6f5ca98f a6ced94691c3ea0e07a3c059e09200f0ed7c7e89 && git checkout -B drift-repro abc4b83ce8b01d4e5af3560a0547421f6f5ca98f && git merge --no-ff a6ced94691c3ea0e07a3c059e09200f0ed7c7e89

node scripts/docs-audit/affected-docs.mjs --json abc4b83ce8b01d4e5af3560a0547421f6f5ca98f

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs abc4b83ce8b01d4e5af3560a0547421f6f5ca98f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Seat adoption record — the verdict below is adopted VERBATIM. domain:spec execution seat, session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-10T23:40Z. ⛔ The seat may not rewrite, trim or polish a review verdict; its only two legitimate moves are to adopt it whole or void it whole, and this one is adopted.

Tier verified from the transcript, ⛔ not from self-report. The reviewer's transcript carries 100 harness-stamped model fields and every one of them reads claude-fable-5-1 = CONTRACT_REVIEW_TIER (scripts/pm/dispatch-gates.mjs:10507); zero residue of any other model, and the 14 occurrences of the string "fallback" are all system-prompt boilerplate or code prose — no serving fallback notice. Lit control: 82 assistant messages in the same file, so the count is a reading.

⚠️ Independence, stated as a limit rather than a credential. The reviewer is a subagent commissioned by this seat and inherits this session id ⇒ in-seat at-tier review, ⛔ not cross-seat independent. It was given the card, the ruling and the PR, and ⛔ was not given the dispatch order or this seat's conclusions.

⚠️ One mechanical alteration, declared: 8 HTML entities (&lt; &gt; &amp;) introduced by the transport between the reviewer and this comment were restored to < > &, because inside a code span an entity renders literally and would have corrupted Promise<…> into noise. ⛔ No word of the verdict was changed.

Consequence for landing — the gate stays shut. The verdict is PASS WITH FINDINGS with two must-fix-before-landing items, both published prose. ⛔ needs:contract-review is NOT cleared, ⛔ this PR is not flipped ready, not enqueued, and carries no auto-merge. The round is going back for the two prose fixes; the push will move the head, which makes this verdict name an older head and the carrier genuinely live again ⇒ a narrow re-read at the new head, ⛔ not a re-litigation of what already passed.

⭐ Worth naming: the reviewer caught that the round's own correction was false — the class comment was fixed against #16495 and not against #13953, so a wrong claim about the sibling verb was replaced with a different wrong claim, in JSDoc that ships. That is exactly the failure an adversarial reader exists to find, and it is why this gate is not a formality.


Contract review (CONTRACT_REVIEW_TIER, isolated seat) — PR #17567 @ 4c145b17

  • Implemented-by: branch claude/issue-15389-approval-service-continue-restored
  • Reviewed-by: isolated subagent at CONTRACT_REVIEW_TIER, adopted by session_01MkQhmuuJAVDjmeWNixwDDH

Ruling reviewed against (card comment 5596173734, quoted, not paraphrased): "The operator repair verb continueRestoredRun (landed as a class member by PR #15966, 6530e044f) is declared on the spec's IApprovalService contract as an optional member, with the shape and docblock discipline of IAutomationService.restoreConsumedSuspension / cancelRun (#16495 — including the 「a repair verb that will refuse is worse than promising nothing」 note). No new REST / CLI door (C refused); class-only (B) refused because it leaves the same verb family half on the contract and half on a class." Execution note in the same ruling: "changeset @objectstack/spec: minor; pins: the contract test beside automation-service.test.ts's #16495 block, and scripts/engine-double-contract.pinned.json if the double registry lists approval verbs. The card closes when the contract half merges (Fixes)."

Corpus. Merge base ad715aca57 → head 4c145b1764, 4 files, +297/−4 (git diff --stat, read from refs; the PR API's base.sha f721ef0f is stale — main moved). Every reading below is from git show/git grep against the head ref or from a compile of the head tree extracted into scratchpad with git archive; nothing was taken from the working tree or from the PR body.

① Derived judgments

Each consequence of the diff, judged.

  1. Accept set — one optional method added to IApprovalService (packages/spec/src/contracts/approval-service.ts:1011-1040, member at :1011): continueRestoredRun?(requestId: string, options?: { requestedBy?: string; reason?: string }): Promise<{ resumed; runId; decision: string; branchLabel?; source: 'journal' | 'reconstructed'; resumeError? }>. Right. The shape is the [spec] Declare cancelRun and restoreConsumedSuspension on IAutomationService — #13953's ruled contract half, step (1), never filed #16495 pair's shape byte-for-byte in its parameter half (automation-service.ts:828-831 is (runId, options?: { requestedBy?; reason? })) and the result is inline, not exported — the [spec] Declare cancelRun and restoreConsumedSuspension on IAutomationService — #13953's ruled contract half, step (1), never filed #16495 route-(i) posture. It matches the landed class member's own signature and its exported ApprovalContinuationResult (packages/plugins/plugin-approvals/src/approval-service.ts:674-695, method at :5120-5123) field for field, including source: 'journal' | 'reconstructed' at :694 and the same resumeError? sentence.

  2. Accept set — the existing implementer still conforms. Right, and measured, not asserted. ApprovalService implements IApprovalService (approval-service.ts:1150). Compiled the head contract + pin in a strict program (below): green. Nothing else in the repo implements the interface: git grep for implements IApprovalService / : IApprovalService / <IApprovalService> at head finds the class and one consumer (packages/rest/src/rest-api-plugin.ts:430, ctx.getService<IApprovalService>('approvals')), which is a reader and is unaffected by an optional member.

  3. Published surface — the export roster does not move. Right. packages/spec/api-surface/contracts.json records name (kind) per export only (packages/spec/scripts/build-api-surface.ts:13, :149-154); the interface row IApprovalService (interface) is at contracts.json:108 and no new name is exported. api-surface-signatures.json hashes only define* factories (build-api-surface.ts:25-27, :163): 27 keys, 27 define* rows, 0 IApprovalService rows. The PR touches 0 files under api-surface* against a control of 2 PR files under packages/spec. The only published-surface movement is inside the emitted dist/contracts/index.d.ts interface body, which is exactly what the ruling ordered.

  4. Published surface — the class docblock in plugin-approvals changes. The PR's stated reason: the old sentence "no entry in the spec ApprovalService contract … appears in no contract" became false. The correction is itself false at head. The new text (approval-service.ts:5101-5103) reads: "an in-process operator repair, reachable from a host or a console script, with no REST route — exactly as restoreConsumedSuspension is reached." At head, restoreConsumedSuspension is reached by a REST route: POST /:name/runs/:runId/restore-suspension (packages/runtime/src/domains/automation.ts:565 RUN_RESTORE_SEGMENT, :1478 route table, :2344-2389 handler calling automationService.restoreConsumedSuspension(parts[2], …) — the service-automation: the two operator run-lifecycle verbs (cancelRun, restoreConsumedSuspension) have no door — no REST route, no CLI command, and not on IAutomationService #13953 services half, platform-operator gated at :1579). This is the sentence the PR set out to fix, and it replaced one wrong claim about the sibling with another. The rest of the paragraph (both verbs declared, optional, probe-and-refuse) is right. Wrong — must fix. This JSDoc is emitted into plugin-approvals' published .d.ts.

  5. Contract docblock — the "why this exists" paragraph (approval-service.ts:955-965) says "{@link decide}, {@link recall}, {@link sendBack} and {@link resubmit} all guard on a pending request". Wrong for resubmit, imprecise for recall. Measured at head: decidedecideNode if (raw.status !== 'pending') (plugin-approvals/src/approval-service.ts:2905); sendBackloadPendingRow (:1322); recallif (raw.status !== 'pending' && !inReviseWindow) (:3432); resubmitif (raw.status !== 'returned') (:3870). The card's own phrasing ("every one of them guards on a non-terminal request") was correct; the contract's is not. The argument survives (a rejected row is neither pending nor returned), but the published text states a guard the code does not have. Must fix — one line of prose in this PR's own hunk.

  6. Contract docblock — "⛔ it does not re-open, re-decide or rewrite the request row — no status, no mirror field and no audit row is written" (:969-971). The three enumerated negatives are true: the method body (:5124-5251) writes no status, mirror or sys_approval_action row. The headline "rewrite the request row" is over-broad: on the strand-again path resumeRecordedOutcome (:3328) calls journalStrandedContinuation (:4741-4760), which updates sys_approval_request.node_config_json. Finding, not a blocker — the class docblock (:5074-5077) makes the same over-claim; tightening it to the three enumerated negatives makes text and code agree.

  7. Contract docblock — resumeError "Set only on the tolerated non-failure: a concurrent resume already had it" (:1037). A second non-throwing path exists: missingRunCapability (:3228-3247) answers { resumed: false, resumeError } when the engine has no resume. Reachable only with an engine that carries hasSuspendedRun and listSuspendedRunsDurable (guards 2/3 pass) but no resume — degenerate, and the class type says the same sentence. Nit, recorded.

  8. Contract docblock — everything else the ruling named is present and pinned. The promising-nothing note (:1000-1003), the [spec] Declare cancelRun and restoreConsumedSuspension on IAutomationService — #13953's ruled contract half, step (1), never filed #16495 cross-reference (:951-953), the refused REST/CLI route (:987-990), "Why it takes no ExecutionContext" (:976-981), decision free-form with the StrandedDecisionDetails precedent — verified real: packages/types/src/stranded-decision.ts:77-80 reads "Free-form by design: the vocabulary belongs to the producing service … decision: string". Right. One discipline gap relative to the sibling: restoreConsumedSuspension's docblock carries a "Who may call it" paragraph (the service-automation: the two operator run-lifecycle verbs (cancelRun, restoreConsumedSuspension) have no door — no REST route, no CLI command, and not on IAutomationService #13953 platform_admin gate); this one carries no authorization posture for any future door. It cannot invent one without a ruling, and it says a route is "a new card". Right to omit; that future card must carry the gate.

  9. The conformance pin (packages/spec/src/contracts/approval-service.test.ts:86-243). Compiled for real: head tree extracted with git archive, a vitest type stub (the pin's type semantics do not depend on vitest), tsconfig.test.json semantics (strict, noUnusedLocals), TypeScript 6.0.2 against the repo's ^6.0.3 — a patch behind, noted. Program --listFilesOnly names the pin file and the contract (2 hits).

    • M0 head: exit 0, 0 errors.
    • M1 drop source from the contract (anchor counted once as a substring, md5 delta on disk): 4 errors — TS2344 at the identity alias (:103), two TS2339 in the runtime test, and TS2578: Unused '@ts-expect-error' directive at :213. Reproduces the PR's A1 exactly.
    • M2 remove the directive (two-line anchor counted once as a substring — grep -c would have been VOID here and was not used): TS2322 at :213:13, the arrow's return missing source. The directive is load-bearing.
    • M3 head pin against the base contract: 8 errors, all continueRestoredRun absent. The pin reads the contract, not itself.
    • The file is under tsconfig.test.json's include: ["src/**/*"], is named by package.json typecheckcheck:test-typecheck --project tsconfig.test.json, and has no row in packages/spec/test-typecheck-debt.json (control: src/contracts/automation-service.test.ts — the [spec] Declare cancelRun and restoreConsumedSuspension on IAutomationService — #13953's ruled contract half, step (1), never filed #16495 sibling — is ledgered with 1 tolerated TS2739), so any error in it is red. Right: a real pin in a real program.
    • The two vitest runtime tests (:143-150, :152-210) exercise hand-written object literals and would pass against any contract; the discriminating pins are the two exported aliases, the directive, and the docblock-text test. That is the [spec] Declare cancelRun and restoreConsumedSuspension on IAutomationService — #13953's ruled contract half, step (1), never filed #16495 block's shape too. Right, stated so nobody counts six pins where three discriminate.
  10. Ruling compliance on B and C. No route, no CLI, no class re-implementation: the diff's only implementation-side change is a comment. Right.

  11. Fixes #15389. Right per the ruling's own terms ("The card closes when the contract half merges"). Deliverables: the contract member ✓, its conformance test ✓ (in approval-service.test.ts, the sibling file of automation-service.test.ts — "beside the [spec] Declare cancelRun and restoreConsumedSuspension on IAutomationService — #13953's ruled contract half, step (1), never filed #16495 block" read as sibling file, which is the only sensible placement), plugin-approvals satisfying it ✓ (M0). Conditional pin: scripts/engine-double-contract.pinned.json is keyed (test file, engine write verb); 0 rows under packages/spec/, 0 rows naming any method, against a control of 26 plugin-approvals rows — the new pin declares no engine double. Not applicable, measured. Nothing the card asks for is left behind; the card's own dead-end table is answered by fix(approvals): give a restored approval suspension an issuer, so it can be decided and not only cancelled #15966's real-engine pins, not by this PR, and the ruling knowingly closes the card on the contract half.

② Semver grading

@objectstack/spec: minor — correct. Instrument: files[] ships dist and api-surface (packages/spec/package.json); the member reaches dist/contracts/index.d.ts as one optional method on an already-exported interface. Additive-and-optional: an implementer that predates it still conforms (M0; M4a below), and a caller must probe. Not major: nothing is removed, narrowed or made required; check:api-surface's breadth roster and depth hashes do not move (①.3). Not patch: the published declaration gains a member. Widening-the-roster vs additive-optional is exactly the line, and this is on the additive-optional side. Precedent agrees: #16495 landed as minor (packages/spec/CHANGELOG.md:276, abb140c). The comment-only edit to plugin-approvals needs no changeset.

③ Boundary-flag disposition

Raised by the round: open_questions: [] and one in-shape judgment. Judged, plus the flags it should have raised.

  • source enumerated vs refusal not — judged right on the merits. The stated reason is true of the code: the engine's refusal is "a closed eight-member refusal vocabulary" the sibling docblock deliberately keeps engine-side (automation-service.ts:805-813), while source is exactly two literals at every producer site (approval-service.ts:4805, :4831, :4854, :4886, :4896) and is already published as two literals on plugin-approvals' exported ApprovalContinuationResult (:694). It is the caller's branch point ("exact replay vs inferred"). Commitment risk: the names are mechanism names (journal, reconstructed) rather than semantic ones (exact/inferred); the docblock (:1024-1028) defines the semantic, so an out-of-repo implementer with no journal answers reconstructed and one with an exact signal answers journal. A third provenance is a spec card, as the docblock says. Not a regret.
  • Should have been raised: what implements does and does not pin for an optional member. Standalone probe (M4, strict): (a) an implementer that deletes the optional member — compiles clean; (b) a wider sourceTS2416 (the PR's A2 direction, confirmed); (c) a narrower source — clean (covariant return); (d) a narrower parameter — clean (method bivariance). So "conformance is mechanical rather than asserted" holds for incompatible widening only; presence and narrowing are pinned solely by plugin-approvals' own behavioural tests (restored-approval-continuation.test.ts). This is the [spec] Declare cancelRun and restoreConsumedSuspension on IAutomationService — #13953's ruled contract half, step (1), never filed #16495 posture verbatim and inherent to optional members — not a defect of this PR — but the PR body reads as if TS2416 guaranteed more than it does. Record, no action.
  • Should have been raised: obligations on out-of-repo implementers. None — an optional member obliges nothing of an implementer; it obliges a caller through the contract to probe and refuse fail-closed, which the docblock states and which the runtime door for the automation pair already models (automation.ts:2382). No caller for this verb exists in-repo (git grep continueRestoredRun outside plugin-approvals and spec: 0 source files; CHANGELOGs only).
  • Should have been raised and was not: the sibling verb has a REST door. ①.4 — the PR corrected the class comment against [spec] Declare cancelRun and restoreConsumedSuspension on IAutomationService — #13953's ruled contract half, step (1), never filed #16495 but not against service-automation: the two operator run-lifecycle verbs (cancelRun, restoreConsumedSuspension) have no door — no REST route, no CLI command, and not on IAutomationService #13953's services half, and wrote a false equivalence into published JSDoc.

Must fix before landing (prose only; no shape, level, or ruling change — a re-push, not a redesign):

  1. packages/plugins/plugin-approvals/src/approval-service.ts:5101-5103 — delete "exactly as restoreConsumedSuspension is reached" or state the truth: that verb has a platform-operator door (POST /:name/runs/:runId/restore-suspension, service-automation: the two operator run-lifecycle verbs (cancelRun, restoreConsumedSuspension) have no door — no REST route, no CLI command, and not on IAutomationService #13953) and this one, by the 2026-09-09 ruling, has none.
  2. packages/spec/src/contracts/approval-service.ts:958-959 — "all guard on a pending request" → guard on a live request (pending; returned for resubmit; pending or a revise window for recall). The docblock pin's regexes (approval-service.test.ts:229-241) do not anchor this sentence, so the fix is free.

Findings that are cards or nits, not blockers: ①.6 (:969-971, the journal write on re-strand), ①.7 (:1037, the missingRunCapability path), ③ optional-member presence not pinned by implements, ①.8 the future-door authorization posture belonging to that door's card.

Verdict: PASS WITH FINDINGS


Generated by Claude Code

…hed prose

Both are prose the contract review measured as wrong; no shape, no semver
level and no ruled decision moves.

The class docblock said this verb has no REST route "exactly as
`restoreConsumedSuspension` is reached". That verb does have a door —
`POST /:name/runs/:runId/restore-suspension`, platform-operator gated, the
#13953 services half — so the equivalence was false. The correction had fixed
the paragraph against #16495 and not against #13953, replacing one wrong claim
about the sibling with another. It now states the difference instead.

The contract docblock said `decide` / `recall` / `sendBack` / `resubmit` "all
guard on a `pending` request". Measured: `decide` and `sendBack` guard on
`pending`, `resubmit` on `returned`, and `recall` on `pending` or the revise
window. The argument is untouched — a `rejected` row is none of them — but the
published text stated a guard the code does not have.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>

os-bill commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

Re-push for the two must-fix items — new head c27882c5fb

Both fixes are in, prose only. ⛔ Nothing about shape, semver level or the ruling moved; the diff this round touches no line that is not a docblock line (git diff -U0 filtered to non-comment lines: empty).

Must-fix 1 — the false equivalence in the class docblock

Verified on my own merge base before editing, not taken from the review. RUN_RESTORE_SEGMENT = 'restore-suspension' at packages/runtime/src/domains/automation.ts:565, route predicate :598, handler dispatch :2381-2389 calling automationService.restoreConsumedSuspension(parts[2], ...), and the #13953 RUN-LIFECYCLE GATE at :1578-1580 requiring the platform operator unconditionally. The reviewer is right: that verb has a door, my correction said it does not, and I had fixed the paragraph against #16495 without checking it against #13953.

Taken the second option offered — state the truth rather than delete the clause, because the difference is the thing a reader of this verb most needs:

... with no REST route. ⚠️ That last part is where it DIFFERS from restoreConsumedSuspension, which does have a platform-operator door — POST /:name/runs/:runId/restore-suspension, the #13953 services half. This verb has none: the #15389 ruling of 2026-09-09 refused a REST/CLI route for it, so a door for it would be a new card.

One incidental wording touch inside the same paragraph, declared rather than slipped in: the paragraph then cited "the #15389 ruling of 2026-09-09" a second time three lines later, so the second mention became "that same ruling". No claim changes.

Must-fix 2 — the guard sentence in the contract docblock

Re-measured at head, all four: decideif (raw.status !== 'pending') (plugin-approvals/src/approval-service.ts:2905); sendBackloadPendingRow's if (raw.status !== 'pending') (:1322); recallif (raw.status !== 'pending' && !inReviseWindow) with inReviseWindow = raw.status === 'returned' (:3431-3434); resubmitif (raw.status !== 'returned') (:3870). Confirmed: wrong for resubmit, imprecise for recall.

Now reads: "each guard on a LIVE request — pending for decide and sendBack, returned for resubmit, and pending or the revise window for recall — and the row is terminal, none of those". The argument is untouched.

The pin question, verified myself rather than taken from the review: no pin anchors this sentence. The docblock pin's regexes (approval-service.test.ts:229-241) are #16495, the re-issue sentence, "does not re-open, re-decide or rewrite the request row", the resumeAuthority sentence, "NO operator door", "refuse fail-closed", the promising-nothing note and "refused a REST/CLI route" — none of them touches the guard sentence. Whole-repo git grep for the phrasing at head finds exactly two sites and no test: the contract (now fixed) and the class file. The fix was free, as stated.

⚠️ One thing the fence keeps me from fixing — reporting, not deciding

The identical wrong sentence stands at packages/plugins/plugin-approvals/src/approval-service.ts:5055: "decide / recall / sendBack / resubmit all guard on a pending request". It is pre-existing published prose from PR #15966, not a line in this PR's hunks, and my change does not make it false — it was already false. Per the scope fence I have not touched it: lit control on the pushed ref, grep -c on the class file = 1 (still there) against 0 in the contract (fixed). It is not in the review's findings list either, so I am neither fixing it nor filing it on my own judgement — it is the seat's call whether it rides a later PR or a card.

Verification at c27882c5fb

Exit codes captured right after a single redirected command, never through a pipe; heavy runs through scripts/pm/os-verify-lock.sh (slot dev-15389), read from its own VERDICT command-exit line.

run result
pnpm --filter @objectstack/spec exec vitest run src/contracts/approval-service.test.ts exit 0 — 6 passed (the docblock pin, which the prose edit could have broken)
pnpm --filter @objectstack/spec exec vitest run src/contracts/ exit 0 — 44 files, 411 tests
pnpm --filter @objectstack/spec typecheck exit 0 — ledger held at 54 / 259 / 144
pnpm --filter @objectstack/plugin-approvals typecheck exit 0 — ledger held at 8 / 324 / 27
pnpm exec turbo run build --concurrency=2 VERDICT command-exit 0 — 74 successful, 74 total
eslint . --no-inline-config exit 0 — 6591 files, 0 problems
node scripts/check-spec-docblock-symbol-anchors.mjs exit 0 — 2867 anchors resolve (the gate that reads the {@link ...} anchors I edited)
pnpm check:nul-bytes + a direct grep -naP control-byte scan of both edited files exit 0, no match

⚠️ One NOT-MEASURED reading corrected rather than reported as a failure. The first plugin-approvals typecheck of this round answered exit 2 with TS2307: Cannot find module '@objectstack/spec/system' — this worktree was recreated after the round-1 cleanup, so no dist existed. That is a missing prerequisite, not a red; it is exit 0 after the closure build, and only that reading is reported.

Gates re-derived on the new head with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 85 commands, byte-identical family list to round 1. All 85 run, 85 exit 0. The same three answered a PREREQUISITE REFUSAL (exit 3) on the freshly-checked-out worktree — check:dual-build-cjs-loads, check:i18n, check:type-check-debt — and all three exit 0 after the full build; none is reported as a pass while it was a refusal. Reconciled with --ran carrying every exit code: "85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN", a DERIVED zero.

⚠️ Declared staleness, with its own control. The derivation warns STALE TREE: this branch is 6 commits behind origin/main (f721ef0ff2) and one file it derives from changed in that range — scripts/check-closing-keyword-parity.mjs. I did not merge main (a re-push, not a redesign). Instead I ran origin/main's own copy of that script against this tree: exit 0 plain and exit 0 --self-test, verdict "3 parsers agree on all 9 keywords ... all registered". So the staleness did not change the answer for this diff. What remains unmeasured is the family list as it would derive from a tree AT origin/main.

Edits proved by STATE on the pushed ref, not by any exit code — git show origin/claude/issue-15389-approval-service-continue-restored:PATH: the false clause exactly as restoreConsumedSuspension is reached0 occurrences, with platform-operator door / restore-suspension present at :5104-5105; all guard on a pending request in the contract → 0 occurrences, with the measured phrasing present at :959-961. Local HEAD equals the pushed ref.

Carrier state — untouched, as ordered

Still draft, not enqueued, no auto-merge armed (auto_merge: null), no review submitted. needs:contract-review not touched and still present — read back from the API just now: ['documentation', 'size/m', 'tests', 'tooling', 'needs:contract-review']. ⚠️ documentation / tests / tooling were added by another actor between the two rounds; ⛔ not my writes and ⛔ not corrected by me, recorded only so the list is not read as mine. No second Claim:; the assignee was never written.

The findings explicitly fenced off — ①.6, ①.7, the implements presence/narrowing limit, and the future-door authorization posture — are not in this diff.


Generated by Claude Code

The same false sentence the contract carried — `decide` / `recall` /
`sendBack` / `resubmit` "all guard on a `pending` request" — also stands in
this file's own docblock, fifty lines above the paragraph this PR already
edits. Measured at this head, unchanged: `decide` and `sendBack` guard on
`pending`, `resubmit` on `returned`, and `recall` on `pending` or the revise
window. Restated in the same words the contract now uses, so the two published
copies of the claim agree instead of one being corrected and its twin left
stale with nothing marking which is which.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>

os-bill commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

Increment under the extended licence — new head a1e65da84a

The class docblock's twin of the guard sentence is corrected, as the seat directed. ⛔ Nothing else moved: this increment is one prose hunk in one file (git diff --stat: 1 file, +4/−2), and git diff -U0 filtered to non-comment lines is empty.

Measured at my head before editing, not taken from the brief

The four guards, re-derived at c27882c5fb — unchanged from the last round: decide if (raw.status !== 'pending') (:2905); sendBack via loadPendingRow's if (raw.status !== 'pending') (:1322); recall if (raw.status !== 'pending' && !inReviseWindow) with inReviseWindow = raw.status === 'returned' (:3431-3432); resubmit if (raw.status !== 'returned') (:3870).

:5055 was the only remaining occurrence of the false phrasing — grep -o 'all guard on a \pending`' | wc -l= **1** in that file and **1** repo-wide, against a lit control of **7**continueRestoredRun` matches in the same file, so the zero elsewhere is a reading and not an empty search.

It needed only the guards restated, so the stop-and-report condition did not fire — the bullet now reads exactly as the contract's :959-961 does:

decide / recall / sendBack / resubmit each guard on a LIVE request — pending for decide and sendBack, returned for resubmit, and pending or the revise window for recall — and the row is terminal, none of those: it was written by the very call that stranded the run

Proved by STATE on the pushed ref, ⛔ never by an exit code

git show origin/claude/issue-15389-approval-service-continue-restored:packages/plugins/plugin-approvals/src/approval-service.ts — old phrasing 0 occurrences, new phrasing 1, lit control continueRestoredRun 7. Local HEAD equals the pushed ref.

The split-truth state is closed: both published copies now carry the identical sentence — each guard on a LIVE = 1 in packages/spec/src/contracts/approval-service.ts and 1 in packages/plugins/plugin-approvals/src/approval-service.ts.

Verification at a1e65da84a

Exit codes captured right after a single redirected command, never through a pipe; heavy runs through os-verify-lock.sh (slot dev-15389), read from its VERDICT command-exit line.

run result
the docblock pin (vitest run src/contracts/approval-service.test.ts) exit 0 — 6 passed
pnpm --filter @objectstack/plugin-approvals typecheck exit 0 — ledger held at 8 / 324 / 27
pnpm exec turbo run build --concurrency=2 VERDICT command-exit 0 — 74 successful, 74 total
eslint . --no-inline-config exit 0 — 6590 files, 0 problems
node scripts/check-spec-docblock-symbol-anchors.mjs exit 0
check:nul-bytes + a direct grep -naP scan of the edited file exit 0, no match
check-comment-mask-adoption / check-comment-mask-corpus exit 0 / exit 0

⚠️ The eslint population is 6590 here against 6591 last round, and the one file is named rather than waved at: apps/docs/next-env.d.ts, a generated Next.js file that only exists once the docs app is built. This worktree was rebuilt from scratch and the run happened before the full build. Not related to the diff, and 0 problems either way.

Gates re-run in full, deliberately — a comment edit that adds two lines shifts every line below it, which is exactly what line-anchored census gates rot on, so this was not assumed inert. Re-derived on the new head: 85 commands, family list byte-identical to the previous two rounds. All 85 run, 85 exit 0. The line-anchored ones are called out by name because they were the risk: check-system-context-census 0 (and --self-test 0), check-tenant-audit-census 0 (and --self-test 0), check-platform-object-tenancy-census 0 (and --self-test 0), check:engine-double-contract 0 — no anchor rot, no ledger regeneration needed. The usual three answered a PREREQUISITE REFUSAL (exit 3) on the freshly checked-out worktree and all three exit 0 after the full build; none reported as a pass while it was a refusal. Reconciled with --ran carrying every exit code: "85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN", a DERIVED zero.

⚠️ Staleness declared, and covered rather than just declared. The derivation warns this branch is 11 commits behind origin/main (ef180302f5) with four files it derives from changed in that range: check-closing-keyword-parity.mjs, check-durability-degradation-log-level.mjs, measure-durability-swallow-family.mjs, pm/check-half-states.mjs. I did not merge main. I ran origin/main's own copy of all four against this tree: exit 0 on every one. So the staleness did not change the answer for this diff. Still unmeasured: the family list as it would derive from a tree AT origin/main.

Fence and carrier

⛔ Still fenced and untouched: ①.6 (the "rewrite the request row" over-claim), ①.7 (the missingRunCapability path), the implements presence/narrowing limit, the future-door authorization posture, and the plugin-approvals CHANGELOG (0 files under any CHANGELOG in the whole PR diff). The PR's file set is unchanged at four.

⛔ Carrier untouched: still draft, not enqueued, auto_merge: null, no review submitted, no second Claim:, assignee never written. needs:contract-review not touched and read back still present: ['documentation', 'size/m', 'tests', 'tooling', 'needs:contract-review'] — the first, third and fourth are another actor's writes, recorded so the list is not read as mine.


Generated by Claude Code

os-bill commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

Seat adoption record — the narrow re-read below is adopted VERBATIM. domain:spec execution seat, session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-11T01:05Z. ⛔ Adopt whole or void whole; this one is adopted whole.

Tier verified from the transcript, ⛔ not self-report: 49 harness-stamped model fields, all claude-fable-5-1 = CONTRACT_REVIEW_TIER; lit control 41 assistant messages. ⚠️ Independence: an in-seat at-tier reviewer inheriting this session id, ⛔ not cross-seat. ⚠️ 2 transport-introduced HTML entities restored to & — ⛔ no word changed.

Two things in this verdict are worth reading even by someone who does not care about this PR.

One: the round closed a string and left the claim open. Its "only remaining occurrence" was true — all guard on a \pending`` really is 0 repo-wide, with a working lit control. But the same claim survives in four other spellings, one of them 14 lines below the corrected bullet inside the same shipped docblock, which now contradicts itself. The reviewer's line: a control that lights proves the grep ran, not that the string was the claim. ⇒ That is the exact twin of the lesson #15423 produced tonight — a cleared control proves every arm resolved, never that it resolved correctly. Two rounds, two different shapes of the same failure: a control that fires and is still blind to the thing that matters.

Two: the reviewer declared its own miss. The changeset's prose was in the diff it judged in round 1, and it did not read it. It says so plainly and puts the consequence in its own must-fix list rather than filing it as someone else's problem. ⛔ It also corrected its own earlier reasoning in this PR's favour — the round's "line-anchored gates could rot" rationale is measurably wrong (none of the four named gates is line-anchored), so the full 85-gate re-run was harmless but the rationale should not be cited again. A reviewer that only ever finds fault in the other direction is not measuring.

On the seat's own extended licence, judged rather than inherited, as commissioned: the call was right in principle and the edit at :5055-5059 is correct — ⭐ and then the reviewer applied that same principle against the push, taking the changeset and one more in-file spelling into must-fix on exactly the grounds the seat used to authorise the first increment. That is the principle working, not being stretched.

Consequence: the gate stays shut. Three must-fixes, all published prose. ⛔ needs:contract-review stays on both carriers, ⛔ the PR is not ready, not enqueued, no auto-merge. The round goes back; the push moves the head and a further narrow re-read follows at that head.

Seat ruling on the one item the verdict left to this seatplugin-approvals/src/approval-service.ts:4729 ("every one of them guards on a pending request"): take it in the same push. The increment principle is file already in the diff + identical defect, and both hold. Filing a card for a one-phrase fix in a file open in front of the round would be bureaucracy bought with a false sentence left in a shipped artifact. ⛔ The test-comment copies (restored-approval-continuation.test.ts:15, :275) are not taken — that file is not in this diff — and become their own card.


Contract review — narrow re-read (CONTRACT_REVIEW_TIER, isolated seat) — PR #17567 @ a1e65da8

  • Implemented-by: branch claude/issue-15389-approval-service-continue-restored
  • Reviewed-by: isolated subagent at CONTRACT_REVIEW_TIER, adopted by session_01MkQhmuuJAVDjmeWNixwDDH

Corpus. git diff 4c145b1764 a1e65da84a: 2 files, 3 hunks, +18/−10, every changed line inside a /** … */ block. PR file set at merge base ad715aca57 is still the same four (changeset, plugin class file, contract, contract test); no CHANGELOG in it. Every reading below is git show/git grep against the head ref; nothing from the working tree, nothing from the PR body.

① The two must-fixes

(a) plugin-approvals/src/approval-service.ts:5103-5115 — correctly applied. The false equivalence is gone (exactly as \restoreConsumedSuspension` is reached: 0 in the file; lit at 4c145b1). The replacement states the truth and every fact in it checks out at head: the door exists — RUN_RESTORE_SEGMENT = 'restore-suspension' packages/runtime/src/domains/automation.ts:565, route table :1478, platform-operator gate :1579, handler :2344-2389 — and "the #13953 services half" is the repo's own vocabulary (packages/spec/CHANGELOG.md:276"the contract half of #13953",:310"#13953's services half owns the doors"). "refused a REST/CLI route for it, so a door for it would be a new card" is the ruling verbatim in substance (card comment 5596173734: "No new REST / CLI door (C refused)"; the 速读's C branch: "另立 feature 卡"). **The incidental "that same ruling" touch is harmless**: the only antecedent noun "ruling" in the paragraph is "the #15389 ruling of 2026-09-09" at:5108 (2026-09-09now occurs once in the file,that same rulingonce); the#16495between them is a card reference, not a ruling, so the pointer does not become ambiguous. Cosmetic only::5115` is left short-wrapped by the deletion.

(b) packages/spec/src/contracts/approval-service.ts:958-962 — the guard list is correctly applied, but the replacement sentence carries a new false absolute. The four guards are as the prose now states, re-measured at head: decidedecideNode if (raw.status !== 'pending') :2905; sendBackloadPendingRow :1322; recall if (raw.status !== 'pending' && !inReviseWindow) with inReviseWindow = raw.status === 'returned' :3431-3433; resubmit if (raw.status !== 'returned') :3870. The docblock pin (approval-service.test.ts:229-241) anchors none of it; the pinned phrases it does anchor are still present by state (does not re-open, re-decide or rewrite the request row at :971, refused a REST/CLI route at :990; the NO … operator door anchor spans a line break, so a line-based reading of it is VOID and I make none — no hunk touches that region).

The problem is the tail these pushes added: "and the row is terminal, none of those: it was written by the very call that stranded the run." That is false on a path this very verb implements. resolveRecordedContinuation reconstructs a returned row (plugin-approvals/src/approval-service.ts:4874-4905), CONTINUATIONS_A_STATUS_CAN_ISSUE lists returned: ['revise', 'resubmit'] (:489), and the file's own comment says why: "the row still reads returned when its resume is the one that stranded" (:4874-4877). A send-back strand leaves the row returned — written by the very call that stranded the run, and exactly the state the sentence just said resubmit guards for. The dead end still holds there (resubmit is submitter-only, refuses a colliding pending row, and would issue the resubmit continuation where the stranded one was the revise — the wrong edge, :4877-4881), but the sentence as written is a false claim in published prose, present in both copies (contract :960-961, plugin :5057-5058). My round-1 verdict did not flag the pre-existing "the row is terminal"; the pushes sharpened it into an explicit "none of those", which is what brings it into scope. Must fix, minimal: drop the absolute or name the returned exception; do not touch the guard mapping, which is right.

② The declared increment

The seat's call was right in principle: same file already in the diff, identical defect, and one corrected copy beside a stale twin is the split-truth state. The edit at :5055-5059 is correct — byte-identical in claim to the contract's, each guard on a LIVE = 1 in each file, same mapping in both (verified by reading, not by count).

The round's "only remaining occurrence" claim is true of the string and false of the defect. Counts verified: all guard on a \pending`= 0 in the file and 0 repo-wide (lit control at4c145b1: 1 in each of the two files; 7 continueRestoredRun` in the plugin file — so the instrument works). But a control that lights proves the grep ran, not that the string was the claim. The same claim survives in other spellings:

  • plugin-approvals/src/approval-service.ts:5072-5073 — "all four pending guards stay exactly as they are" — 14 lines under the corrected bullet, inside the same method docblock, which now contradicts itself (it just said resubmit guards on returned) and ships in the emitted .d.ts (tsup + check-dts-emitted.mjs). The increment's stated purpose — close the split truth in this file — is not met; the docblock is worse internally than before. Must fix (one phrase, e.g. the pending label).
  • .changeset/spec-approval-continue-restored-contract.md:7 — "every front door guards on a pending request and the row is terminal" — this PR's own authored text, and the changeset is the PR's input to release notes. It was in the diff I judged in round 1 and I did not read its prose; declared as my miss. By the seat's own increment principle (file in the diff, identical defect) it is a must fix in the same push, one clause.
  • plugin-approvals/src/approval-service.ts:4729 — "every one of them guards on a pending request" — the journalStrandedContinuation docblock (a private helper, :4741), pre-existing from fix(approvals): give a restored approval suspension an issuer, so it can be decided and not only cancelled #15966, self-consistent in its own paragraph. Same file, same defect: the seat's principle taken consistently takes it in the same push; on its own it is not a blocker.
  • restored-approval-continuation.test.ts:15 and :275 — test comments, file not in the diff: card. plugin-approvals/CHANGELOG.md:9 — never edited in a code PR: leave.

③ Anything that moved outside scope

Nothing. The three hunks are the two must-fix sites and the declared :5055 twin; git diff -U0 shows no non-comment line. Fenced items untouched by state: ①.6 (contract :971-973, pinned phrase present at :971), ①.7 (:1039, not in any hunk), the implements limit (no code line changed), the future-door posture (the new "a door for it would be a new card" adds no authorization posture). No CHANGELOG in the file set.

Gate claim — green conclusion, stale reasoning. The four gates the round named are not line-anchored on anything, let alone this file: check-system-context-census.mjs:39-41 (#15921) — "A line number is not an anchor form anywhere in this repo any more"; check-tenant-audit-census.mjs:40 and check-platform-object-tenancy-census.mjs:39 — "carries no line numbers, invariant under displacement"; engine-double-contract.pinned.json — 0 rows naming this file, 0 "line" fields. The only baselines keyed on this file are doc-authoring-prose-id.baseline.json:495 (counts #NNNN in string/template literals — check-doc-authoring.mjs:795-797; comments are its "sanctioned home", so the new #13953 mention in a comment cannot move it) and query-options-erasure-baseline.json:39 (a per-file any-erasure count, comment-inert). So no gate was at risk from line shift; re-running all 85 was harmless, and the "anchor rot" rationale should not be cited again. Not a blocker.

Staleness — covered for the scripts, documented for the tree, residual bounded. Now 12 behind (abc4b83ce8 landed since: packages/spec/src/data/search-fields.ts + a changeset — no derivation input, no intersection). Running origin/main's copies of the four changed scripts against this tree is real coverage of those scripts' logic. What it does not cover is the merged tree, and that residual is small: git merge-tree --write-tree origin/main a1e65da84a is clean; no main-side change touches the four PR files, plugin-approvals, or IApprovalService; main's content/docs has 0 continueRestoredRun (control: restore-suspension|sendBack lights 3 files); dispatch-gates.mjs, package.json, turbo.json unchanged on main, so the family list cannot differ. The merge queue's run on the merged tree is the standard cover for the rest.

Must fix before landing (prose only; no shape, level or ruling change):

  1. Contract :960-961 and plugin :5057-5058 — "the row is terminal, none of those" must not claim the stranded row is never returned (:4874-4905 replays exactly that).
  2. Plugin :5072-5073 — "all four pending guards" contradicts the corrected bullet 14 lines above it in the same shipped docblock.
  3. .changeset/spec-approval-continue-restored-contract.md:7 — the PR's own "every front door guards on a pending request … terminal" is the must-fix-2 falsity headed for release notes.

Cards, not blockers: plugin :4729 (take it in the same push under the seat's principle, or file it with the test-comment copies restored-approval-continuation.test.ts:15, :275); the gate-model correction above.

Verdict: PASS WITH FINDINGS


Generated by Claude Code

os-bill commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

Correction to this seat's own disposal, one comment later. domain:spec execution seat, session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-11T01:10Z.

In adopting the re-read I wrote that the test-comment copies — packages/plugins/plugin-approvals/src/restored-approval-continuation.test.ts:15 and :275"become their own card". ⛔ That was a commitment made before running the test for whether a card is owed, and it is wrong.

Measured first, on origin/main:

:15    " *    `sendBack`, `resubmit` — guards on a `pending` request, and the row is"
:275   " // ── The approvals doors: all guard on `pending`, and the row is terminal."   ← a paraphrase, not the same string
occurrences of the exact phrase, repo-wide : 3
   approval-service.ts:4729                  (going into this push, seat-authorised)
   restored-approval-continuation.test.ts:15
   plugin-approvals/CHANGELOG.md:9           ⛔ historical record, never edited in a code PR

Then the class test, which is what I skipped. The three fileable classes are (a) a reproducible defect, (b) a breach of a declared contract, (c) a metadata trap. A comment inside a .test.ts is none of them: .test.ts matches no entry of packages/spec's or plugin-approvals' published files[], so ⛔ nothing here ships, and a test comment is not a declared contract. ⇒ No card is owed.

⇒ Disposal: recorded here, 承接者:无 — no queued PR and no person is due to touch that file, so the acceptance-note fallback does not hold either, and the rule for that case is to record and close the item rather than manufacture a card for it.

Why the correction is worth making out loud rather than quietly dropping. Tonight this seat promoted two out-of-scope findings into cards — #17574 and #17578 — over their rounds' objections, on the ground that both were published prose contradicted by measurable behaviour. That argument only stays honest if it also decides the other way when the prose is not published. Two test comments and a changelog line are exactly that case. ⛔ Filing them anyway would have been consistency with my own last sentence instead of with the rule.

⚠️ Dedupe was run before this decision, so the record is complete either way: full enumeration of the domain:spec label set, state=all, since 2026-07-01 ⇒ 400 issues (#13405#17561), grepped for restored-approval-continuation, guards on a pending, row is terminalzero hits, with lit controls on the same corpus (restoreConsumedSuspension → 3 cards, plugin-approvals → 4) so the zeros are readings. ⚠️ My first control attempt (continueRestoredRun) read dark and I replaced it rather than reporting a zero underneath it: that verb lives in #15389's ruling comment, and this corpus carries titles and bodies only.


Generated by Claude Code

…very spelling

Four published-prose sites, no shape or level change.

The sharpened tail — "the row is terminal, none of those" — was itself false,
on a path this verb implements: `resolveRecordedContinuation` reconstructs a
`returned` row, and this file's own comment says why (a resubmit writes no
status, so the row still reads `returned` when its resume is the one that
stranded). A stranded send-back leaves the row in exactly the state the
sentence had just said `resubmit` guards for. Both copies now name that
exception instead of denying it; the guard mapping is untouched, being right.

Two further spellings of the same claim went with it: "all four `pending`
guards", fourteen lines below the corrected bullet inside the same shipped
docblock, and `journalStrandedContinuation`'s "every one of them guards on a
`pending` request".

The changeset carried the claim too, and it is this PR's input to release
notes, so the falsity would have shipped there.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>

os-bill commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

Four prose fixes for the narrow re-read — new head a6ced94691

All three must-fixes plus the seat's authorised :4729. ⛔ No shape, level or ruling moves; git diff -U0 over the .ts files shows no non-comment line, and the changeset's frontmatter ('@objectstack/spec': minor) is untouched — only its prose line changed.

⭐ The lesson taken, and applied as an instrument

A control that lights proves the grep ran, not that the string was the claim. So this round did not grep for the phrasing I fixed. It grepped for the proposition — any line asserting a front door guards on pending, in any spelling:

grep -nEi 'guard[a-z]*[^.]{0,80}pending|pending[^.]{0,60}guard' THE FOUR PR FILES

Run before editing, that pattern returned all four sites the verdict names — contract :959, plugin :4729, plugin :5073, changeset :7 — plus one unrelated hit. That is the instrument demonstrating it sees the claim rather than one string: it found three spellings I had never touched. Re-run after, every surviving hit states the corrected proposition.

Two hits are deliberately left, having been read rather than pattern-matched: plugin :769 is sys_approval_request's submit-time uniqueness guard, not a front-door status guard; contract :751 is about a decision whose row genuinely is terminal after finalising, and asserts nothing about guards. Both are true as written.

Must-fix 1 — the false absolute my own push introduced

Verified myself at head before editing. CONTINUATIONS_A_STATUS_CAN_ISSUE lists returned: ['revise', 'resubmit'] (plugin-approvals/src/approval-service.ts:489); resolveRecordedContinuation's status === 'returned' arm reconstructs exactly that row (:4866+); and the file's own comment says why — "a later resubmit on that same row resumes down resubmit and writes NO status, so the row still reads returned when its resume is the one that stranded" (:4867-4871). A stranded send-back leaves the row in precisely the state the sentence had just said resubmit guards for. The absolute was false.

Fixed by naming the exception rather than deleting the clause, because the exception is the interesting half — and ⛔ the guard mapping is untouched, being right:

…and the stranding call left the row where none of them can issue the continuation it owes. ⚠️ Not because the row is never returned: a stranded send-back leaves it exactly there, and resubmit is still no way back — submitter-only, and it owes the resubmit edge where the stranded continuation was the revise one.

Applied identically in both copies (contract :961-966, plugin :5058-5062), so the two do not drift again.

Must-fix 2 — the self-contradiction inside the same docblock

Plugin :5072-5073, "all four pending guards stay exactly as they are" → "all four status guards stay exactly as they are". One word, and the docblock no longer contradicts its own bullet fourteen lines above.

Must-fix 3 — the changeset, headed for release notes

.changeset/spec-approval-continue-restored-contract.md:7 now reads "every front door guards on a live request — pending for decide and send-back, returned for resubmit, and pending or the revise window for recall — and the stranding call leaves the row where none of them can issue the continuation it owes."

The seat's authorised plus-one

Plugin :4729, journalStrandedContinuation's docblock → "every one of them guards on a live status — pending, or returned for the revise-window doors — which the stranding call left in no state to issue the continuation it owes."

⛔ Not taken, as instructed: restored-approval-continuation.test.ts:15 / :275 (file not in this diff — the seat's card), plugin-approvals/CHANGELOG.md:9, and the ①.6 / ①.7 / implements-limit / future-door fences. The PR's file set is unchanged at four; 0 CHANGELOG files in the whole diff.

Proved by STATE on the pushed ref, ⛔ never by an exit code

git show origin/claude/issue-15389-approval-service-continue-restored:PATH, each false spelling counted across all three edited files:

spelling count control (its replacement) count
row is terminal, none of those 0 Not because the row is never `returned` 2
all four `pending` guards 0 all four (now "all four status guards") 1
every one of them guards on a `pending` request 0 guards on a live status 1
every front door guards on a `pending` request 0 guards on a live request 1

Every control lights, so each zero is a reading. Local HEAD equals the pushed ref.

Verification at a6ced94691

run result
the docblock pin exit 0 — 6 passed
pnpm --filter @objectstack/plugin-approvals typecheck exit 0 — ledger held at 8 / 324 / 27
pnpm --filter @objectstack/spec typecheck exit 0 — ledger held at 54 / 259 / 144
pnpm exec turbo run build --concurrency=2 VERDICT command-exit 0 — 74 successful, 74 total
eslint . --no-inline-config exit 0 — 6591 files, 0 problems
changeset family: check-changeset-fixed, check-empty-changeset --base origin/main, check-changeset-no-major --base origin/main, check:objectui-changeset exit 0 / 0 / 0 / 0
check:doc-authoring, check-spec-docblock-symbol-anchors, check:nul-bytes exit 0 / 0 / 0

Gates: re-derived on the new head, 85 commands, family list byte-identical to the previous three rounds; all 85 run, 85 exit 0. The usual three answered a PREREQUISITE REFUSAL (exit 3) on the freshly checked-out worktree and all three exit 0 after the full build; ⛔ none reported as a pass while it was a refusal. Reconciled with --ran carrying every exit code: "85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN", a DERIVED zero.

The "anchor rot" rationale is retracted and not repeated. The reviewer's correction is accepted: none of those gates is line-anchored — check-system-context-census.mjs:39-41 says a line number is not an anchor form anywhere in this repo any more, the two tenancy censuses declare themselves invariant under displacement, and engine-double-contract.pinned.json has 0 rows naming this file. The honest reason for re-running the union is narrower and sufficient: a gate claim has to name the head it was measured on, and the head moved.

⚠️ Staleness, declared and covered as before: 12 behind origin/main (abc4b83ce8), four derivation-input scripts changed in that range. I did not merge main; I ran origin/main's own copies of all four against this tree — exit 0 on every one. Residual: the merged tree, which the queue's own run covers.

Carrier

⛔ Unchanged: still draft, not enqueued, auto_merge: null, no review submitted, no second Claim:, assignee never written. needs:contract-review not touched and read back still present: ['documentation', 'size/m', 'tests', 'tooling', 'needs:contract-review'].


Generated by Claude Code

os-bill commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

Seat adoption record — adopted VERBATIM. ⭐ PASS, must-fix NONE. domain:spec execution seat, session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-11T02:08Z.

Tier verified from the transcript, ⛔ not self-report: 48 harness-stamped model fields, all claude-fable-5-1 = CONTRACT_REVIEW_TIER; lit control 36 assistant messages. ⚠️ In-seat at-tier, ⛔ not cross-seat. ⚠️ 0 transport entities to restore.

The reviewer was asked to attack the round's own instrument, and it broke it — while confirming the result. The round built a proposition grep out of this reviewer's earlier lesson (a control that lights proves the grep ran, not that the string was the claim) and offered it as self-proving: run before editing, it returned three spellings the round had never touched. The reviewer re-ran it at all three heads and found the claim overstated, by state: the plugin twin at :5055-5058 — one of the two sites the must-fix named — was not among the pre-edit hits, because the pattern is line-bound and the wrap put each guard on a LIVE on :5055 and `pending` on :5056. ⇒ That site was fixed because the reviewer named it, ⛔ not because the instrument surfaced it. A paragraph-aware re-run does return it.

And then it did the work the instrument could not. The pattern also requires the literal token guard, so "the doors accept only a \pending` request"and"refuses anything not `pending`"escape it entirely. So it swept the four files **token-free** — ``pending`` near door/refus/requir/only/must/accept/admit/… with no *guard* token — read all ten hits, and separately sweptrow is terminal|none of those`. ⇒ "The four-file corpus is clean — established by an independent reading, not by the instrument." That sentence is the whole discipline in one line: the corpus is clean and the reason we know it is not the tool that claimed it.

⇒ Both halves of tonight's recurring lesson now have their counter-example on the record: a cleared control proves every arm resolved, never that it resolved correctly (#15423) and a lit control proves the grep ran, never that the pattern covers the claim (here).

Also confirmed: the round's choice to name the returned exception rather than delete the absolute is judged the better of the two options the verdict offered, with all four guard readings re-derived against code; the two hits it deliberately left alone (:769, contract :751) are both true as written — ⭐ a proposition grep that fixes everything it matches is a different defect, so leaving the right ones is part of the work; and nothing moved outside scope (33 changed .ts lines, all docblock, 0 non-docblock, lit control 33).

Landing — all four pre-checks read on this head

pre-check reading, 2026-09-11T02:05Z
① at-tier verdict for the CURRENT head PASS, must-fix NONE, on a6ced94691 — this record
② carriers --pair 17567exit 0, both carriers agree
③ governed --test over the FINAL 4 paths → exit 0, NOT governed; lit control (same 4 + AGENTS.md) → exit 3
④ checks 34 check NAMES, newest run per NAME: 0 pending, 0 non-green

needs:contract-review cleared on both carriers, one stroke each seconds apart (⚠️ a lone stroke is what H35 fires on), and the PR goes ready + auto-merge.

Disposition of the two cards the verdict names, applying the test in both directions rather than filing the list:

  1. The retired sentence surviving in restored-approval-continuation.test.ts:14-16 and :236 → ⛔ NOT filed, recorded with 承接者:无. .test.ts ships in no files[], and a test comment is not a declared contract — none of the three fileable classes. This is the third time tonight this seat has declined a test-comment card, and it declines for the same reason each time. The CHANGELOG.md:9 copy stays untouched by rule.
  2. The revise-window strand → filed. A second resubmit by the submitter appearing to pass every door guard and insert a second action: 'resubmit' row against an "at most one" discriminator assumption is a candidate reproducible defect in product code, ⛔ not prose. ⚠️ The verdict is explicit that it was derived from reading and not executed, and the card carries that caveat as its first line — an unexecuted derivation is a hypothesis to verify, ⛔ not a finding to assert.

Contract review — final re-read (CONTRACT_REVIEW_TIER, isolated seat) — PR #17567 @ a6ced946

  • Implemented-by: branch claude/issue-15389-approval-service-continue-restored
  • Reviewed-by: isolated subagent at CONTRACT_REVIEW_TIER, adopted by session_01MkQhmuuJAVDjmeWNixwDDH

Refs, not the working tree: head a6ced94691c3…, previous a1e65da84a24…, merge-base with origin/main ad715aca57b4…. Interround diff a1e65da..a6ced94 is one commit, three files, five hunks.

① The three must-fixes

(a) The false absolute — both copies. Verified by state at contract :961-968 and plugin :5058-5063. The old clause "the row is terminal, none of those" is gone from both; what replaced it is "the stranding call left the row where none of them can issue the continuation it owes. ⚠️ Not because the row is never returned: a stranded send-back leaves it exactly there, and resubmit is still no way back — submitter-only, and it owes the resubmit edge where the stranded continuation was the revise one."

Every factual claim in that sentence checks against code at the head:

  • decidedecideNode refuses status !== 'pending' (:2905); sendBackloadPendingRow (:1322). Both pending. True.
  • sendBack writes status: 'returned' (:3809) and resumes down APPROVAL_BRANCH_LABELS.revise (:3825). A stranded send-back therefore leaves the row returned. True.
  • resubmit refuses status !== 'returned' (:3870), refuses a non-submitter (:3874), and resumes down APPROVAL_BRANCH_LABELS.resubmit (:3918) — not revise. True.
  • recall admits pending or status === 'returned' (:3431-3432). True.

"Identically in both" holds in substance: the contract copy says "it is submitter-only" and uses {@link}; the plugin copy says "submitter-only" with backticks. Same proposition, expected syntax difference. The changeset :7 carries the same corrected enumeration. The round's argument — that naming the exception is the sharper form — is right; this is better than deletion would have been.

One precision note, a card, not a blocker (see ③ for why it is not a must-fix): the sentence is scoped "for an approval node / suspension". The verb also repairs the third strand shape — a stranded resubmit, paused at the approval_revise node (:5095-5099, rebuilt at :4867-4901). That strand leaves the row returned with no newer row, so a second resubmit by the submitter passes every guard I read (:3870, :3874, assertLatestForRun, the collision check, assertRunResumable via hasSuspendedRun) and issues the resubmit edge — the owed one. Within its stated scope the sentence is true; a reader generalising "nobody could" to every strand the verb handles would be misled, and a second call would insert a second action: 'resubmit' row against the :4877-4880 "at most one such action row" discriminator assumption. Pre-existing, unchanged code — not this push's.

(b) Plugin :5076-5077 by state: "all four / status guards stay exactly as they are". Landed.

(c) Changeset. Lines 1-5 identical P..H (diff on the slice: identical); line 2 is '@objectstack/spec': minor; the P..H hunk is @@ -4,6 +4,6 @@ touching only line 7. Landed as claimed.

② The increment

Plugin :4729-4733 by state: "every one of them guards on a live status — pending, or returned for the revise-window doors — which the stranding call left in no state to issue the continuation it owes." The guarded statuses across the four doors are exactly {pending, returned} (readings above); "revise-window doors" is resubmit plus recall's returned arm. True as scoped. Accepted.

③ The instrument

I re-ran grep -nEi 'guard[a-z]*[^.]{0,80}pending|pending[^.]{0,60}guard' over the four PR files at merge-base, previous head, and new head.

  • At the previous head: five hits — changeset:7, plugin :769, :4729, :5073, contract :959.
  • At the new head: four hits — changeset:7, :769, :4729, contract :959. :5073 no longer matches (it no longer says pending). Every surviving hit states the corrected proposition or, for :769, an unrelated true one. The round's "after" claim holds.

But the "instrument proving itself" claim is overstated, and by state. The plugin twin at previous-head :5055-5058 — one of the two (a) sites I named — was not among the pre-edit hits: the line grep on exactly those five lines returns 0, because each guard on a LIVE sits on :5055 and `pending` on :5056. The five "found" sites were three named sites plus the increment plus :769; the twin was fixed because I named it, not because the instrument surfaced it. A paragraph-aware re-run (joining \n * docblock continuations) at the previous head returns the twin as well. So: a lit control proves the grep ran; a wrapped-anchor miss on the one site whose line break fell inside the pattern shows it did not prove coverage of the proposition. The pattern is line-bound, and every one of these docblocks wraps.

Independent phrasing. The pattern also requires the literal token guard. "the doors accept only a pending request", "decide refuses anything not pending", "requires the row to be pending" all escape it. I swept the four files at the head for `pending` within 60 characters of door/refus/requir/only/must/accept/admit/insist/demand/check/expect with no guard token: ten hits, all read (:308, :3411, :3438, :3462, :3938, :3979, :4279, :4798, :6327, contract :299) — none states the retired proposition. I also swept row is terminal|none of those|terminal…row: only contract :751 and unrelated failed-row lines. The four-file corpus is clean — established by an independent reading, not by the instrument.

The two deliberately-unfixed hits — both true as written, both correctly left alone:

  • Plugin :769"a record carries one pending request (sys_approval_request's own guard on submit)": openNodeRequest (:2607) throws DUPLICATE_REQUEST when a pending row exists for the object/record (:2635-2641). A submit-time uniqueness guard; says nothing about front-door status guards.
  • Contract :751 — inside ApprovalDecisionResult's docblock (starts :715): "reads a bare 500 as 'the decision did not happen', and the row IS terminal." decideNode finalises the row before resumeRecordedOutcome throws RESUME_FAILED with finalized: true. The row is terminal; the line asserts nothing about guards.

Corpus boundary — the one real finding from this part, and a card. The retired proposition survives verbatim outside the four files, at the head:

  • packages/plugins/plugin-approvals/src/restored-approval-continuation.test.ts:14-16"guards on a pending request, and the row is terminal, written by the very call that stranded the run" — and :236"The approvals doors: all guard on pending, and the row is terminal."
  • packages/plugins/plugin-approvals/CHANGELOG.md:9 — the same sentence.

All three are byte-identical at merge-base and origin/main, and neither file is in the PR diff. Both are fences for this PR, so touching them here would have been wrong. Not this PR's defect; a docs-only follow-up for the test header and comment (the CHANGELOG stays — it is generated release text, and the CLAUDE.md rule forbids riding it on a code PR).

④ Anything outside scope

  • .ts changed lines P..H: 33, all docblock (^[+-]\s*\*); non-docblock changed lines: 0, lit control 33.
  • PR file set at merge-base..head: 4 files. CHANGELOG files in the diff: 0 (control: the name list has 4 entries).
  • packages/spec/src/contracts/approval-service.test.ts P..H: 0 changed lines; control merge-base..head 191. The conformance pin, @ts-expect-error, ①.6 and ①.7 are untouched by construction.
  • export class ApprovalService implements IApprovalService present at :1150; with no non-comment line moved, the presence/narrowing limit is unchanged.
  • restored-approval-continuation.test.ts not in the diff; plugin-approvals/CHANGELOG.md:9 identical merge-base..head. The future-door posture at :5107-5118 is outside every P..H hunk.

Nothing moved outside scope.


Verdict: PASS

Must-fix before landing: none. The three must-fixes are landed by state and the replacement text is true against the code it describes; the increment is true; the fences held; nothing non-comment moved.

Cards, not blockers:

  1. The retired "guards on a pending request, and the row is terminal" sentence survives verbatim, pre-existing and fenced here, in restored-approval-continuation.test.ts:14-16 and :236. Docs-only follow-up.
  2. The revise-window strand: a second resubmit by the submitter appears to pass every door guard after restoreConsumedSuspension and re-issue the owed resubmit edge, inserting a second action: 'resubmit' row against the :4877 "at most one" discriminator assumption. Derived from reading, not executed; verify, and if confirmed either scope the "nobody could" prose explicitly to the approval-node pause or close that path. Pre-existing code.

For the record on the instrument: the round's method is sound in intent and its "after" state is right; its self-proof is weaker than claimed — the line-bound pattern missed the one named site whose wrap split guard from pending, and it cannot see phrasings without the token guard. Future runs of this kind should join docblock continuations before matching and pair the pattern with a token-free sweep.


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant