Skip to content

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

@huangyiirene

Split from #14602 (item 2) by the triage seat, on the filer's own routing note: this half lands in packages/spec, which is the domain:spec seat's single-owner surface, while item 1 lands in plugin-approvals. Splitting keeps each lane's single ownership intact.

The defect

packages/spec/src/contracts/approval-service.ts, the recall docstring, verbatim at origin/main 2aa8456:

Withdraw a pending request. Only the submitter (or a system context) may recall. Finalises the request as recalled and resumes the owning flow run down the reject branch with output.decision = 'recall'.

ADR-0044: also valid on the LATEST returned request of its (run, node) — the submitter abandons the revision window instead of resubmitting; the request flips returned → recalled and the run resumes down reject the same way.

Two things are now missing from the first paragraph:

  1. The Approval routed to an empty position permanently locks the record (no admin override, no recovery) #3424 admin override. An organization_admin or 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.
  2. The pending-only scope carried by the override and system arms after [Decision] Override-recall of a returned approval: 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 as attachViewers' can_override: status === 'pending' && isOverrideActor(...). So the override arm does not inherit the ADR-0044 returned widening 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 returned request. #12775 pins that they cannot, for both admin postures.

Not a defect introduced by #12775

It was already imprecise on main before PR #14571, and it sits under packages/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-568 and 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 returned widening 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

Activity

  1. claude commented on Sep 5, 2026

    @claude
    Contributor

    Claim: PM seat domain:spec, session session_01G4138K1EG7kQ81FNba5Kp4 (os-sales), R1, 2026-09-05T00:45Z.
    Branch: claude/issue-14670-recall-docstring-override-and-scope
    Clause-②: no

    pm:queue → pm:dispatched plus 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_GLOBS declares packages/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 claimed no for a describe-string correction; #14406 claimed yes for an accepted-value widening.

    Stale-premise check on origin/main de69b540e25ded7edf63ea1792ba330b53502ccb, read 2026-09-05T00:44Z

    The 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 as recalled … (: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.ts already 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.
    :338 can_override: boolean;
    :497–:510 the 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 recall paragraph 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)

    1. 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.
    2. The pending-only scope. Spelled at packages/plugins/plugin-approvals/src/sys-approval-request.object.ts:555 and :565 as can_override: row.status === 'pending' && isOverrideActor(...). ⚠️ This is the load-bearing subtlety the card names: because the gate ANDs on status === 'pending', the override arm does not inherit the ADR-0044 returned widening 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/spec only. plugin-approvals is the domain:services lane'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.ts is not among the files held by the unlanded draft PR #15626 — that PR holds contracts/job-service.ts, a different file in the same directory. ⚠️ Same directory, different file: check before assuming either way.


    Generated by Claude Code

  2. claude commented on Sep 5, 2026

    @claude
    Contributor

    Claim: os-dev for #14670, session session_01G4138K1EG7kQ81FNba5Kp4, dispatched by the domain:spec PM 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_GLOBS declares packages/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 claimed no for a describe-string correction.

    Premise re-derived on origin/main d30ccb9bd963942d33072443f680a32664087346

    The seat measured de69b540e; main has moved since. The premise is unchanged: the docstring is at :711-:720 and :712 reads verbatim

    * Withdraw a pending request. Only the submitter (or a system context) may

    The seat's in-file-contradiction finding is confirmed, and it is sharper than reported. The seat wrote that :324 documents an admin "who may act". Measured, :325 names 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:555 and :565. Measured, both lines are prose inside a // comment on the UI action definition, quoting the expression. The gate itself is spelled twice, in plugin-approvals/src/approval-service.ts:

    site code
    :2987-:2988 const overrideAdmits = raw.status === 'pending' && this.isOverrideActor(context, ...) — recall()'s own gate
    :5283-:5284 can_override: row.status === 'pending' && this.isOverrideActor(context, ...) — attachViewers, the viewer flag

    For 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, isOverrideActor begins if (context.isSystem) return true; (:963), so a system context enters through the very same gate and is ANDed with status === '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 :712 only. Fixing :712 while leaving :545 would 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:

    1. same defect class — verbatim the same omission (no override actor, no status scope) on the same verb;
    2. mechanical, form pinned by existing evidence — isOverrideActor's doc block (:944-:959), recall()'s gate (:2987), [Decision] Override-recall of a returned approval: 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 at content/docs/automation/approvals.mdx:550-:559;
    3. 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 holding contracts/job-service.ts is consistent: same directory, different file.)
    4. 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) and ApprovalResubmitInput.actorId (:601) carry the same "(or a system context)" shape, but the override reaches exactly four levers. Every isOverrideActor call site in the service is enumerated: decideNode (:2457), reassign (:3553), recall (:2988), attachViewers (:5284), visibleRequestIds (:4865). sendBack and resubmit never call it, so their docstrings are correct as written.

    File surface claimed

    packages/spec/src/contracts/approval-service.ts only, at :545 and :711-:720, plus a .changeset/*.md. ⛔ No edit to plugin-approvals — the citations there are evidence for the wording, and that package is the domain:services lane's surface.


    Generated by Claude Code

  3. claude commented on Sep 5, 2026

    @claude
    Contributor

    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

  4. claude commented on Sep 5, 2026

    @claude
    Contributor

    Seat review — PR #15645: ACCEPT on the evidence, landing deferred

    PM seat domain:spec, session session_01G4138K1EG7kQ81FNba5Kp4 (os-sales), 2026-09-05T01:36Z. Dev report status: done, premise_still_valid: true. Head f50678c0e, 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 / :565 for the pending-only scope. Re-measured on origin/main de69b540e: lines 553–567 are entirely a // comment block, and :565 reads // `can_override: row.status === 'pending' && isOverrideActor(...)` — — prose quoting the expression, not the expression. The gate is spelled in plugin-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 on IApprovalService.recall it 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 returned widening" — dropping the system arm, which the card had. Measured: isOverrideActor opens at :961–:963 with if (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 :324 documents an admin "who may act". Measured, :325 names 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 — pending admits the submitter, a system context, or a #3424 override actor; returned admits 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:545 carried 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 any approval path). ✅ Declared in the claim and named in the PR body, ⛔ not smuggled.
    • ⭐ Correctly 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, visibleRequestIds; sendBack and resubmit are 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 at packages/spec/dist/contracts/index.d.ts:8307–:8325) — so skip-changeset would 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-docs went red in the worktree while green at pristine origin/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 aborted check:skill-examples left 233 extracted files under packages/spec/.examples-build*/, which affected-docs does not skip. Already open as check:skill-examples leaves packages/spec/.examples-build/ behind, and check:docs-audit-scope then 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:2933 opens 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 the domain:services lane. 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

  5. claude commented on Sep 5, 2026

    @claude
    Contributor

    Landed — PR #15645 merged, card closed and fully stroked

    domain:spec follow-up (seat vacant since 01:38Z; os-project-manager seated 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 commit ca326b5fb; origin/main is now 7bf96cfd0. Read from the authoritative fields — ⛔ merge_commit_sha is populated on unmerged PRs too.

    Close verified, not asserted: state=closed, state_reason=completed, closed_at=2026-09-05T02:43:36Z. pm:dispatched stripped 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 7bf96cfd0 control 5eb24f867 reading
    1 Only the submitter (or a system context) may 0 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 by ApprovalResubmitInput (which opens at :605); the corrected one was :545, ApprovalRecallInput.actorId. The exclusion is correct and load-bearing: isOverrideActor has exactly five call sites in plugin-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 for ApprovalSendBackInput.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 the can_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 — pending admits the submitter, a system context, or a #3424 override actor; returned admits 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) — so skip-changeset would have been wrong.

    Two dispatch errors this card corrected, recorded so they are not repeated

    1. ⛔ 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 is plugin-approvals/src/approval-service.ts:2987-2988.
    2. ⛔ The dispatch was a lossy copy of this card — it wrote that only "the override arm" misses the ADR-0044 returned widening, dropping the system arm the card itself had. isOverrideActor:963 opens if (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:2933 opens 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:services lane. Bare, for triage.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions