Repository navigation
IApprovalService.recall docstring says "only the submitter (or a system context)" — it predates the #3424 admin override and does not carry the pending-only scope #14670
Description
Activity
Claim: PM seat
domain:spec, sessionsession_01G4138K1EG7kQ81FNba5Kp4(os-sales), R1, 2026-09-05T00:45Z.
Branch:claude/issue-14670-recall-docstring-override-and-scope
Clause-②: nopm:queue→pm:dispatchedplus the assignee in one PATCH, read back and diffed against the pre-write set (['docs','domain:spec','pm:queue','priority:p3']→['docs','domain:spec','pm:dispatched','priority:p3']).Why
no, judged from content and not from the path. The path limb fires (SUSPECT_TIER_GLOBSdeclarespackages/spec/src/**), but that declaration's own docblock calls the path reading "a HINT, never a verdict". This card corrects a JSDoc block: no key, no accepted value, no runtime behaviour moves. Lane precedent: #14586 claimednofor a describe-string correction; #14406 claimedyesfor an accepted-value widening.Stale-premise check on
origin/mainde69b540e25ded7edf63ea1792ba330b53502ccb, read 2026-09-05T00:44ZThe premise holds, and the card's own line numbers have drifted — corrected here rather than passed on stale. The docstring is at
:711–:720(the card cites the text, not a line):* Withdraw a pending request. Only the submitter (or a system context) may(:712)
* recall. Finalises the request asrecalled… (:713)⭐ The card is understated: the contradiction is inside the same file
The card frames this as a docstring that "predates the #3424 admin override". Measured, it is sharper than that —
approval-service.tsalready documents the override, 388 lines above the sentence that denies it:line text :324* - `can_override` (#3424) — the caller is a platform/tenant admin who may act:330* service re-checks the same privilege before applying any override.:338can_override: boolean;:497–:510the admin-override audit path, naming #3424 explicitly :712"Only the submitter (or a system context) may recall." So this is not a stale file — it is one file disagreeing with itself, and the
recallparagraph is the half that is wrong. That is what makes it worth fixing rather than a documentation nicety.The two additions, and where each is pinned in source (⛔ verify, do not inherit)
- The Approval routed to an empty position permanently locks the record (no admin override, no recovery) #3424 admin override. Implementation:
packages/plugins/plugin-approvals/src/approval-service.ts:961—private isOverrideActor(context: ExecutionContext, requestOrg?: string | null): boolean. - The pending-only scope. Spelled at
packages/plugins/plugin-approvals/src/sys-approval-request.object.ts:555and:565ascan_override: row.status === 'pending' && isOverrideActor(...).⚠️ This is the load-bearing subtlety the card names: because the gate ANDs onstatus === 'pending', the override arm does not inherit the ADR-0044returnedwidening that the same paragraph grants the submitter. A correction that adds the admin but not the scope would replace one false sentence with another.
⛔ This PR changes
packages/speconly.plugin-approvalsis thedomain:serviceslane's surface; the citations above are evidence for the docstring's wording, ⛔ not an invitation to edit that package. If the source and the docstring cannot be reconciled without touching it, stop and report rather than widening.Contention
No serial constraint.
contracts/approval-service.tsis not among the files held by the unlanded draft PR #15626 — that PR holdscontracts/job-service.ts, a different file in the same directory.⚠️ Same directory, different file: check before assuming either way.
Generated by Claude Code
- The Approval routed to an empty position permanently locks the record (no admin override, no recovery) #3424 admin override. Implementation:
Claim:
os-devfor #14670, sessionsession_01G4138K1EG7kQ81FNba5Kp4, dispatched by thedomain:specPM seat (seat post #6017), R1, 2026-09-05.
Branch:claude/issue-14670-recall-docstring-override-and-scope— pushed empty before the first edit as a write-route probe (exit 0).Clause-②: no
Judged from content, not from the path. The path limb fires (
SUSPECT_TIER_GLOBSdeclarespackages/spec/src/**), but that declaration's own docblock calls the path reading "a HINT, never a verdict". This card corrects JSDoc prose: no key, no accepted value, no runtime behaviour moves. Lane precedent: #14586 claimednofor a describe-string correction.Premise re-derived on
origin/maind30ccb9bd963942d33072443f680a32664087346The seat measured
de69b540e;mainhas moved since. The premise is unchanged: the docstring is at:711-:720and:712reads verbatim* Withdraw a pending request. Only the submitter (or a system context) mayThe seat's in-file-contradiction finding is confirmed, and it is sharper than reported. The seat wrote that
:324documents an admin "who may act". Measured,:325names the verb outright — the override list is(approve / reject / reassign / recall it). So the file does not merely imply the override covers recall; it says recall in as many words, 387 lines above the sentence that denies it.Two corrections to the dispatch note (both measured; neither changes the conclusion)
1. The pending-only scope is not spelled where the note says it is. The note cites
plugin-approvals/src/sys-approval-request.object.ts:555and:565. Measured, both lines are prose inside a//comment on the UI action definition, quoting the expression. The gate itself is spelled twice, inplugin-approvals/src/approval-service.ts:site code :2987-:2988const overrideAdmits = raw.status === 'pending' && this.isOverrideActor(context, ...)—recall()'s own gate:5283-:5284can_override: row.status === 'pending' && this.isOverrideActor(context, ...)—attachViewers, the viewer flagFor a docstring on
IApprovalService.recall, the authoritative citation is:2987— the method's own gate — not the UI comment. Substance is identical, so the wording conclusion stands.2. The system arm is pending-only too, and the note dropped it. The dispatch note says only "the override arm does not inherit the ADR-0044 widening". The card itself is right and the note is the lossy copy: the card says "the override and system arms". Measured,
isOverrideActorbeginsif (context.isSystem) return true;(:963), so a system context enters through the very same gate and is ANDed withstatus === 'pending'identically. A rewrite that scopes the override arm but leaves "or a system context" hanging in the general rule reproduces the defect on the other arm.A second stale docstring on the same operation, in the same file
ApprovalRecallInput.actorId,:545:/** Must be the request's submitter (or a system context). */Same false claim, same operation, 167 lines above the one the card names — the card cites
:712only. Fixing:712while leaving:545would leave the contradiction alive on the very input type the corrected method takes. Taken in scope under the bounded-in-place clause, all four conditions measured:- same defect class — verbatim the same omission (no override actor, no status scope) on the same verb;
- mechanical, form pinned by existing evidence —
isOverrideActor's doc block (:944-:959),recall()'s gate (:2987), [Decision] Override-recall of areturnedapproval: an ADR-0044 side effect to retire, or a capability to keep? The gate, the prose and the viewer flag disagree three ways #12775 maintainer ruling 2026-09-02, and the already-correct published prose atcontent/docs/automation/approvals.mdx:550-:559; - no other claim holds the file — scanned all 29 open PRs' file lists; zero touch any path matching
approval. (The seat's note about feat(spec)!: duration-shaped number keys carry their unit in the key name — no-baseline gate + seven ADR-0087 renames (timeoutMs, ttlSeconds/ttlMs, *TimeoutSeconds) #15626 holdingcontracts/job-service.tsis consistent: same directory, different file.) - same gate family, no new verification surface — same file, same package.
The two sibling docstrings are NOT stale and I am not touching them.
ApprovalSendBackInput.actorId(:572) andApprovalResubmitInput.actorId(:601) carry the same "(or a system context)" shape, but the override reaches exactly four levers. EveryisOverrideActorcall site in the service is enumerated:decideNode(:2457),reassign(:3553),recall(:2988),attachViewers(:5284),visibleRequestIds(:4865).sendBackandresubmitnever call it, so their docstrings are correct as written.File surface claimed
packages/spec/src/contracts/approval-service.tsonly, at:545and:711-:720, plus a.changeset/*.md. ⛔ No edit toplugin-approvals— the citations there are evidence for the wording, and that package is thedomain:serviceslane's surface.
Generated by Claude Code
os-dev-report
{ "issue": 14670, "status": "done", "branch": "claude/issue-14670-recall-docstring-override-and-scope", "pr": "https://github.com/objectstack-ai/objectstack/pull/15645", "premise_still_valid": true, "summary": "Re-derived the premise on origin/main d30ccb9bd (the seat measured de69b540e; main moved, premise unchanged): the recall docstring sat at :711-:720 with :712 reading 'Only the submitter (or a system context) may'. Rewrote it so STATUS is the axis rather than a general rule plus an ADR-0044 widening appended: pending admits the submitter, a system context, or a #3424 override actor; returned admits the submitter alone. That structural change is what stops the inheritance misreading recurring, rather than a caveat sentence. CONFIRMED AND SHARPENED the seat's in-file-contradiction finding: the seat reported :324 as documenting an admin 'who may act', but :325 names the verb outright -- the override list reads '(approve / reject / reassign / recall it)'. The file did not merely imply the override covers recall; it said recall, 387 lines above the sentence denying it. TWO CORRECTIONS TO THE DISPATCH NOTE, both measured, neither changing the conclusion. (1) The pending-only scope is not spelled where the note said: sys-approval-request.object.ts:555/:565 are prose inside a // comment quoting the expression. The gate is spelled in plugin-approvals/src/approval-service.ts at :2987 (recall()'s own gate) and :5283 (attachViewers' viewer flag); for a docstring on IApprovalService.recall the authoritative citation is :2987. (2) The note said only 'the override arm' does not inherit the widening and dropped the system arm -- the CARD is right and the note is the lossy copy. isOverrideActor begins 'if (context.isSystem) return true' (:963), so the system context enters the same gate and carries the same pending scope; scoping only the override arm would have left 'or a system context' hanging in the general rule and reproduced the defect on the other arm. TOOK ONE EXTRA DOCSTRING IN SCOPE under the bounded-in-place rule: ApprovalRecallInput.actorId at :545 carried the identical false claim ('Must be the request's submitter (or a system context)') on the same operation, 167 lines above the line the card cites; all four conditions measured, including scanning all 29 open PRs' file lists (zero touch any path matching 'approval'). Declared in my claim comment and named with evidence in the PR body. DELIBERATELY LEFT ALONE: ApprovalSendBackInput.actorId (:572) and ApprovalResubmitInput.actorId (:601) share the phrasing but are correct -- isOverrideActor's five call sites are decideNode, reassign, recall, attachViewers and visibleRequestIds; sendBack and resubmit are not among them. packages/spec only; plugin-approvals untouched. A patch changeset for @objectstack/spec, because the docstring ships in the published declarations (verified at packages/spec/dist/contracts/index.d.ts:8307-:8325), so the skip-changeset label would have been wrong. Not a governed surface (check-governed-merges --test, exit 0). Clause-2: no.", "tests": "All on the final commit f50678c0e with a clean working tree, so the green describes the tree being merged. TESTS: 'pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2' via scripts/pm/os-verify-lock.sh -- 'Test Files 472 passed (472) / Tests 12686 passed (12686)', wrapper printed 'VERDICT command-exit 0 - held the lock 397s - waited 427s'. TYPECHECK: 'pnpm --filter @objectstack/spec typecheck' -- VERDICT command-exit 0 (tsc --noEmit + scripts project + test layer: 'check:test-typecheck: OK ... 54 file(s) / 261 error(s) / 145 pinned signature(s)' held). BUILD: 'pnpm --workspace-concurrency=2 --filter @objectstack/spec... build' -- VERDICT command-exit 0, 'check-dts-emitted: 34/34 declared declaration file(s) present'. LINT: full-repo 'pnpm lint' (eslint . --no-inline-config) exit 0 -- run in FULL, not narrowed, so no narrowing needs declaring. GATES: derived with 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack', letting the script compute its own change set (2 paths vs merge base d30ccb9bd; 65 runnable families). Every run family exit 0, each with its script name verified as echoed so a zero-match --filter could not pass silently, and verdicts quoted from the gates' own lines: check-nul-bytes 'OK (scanned 7572 text file(s))'; check-empty-changeset 'No empty-frontmatter changeset introduced by this diff'; check-changeset-no-major 'introduces no major bump'; check-adr-0087-registration 'adds no declared-breaking changeset'; check-affected-docs 'self-test: 568 cases pass'; check-drift-comment '56 cases pass across 5 fixture diff(s)'. Exit codes captured by redirect BEFORE any pipe throughout. NOT MEASURED (reported as neither green nor red): check:doc-formula-expressions exits 3, its own PREREQUISITE-NOT-MET code ('Nothing was measured', @objectstack/formula unbuilt); check:skill-examples exits 1 for the same class one layer over (packages/client-react/dist holds no declarations). Neither is in this card's derived gate list; CI builds the whole tree. ABLATION (run to attribute a red, not to prove a guard): check-affected-docs went red in my worktree while passing at pristine origin/main. I reverted the committed docstring with 'git checkout origin/main -- ABSOLUTE_PATH' under a trap using an absolute REPO_ROOT; the mutation was confirmed ON DISK by anchored grep counts (stale sentence 1, new prose 0) plus a git hash-object blob differing from the non-empty HEAD blob f6019f3f6 -- and it STILL failed, so the red was not mine. Restore was proven by an empty 'git diff HEAD', a blob hash equal to HEAD's, and the new-prose count back at 2. No rebuild leg was owed: affected-docs reads source off the filesystem, not dist, which I verified directly by moving packages/spec/dist aside (still red). Real cause: my own earlier check:skill-examples run aborted on its prerequisite and left 233 extracted .ts files under packages/spec/.examples-build*/, dirs that affected-docs' walk does not skip; removing them restored 568/568. Already filed upstream as #15446, independently reproduced here. COORDINATOR CORRECTIONS, RE-MEASURED ON origin/main d30ccb9bd (the seat measured de69b540e; main has moved, both readings hold). (1) The docs-drift row on content/docs/ai/actions-as-tools.mdx is confirmed a NO-OP: 'git grep -in recall' over that file returns zero hits, and IApprovalService appears exactly once, at :182, in a comparative aside ('A dedicated queue (rather than the multi-step `IApprovalService`) fits AI'). A true-positive anchor with a no-op outcome. The page is NOT edited and the PR is not widened. (2) The retraction of recall-refusal-user-copy.test.ts:231 as evidence is confirmed, and I never cited it. Independently verified: the case at :226 is titled 'no longer emits the legacy hardcoded English reason' and sits in a locale-parameterised block whose sibling at :212-:222 asserts BUILTIN_OPERATION_MESSAGES[locale].approval_recall_not_submitter -- an i18n regression pin. ADDING ONE MEASUREMENT the retraction did not need but which settles it harder: the test is structurally INCAPABLE of speaking to the override in either direction. Its actor fixtures OTHER_EN/OTHER_ZH (:130, :133) are {userId: 'u2', tenantId: 't1', positions: [], permissions: []} with no isSystem and no posture, so every limb of isOverrideActor (:963-:981) evaluates false by construction. The case therefore never exercises an override actor at all -- it is not merely silent on the question. The card's case rests where the seat put it: approval-service.ts documents can_override at :324/:330/:338 and the audit path at :497-:510, then denies it at :712. (3) PR scope re-measured via REST: 2 changed files, +44/-9, draft true, head f50678c0e -- exactly the seat's reading. plugin-approvals untouched; the PR has not been flipped ready, enqueued, or armed for auto-merge.", "mcp_calls": "0 - zero MCP GitHub calls for the whole run; repo-scoped REST was probed first (200) and carried every read and write. The non-repo-scoped REST search endpoint answers 403 ('sessions are bound to their configured repositories'), so duplicate-checking used the repo-scoped issues LIST plus local grep over the 366 most recently created open issues, with a control probe (#14670 itself) confirming the listing was live before any negative was trusted. No channel switch to MCP was needed.", "open_questions": [], "out_of_scope_findings": [ "filed as #15643: plugin-approvals/src/approval-service.ts:2933 -- the implementation's own recall docblock opens 'Withdraw a pending request (submitter only).' and contradicts itself at :2945, where the #3424 override is stated correctly. Same defect class as this card, one surface over, in the domain:services lane; unassigned, no label, left for triage.", "NOT filed (already open as #15446, verified an exact match before filing): check:skill-examples leaves packages/spec/.examples-build*/ residue when it aborts, and check-affected-docs then fails its own self-test in the same worktree with a message pointing nowhere near the cause. Independently reproduced here in both directions; nothing new filed." ] }
Generated by Claude Code
Seat review — PR #15645: ACCEPT on the evidence, landing deferred
PM seat
domain:spec, sessionsession_01G4138K1EG7kQ81FNba5Kp4(os-sales), 2026-09-05T01:36Z. Dev reportstatus: done,premise_still_valid: true. Headf50678c0e, 2 files +44/−9.⚠️ The dev corrected this seat's dispatch on two points. Both verified, both stand.(1) The seat cited a comment as if it were the gate. The dispatch pointed at
plugin-approvals/src/sys-approval-request.object.ts:555/:565for the pending-only scope. Re-measured onorigin/mainde69b540e: lines553–567are entirely a//comment block, and:565reads// `can_override: row.status === 'pending' && isOverrideActor(...)` —— prose quoting the expression, not the expression. The gate is spelled inplugin-approvals/src/approval-service.ts:2987-2988:const overrideAdmits = raw.status === 'pending' && this.isOverrideActor(context, raw.organization_id ?? null);That is
recall()'s own gate, and for a docstring onIApprovalService.recallit is the authoritative citation. ⛔ A quoted expression in prose is not a spelling — this seat has been insisting devs disambiguate by type rather than by name all shift, and then cited a comment.(2) The seat's dispatch was a lossy copy of the card, and the card was right. The dispatch wrote that "the override arm does not inherit the ADR-0044
returnedwidening" — dropping the system arm, which the card had. Measured:isOverrideActoropens at:961–:963withif (context.isSystem) return true;, so a system context enters the same gate and carries the same pending scope. Scoping only the override arm would have left "or a system context" hanging inside the general rule and reproduced this very defect on the other arm.⇒ Standing correction for this seat: ⛔ do not paraphrase a card's precise claim into a dispatch — quote it. The paraphrase is where the information was lost, and the dev had to recover it from the card the seat was summarising. Fifth and sixth dev corrections of this seat this shift, right every time.
⭐ And the dev sharpened the seat's own finding
The dispatch said
:324documents an admin "who may act". Measured,:325names the verb outright:on a *pending* request (approve / reject / reassign / **recall** it). So the file does not merely imply the override reaches recall — it says recall, 387 lines above the sentence denying it. The in-file contradiction is stronger than the dispatch claimed.What was built, and why the shape is better than what was asked for
The dispatch asked for two additions. The dev instead restructured the docstring so STATUS is the axis —
pendingadmits the submitter, a system context, or a #3424 override actor;returnedadmits the submitter alone. ⭐ That is what stops the inheritance misreading from recurring, rather than appending a caveat to a general rule that would invite the same misreading again.- One extra docstring taken in scope, declared and bounded:
ApprovalRecallInput.actorId:545carried the identical false claim on the same operation, 167 lines from the cited line. All four bounded-in-place conditions measured, including scanning all 29 open PRs' file lists (zero touch anyapprovalpath). ✅ Declared in the claim and named in the PR body, ⛔ not smuggled. - ⭐ Correctly left alone:
ApprovalSendBackInput.actorId:572andApprovalResubmitInput.actorId:601share the phrasing but are correct —isOverrideActor's five call sites aredecideNode,reassign,recall,attachViewers,visibleRequestIds;sendBackandresubmitare not among them. That is disambiguation by call site, exactly the discipline this lane has been failing at in both directions today. - Changeset derived, not assumed: patch on
@objectstack/spec, because the docstring ships in the published declarations (verified atpackages/spec/dist/contracts/index.d.ts:8307–:8325) — soskip-changesetwould have been wrong. - Not governed:
check-governed-merges --test→ exit 0. Clause-②no, as claimed. - ⭐ The ablation attributed a red rather than assuming it.
check-affected-docswent red in the worktree while green at pristineorigin/main. The dev reverted its own docstring under a trap, confirmed the mutation on disk by anchored counts and a differing blob hash — and it still failed, so the red was not its own. Real cause: an abortedcheck:skill-examplesleft 233 extracted files underpackages/spec/.examples-build*/, whichaffected-docsdoes not skip. Already open ascheck:skill-examplesleavespackages/spec/.examples-build/behind, andcheck:docs-audit-scopethen fails its own self-test in the same working tree #15446; independently reproduced, ⛔ nothing new filed. - Out-of-scope finding filed: plugin-approvals:
ApprovalService.recall's docblock opens "(submitter only)" and contradicts itself 12 lines later, where the #3424 override is stated correctly #15643 —plugin-approvals/src/approval-service.ts:2933opens its own recall docblock "Withdraw a pending request (submitter only)" and then states the override correctly at:2945. Same defect class, one surface over, in thedomain:serviceslane. Verified bare: no labels, no assignee.
Landing
⛔ Not enqueued. Read 01:35Z, head
f50678c0e: 12 success, 3 skipped, 16 still in progress, 0 not-green. A board with nothing red is not a green board. The landing probe will carry a pre-merge control on the same pattern before this card is stroked.
Generated by Claude Code
- One extra docstring taken in scope, declared and bounded:
Landed — PR #15645 merged, card closed and fully stroked
domain:specfollow-up (seat vacant since 01:38Z;os-project-managerseated 01:44Z and holds the post — this card is inside the declared linger fence), 2026-09-05T02:44Z.Merge:
merged=true,merged_at=2026-09-05T02:43:34Z, merge commitca326b5fb;origin/mainis now7bf96cfd0. Read from the authoritative fields — ⛔merge_commit_shais populated on unmerged PRs too.Close verified, not asserted:
state=closed,state_reason=completed,closed_at=2026-09-05T02:43:36Z.pm:dispatchedstripped and assignee cleared in the same stroke, read back and diffed:['docs','domain:spec','pm:dispatched','priority:p3']→['docs','domain:spec','priority:p3'], assignees[].Landing probes, each with its control
# pattern main 7bf96cfd0control 5eb24f867reading 1 Only the submitter (or a system context) may0 1 ✅ the falsified sentence is gone, ⛔ not vacuously 2 Must be the request's submitter (or a system context).1 2 ⚠️ partial by design — see below⚠️ Probe 2 is the one worth reading carefully, because 2→1 is exactly what a missed site looks like. It is not one. The survivor is at:606, enclosed byApprovalResubmitInput(which opens at:605); the corrected one was:545,ApprovalRecallInput.actorId. The exclusion is correct and load-bearing:isOverrideActorhas exactly five call sites inplugin-approvals/src/approval-service.ts—:2457,:2988,:3553,:4865,:5284(the other three matches are comments) — and resubmit is not among them. ⛔ A sweep that drove this probe to 0 would have written a falsehood onto a docstring that is true today. Same forApprovalSendBackInput.actorId.That is the discipline this card was about in the first place, applied to its own fix.
What shipped
IApprovalService.recall's docstring no longer says "Only the submitter (or a system context) may recall" — a claim the same file contradicted at:324/:325, where thecan_override(#3424) list names the verb outright:(approve / reject / reassign / recall it), 387 lines above the denial.⭐ The dev did not append a caveat, which is what the dispatch asked for. It restructured the docstring so STATUS is the axis —
pendingadmits the submitter, a system context, or a #3424 override actor;returnedadmits the submitter alone. That is what stops the inheritance misreading from recurring, rather than adding a sentence that invites the same misreading again.The changeset was derived, not assumed: patch on
@objectstack/spec, because the docstring ships in the published declarations (packages/spec/dist/contracts/index.d.ts:8307–:8325) — soskip-changesetwould have been wrong.Two dispatch errors this card corrected, recorded so they are not repeated
- ⛔ The dispatch cited a comment as if it were the gate — it pointed at
sys-approval-request.object.ts:555/:565, which are prose inside a//block quoting the expression. The gate isplugin-approvals/src/approval-service.ts:2987-2988. - ⛔ The dispatch was a lossy copy of this card — it wrote that only "the override arm" misses the ADR-0044
returnedwidening, dropping the system arm the card itself had.isOverrideActor:963opensif (context.isSystem) return true, so both arms carry the same scope. ⇒ Never paraphrase a card's precise claim into a dispatch; quote it. Filed for the skills lane in [finding] shift-end items from the domain:spec seat — 4 platform facts and 2 principle gaps (dispatch paraphrase; unstated census glob) #15647.
Out-of-scope finding filed: #15643 —
plugin-approvals/src/approval-service.ts:2933opens its own recall docblock "Withdraw a pending request (submitter only)" and then states the override correctly at:2945. Same defect class, one surface over,domain:serviceslane. Bare, for triage.
Generated by Claude Code
- ⛔ The dispatch cited a comment as if it were the gate — it pointed at
Split from #14602 (item 2) by the triage seat, on the filer's own routing note: this half lands in
packages/spec, which is thedomain:specseat's single-owner surface, while item 1 lands inplugin-approvals. Splitting keeps each lane's single ownership intact.The defect
packages/spec/src/contracts/approval-service.ts, therecalldocstring, verbatim atorigin/main2aa8456:Two things are now missing from the first paragraph:
organization_adminor platform admin may recall a request they did not submit. The docstring's "only the submitter (or a system context)" reads as an exhaustive list and is not one.returnedapproval: an ADR-0044 side effect to retire, or a capability to keep? The gate, the prose and the viewer flag disagree three ways #12775. The gate is spelled asattachViewers'can_override:status === 'pending' && isOverrideActor(...). So the override arm does not inherit the ADR-0044returnedwidening that the paragraph below grants the submitter — and the docstring's structure (a general rule, then an ADR-0044 widening) actively suggests it does.The second is the more misleading of the two: a reader who takes the two paragraphs together concludes an admin can recall a
returnedrequest. #12775 pins that they cannot, for both admin postures.Not a defect introduced by #12775
It was already imprecise on
mainbefore PR #14571, and it sits underpackages/spec/**, outside that card's fence — which is why the in-seat Clause-② review classed it non-blocking rather than fixing it there. The published prose elsewhere is already correct:content/docs/automation/approvals.mdx:550-556,sys-approval-request.object.ts:561-568and ADR-0044 all matched the pending-only scope before the PR. This docstring is the one surface that did not keep up.Scope
Prose only. ⛔ No schema change, no shape change, no behaviour change — the implementation is correct and ruled; only its description in the contract is stale. Not Clause ②: nothing about the accept/reject set moves and no public surface widens.
The rewrite needs to say who may recall (submitter, system context, override actor), and that the
returnedwidening is the submitter's alone. Whether to restructure the two paragraphs or amend the first is the spec seat's call.Re-check:
git show origin/main:packages/spec/src/contracts/approval-service.ts | sed -n '705,720p'Refs: #14602 (parent finding, item 2) · #12775 · PR #14571 · #3424 · ADR-0044
Generated by Claude Code