Skip to content

docs(approvals): say how a request with no submitter leaves the returned state - #30137

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-29910-no-submitter-returned-docs
Nov 4, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-29910-no-submitter-returned-docs

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by 田騏 · project thread

Fixes #29910

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:审批指南和 IApprovalService 的 recall / sendBack / resubmit 注释写明「没有提交人的审批单」被退回后怎么走出来:退回后谁也撤不回,唯一出路是服务端代码以系统上下文 resubmit,新一轮 pending 仍无提交人,系统或管理员覆盖可撤回。指南还告诉作者:由无用户触发器启动的流程,只有在服务端代码会做这次重提时才声明 revise 边,否则不声明,让审批人改用驳回。
  • 为什么改:维护者在「第三批 同意」中选了 D(行为不变,把出路写清楚);此前文档没有说过这种审批单退回后的去向。
  • 风险与回滚:只改注释和文档,外加一个 @objectstack/spec patch changeset;非注释的 token 前后完全一致(见 Verification),行为不变。回滚即 revert。
  • 先看:packages/spec/src/contracts/approval-service.ts 的 recall 与 sendBack 注释;content/docs/automation/approvals.mdx 的「A request with no submitter」段。
  • 两分钟验证:git diff origin/main -- packages/spec/src/contracts/approval-service.ts,应只看到 * 开头的注释行变化。
  • 需要你做的:无,席位落地(spec 包改动,落地前席位做一次独立复审)。

What changes

  • content/docs/automation/approvals.mdx, under Send-back → resubmit: a new paragraph, A request with no submitter. It says that a run whose trigger resolved no user opens its request with no submitter; that an approver can still send it back, but nobody can recall it once it is returned and no viewer sees Resubmit or Recall; that its one way out is a system-context resubmit from server-side code, which opens the next round pending, again with no submitter, where the system or the admin override can recall it. It also says what an author should do on such a flow: keep the revise edge into its approval_revise node only when server-side code will make that resubmit, and otherwise leave the edge off, so send-back is refused and approvers reject instead. content/docs/capabilities/approvals.mdx only names send-back in one line and does not teach recall or resubmit, so it is unchanged.
  • packages/spec/src/contracts/approval-service.ts, comments only:
    • recall: a closing paragraph saying the returned arm has no one for a request with no submitter, and stating the way out above.
    • sendBack: a closing paragraph saying such a request is sent back the same way, that only a system-context resubmit moves it on, and that a flow whose trigger resolves no user declares revise only when server-side code will make that call.
    • resubmit: "submitter-only" now reads "by the submitter or a system context (the system context alone on a request with no submitter)". The code already admits the system context, and ApprovalResubmitInput.actorId already said so.
  • .changeset/29910-no-submitter-returned-docs.md: '@objectstack/spec': patch. The docblock text ships: after a build it is in packages/spec/dist/contracts/index.d.ts and index.d.mts (positive control: the existing sentence "refused there exactly as any other non-submitter" is found in the same two files). So this is a published, comment-only change, which takes a patch changeset rather than skip-changeset.

The documented path, read from code at 987ff33b

  • No submitter: packages/plugins/plugin-approvals/src/approval-node.ts:107-111 (runTriggerUser, null when the trigger resolved no user) feeds submitterId at approval-node.ts:199.
  • Send-back is allowed and lands on the window: approval-service.ts:6041 (pending approver or system, no submitter test) and assertReviseEdge at approval-service.ts:6278-6312 (the revise edge must exist and must target an approval_revise node). approval-revise-node.ts:89-98: the window arms nothing, and only resubmit or a recall ends it.
  • Nobody recalls it on returned: approval-service.ts:5613-5618 (overrideAdmits requires status === 'pending'; actsAsSubmitter at approval-service.ts:1114-1117 answers false for every actor when submitter_id is null).
  • A system-context resubmit is admitted: approval-service.ts:6202 (if (!context.isSystem && !actsAsSubmitter(raw, actorId))), pinned at approval-service.test.ts:6418-6423. Its resume carries no resumedBy (approval-service.ts:6241-6248), so the run context is not switched (service-automation/src/run-resume.ts:326-328). The re-entered approval node opens round N+1 through approval-node.ts:199 again, with no submitter (also pinned for a later step by later-step-original-submitter.integration.test.ts:322).
  • The new pending round is recalled by the system or the override actor: approval-service.ts:5613-5614 with isOverrideActor at approval-service.ts:2111-2113 (context.isSystem always passes). Pinned at approval-service.test.ts:6403-6416.
  • No console door: sys-approval-request.object.ts:766-778 shows Resubmit only to viewer.is_submitter, and Recall on returned only to the submitter (can_override is pending-only). The REST /resubmit route (packages/rest/src/approvals-routes.ts:518-523) passes the caller's own resolved context.

Tests

No test was added: nothing a test can pin changed. The behaviour documented here is already pinned by the approval-service.test.ts battery cited above.

Acceptance notes

  • A system-context resubmit is reachable only from server-side code. The console offers no button for it, and the REST route acts as its caller. The guide says "server-side code in a system context" and does not name any other door.
  • Comments-only proof: packages/spec/src/contracts/approval-service.ts was tokenised with the TypeScript scanner (comments skipped) at 987ff33b and at this branch. Both give 1420 tokens and the streams are identical. Control: changing one actorId: string to number makes the comparison report false. So no .d.ts statement other than a comment can move. api-surface/contracts.json records only the interface names, and the spec build regenerated nothing (clean git status afterwards).

Verification

Head 74870830 (630966a2, then one merge of origin/main at a52761fd). The diff is 3 files, +37 / -1.

Local gates on 7487083: 95 derived, 95 run, all exit 0, 0 NOT MEASURED (`dispatch-gates --ran`, exit codes recorded)
  • pnpm install --frozen-lockfile (exit 0), then pnpm --filter @objectstack/spec build under the verify lock (VERDICT command-exit 0). After that, every command from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, each with its exit code captured before any pipe.
  • On the first pass 88 commands exited 0 and 7 exited 3 (PREREQUISITE NOT MET): check-plugin-teardown-shape --self-test (shallow clone), check:doc-formula-expressions, check:doc-security-posture, check:skill-examples, check:docs-transcript-drift, check:dual-build-cjs-loads and check:lean-entry-closure (unbuilt workspace packages). I unshallowed the clone and ran turbo run build --concurrency=2 --filter='!@objectstack/docs' under the lock (VERDICT command-exit 0, 119/119 tasks). All 7 then exited 0.
  • --ran with COMMAND :: exit N records: 95 derived famil(ies) accounted for — 95 run, 0 NOT-MEASURED (a DERIVED zero — all 95 recorded an exit code and none of them is 3).
  • NODE_OPTIONS=--max-old-space-size=4096 pnpm --filter @objectstack/spec check:generated: exit 0, All 14 generated artifacts are up to date. A first run at the default heap went red only because check:test-typecheck's tsc ran out of memory ("failed without diagnostics", heap OOM), not because an artifact was stale.
  • pnpm --filter @objectstack/spec typecheck: exit 0. node scripts/check-dev-prereqs.mjs: exit 0. node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD: exit 0.
  • Not run locally: @objectstack/spec's vitest suite. Every non-comment token is unchanged (see Acceptance notes), so it is left to CI's Test Core.
  • Writes: git push (empty branch, 630966a2, 74870830); one relay pr_create; no GitHub MCP call.

Generated by Claude Code

🤖 Generated with Claude Code

The approvals guide and the IApprovalService recall / sendBack /
resubmit docblocks now state the path for a request whose run's trigger
resolved no user: nobody can recall it once returned, a system-context
resubmit is its way out, and the next round opens pending with no
submitter, where the system or the override actor may recall it. The
guide says when such a flow should declare its revise edge. Comments
and prose only; no behaviour change.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tooling labels Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 1 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/approvals.mdx (via IApprovalService (symbol, a top-level interface))
What this run could not see

Coarse fallback — 170 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 55337f4ce35dbe62434b23517547a0b5052b936c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from bac9d60dc1368016061b7b59f2733c0c169f1001 — the merge of head f4d534bd32cb18f6a350e67a7eb88469d78b53d2 into base 55337f4ce35dbe62434b23517547a0b5052b936c, 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 bac9d60dc1368016061b7b59f2733c0c169f1001 && git checkout bac9d60dc1368016061b7b59f2733c0c169f1001
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55337f4ce35dbe62434b23517547a0b5052b936c f4d534bd32cb18f6a350e67a7eb88469d78b53d2 && git checkout -B drift-repro 55337f4ce35dbe62434b23517547a0b5052b936c && git merge --no-ff f4d534bd32cb18f6a350e67a7eb88469d78b53d2

node scripts/docs-audit/affected-docs.mjs --json 55337f4ce35dbe62434b23517547a0b5052b936c

⚠️ 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 55337f4ce35dbe62434b23517547a0b5052b936c → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8

Contract-review-claim: session_017ihL2ebbZkr2QAEG1x7UC8 · head 7487083 · 2026-11-04T15:34Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 74870830a7908f70235f91a0a11b07fef65dd41b
Local-runs: none
Scope: packages/spec/src/contracts/approval-service.ts + packages/plugins/plugin-approvals/src/approval-service.ts (recall 5577-5625, sendBack 6032-6140, resubmit 6186-6260, assertReviseEdge 6278-6312, actsAsSubmitter 1114-1117, isOverrideActor 2111-2165, resolveActor 2254-2262, serviceResume), packages/plugins/plugin-approvals/src/approval-node.ts (runTriggerUser 107-112, submitterId 199), packages/plugins/plugin-approvals/src/approval-revise-node.ts 70-110, packages/plugins/plugin-approvals/src/approval-service.test.ts 6298-6423, packages/plugins/plugin-approvals/src/later-step-original-submitter.integration.test.ts 322-330, packages/plugins/plugin-approvals/src/sys-approval-request.object.ts 663-778, packages/rest/src/approvals-routes.ts 440-523, packages/rest/src/rest-server.ts 2448-2491, packages/services/service-automation/src/run-resume.ts 190-197 and 320-345, packages/services/service-automation/src/runtime-identity.ts 209-230 and 500-530, packages/services/service-automation/src/engine.ts 5950-6060, packages/spec/src/contracts/automation-service.ts 1738-1744 (cancelRun). Remainder of the diff (content/docs/automation/approvals.mdx, .changeset/29910-no-submitter-returned-docs.md) is the lane seat's ACCEPT at TIER_DEFAULT; the guide paragraph was read only to check it says the same as the docblocks.

① Derived judgments

Read from the tree at 74870830, not from the PR body.

  • Accept set: unchanged — right. The only hunks in packages/spec/src/contracts/approval-service.ts are inside three JSDoc blocks (recall +6 lines, sendBack +5 lines, resubmit 1 sentence rewritten). No key, type, enum, parameter or return shape moves; IApprovalService.recall / sendBack / resubmit signatures are byte-identical to origin/main. Nothing a consumer compiles against changes.
  • Public surface: prose only, but published — right that it is treated as published. The spec package builds .d.ts through tsup's dts pass (packages/spec/tsup.config.ts:476, dts: { only: true, ... }), which carries JSDoc; the amended sentences therefore ship in @objectstack/spec's declaration files. That is why a changeset is owed (see ②), not skip-changeset.
  • recall docblock claim "A request with no submitter ... has no one on the returned arm" — right. actsAsSubmitter (approval-service.ts:1114-1117) answers false for every actor when submitter_id is null; overrideAdmits (5613-5614) is ANDed with raw.status === 'pending', so on returned neither the system context nor the override actor passes. Pinned at approval-service.test.ts:6403-6416 (tenant admin refused recall of a returned no-submitter request) and 6361-6395 (reader/approver refused).
  • Claim "Its way out of returned is a system-context resubmit" — right, with one completeness gap (④ S2/S4). resubmit (6202) admits context.isSystem regardless of submitter; pinned at approval-service.test.ts:6418-6423. isSystem is never resolved from inbound HTTP (rest-server.ts:2448, 2488) and the REST /resubmit route passes the caller's own context (approvals-routes.ts:518-523), so the call is server-side only, as the docblocks imply and the guide states.
  • Claim "opens the next round pending, again with no submitter" — right. The send-back resume (6140-6146) and the resubmit resume (6241-6248) carry no resumedBy; serviceResume adds only the RESUME_AUTHORITY_SERVICE marker; deciderSegmentContext (run-resume.ts:326-327) switches identity only on a resumedBy. The run context therefore stays the trigger's, and the re-entered approval node stamps submitterId: runTriggerUser(context) (approval-node.ts:199), which is null for a context with no userId / a recorded triggerUserId: null (107-112). The exact round-N+1 case is not pinned by a test (the integration pin at later-step-original-submitter.integration.test.ts:322 covers a later step, not a resubmitted round); the code path is unambiguous.
  • Claim "where the pending arm's system context or override actor may recall it" — right. 5613-5618 with isOverrideActor (2111-2113: context.isSystem passes; platform/tenant admin by posture or grant; a null-org request may be released by any tenant admin). Pinned at approval-service.test.ts:6403-6416 and 6418-6421.
  • sendBack docblock claim "A request with no submitter is sent back the same way" — right. sendBack (6032-6050) tests the actor against the pending slate or isSystem, never the submitter; assertReviseEdge (6278-6312) requires a revise edge into an approval_revise node. Pinned by the test battery's sendBack helper on no-submitter requests (6341-6346).
  • resubmit docblock rewrite "by the submitter or a system context (the system context alone on a request with no submitter)" — right, and it now agrees with ApprovalResubmitInput.actorId's existing text (L825) and with the code comment at 6201.
  • Guide paragraph vs docblocks — consistent. The guide's trigger list (schedule, time-relative, api, record-change fired by a system write) matches the engine's own statement (runtime-identity.ts:508-514; run-resume.ts:191-197). One guide-only nit for the lane seat, outside ④: the guide says "IApprovalService.resubmit with isSystem: true" but resolveActor (2254-2258) throws VALIDATION_FAILED: actorId is required for a system context that names no actor, so the call also needs actorId (the battery passes SYSTEM_ACTOR).
  • Nothing else in the diff implies a judgment. No generated artifact moves (api-surface/contracts.json records interface names only; the dev reports check:generated clean; Spec property liveness and Check Changeset are green on the head).

② Semver level

  • Changeset .changeset/29910-no-submitter-returned-docs.md: '@objectstack/spec': patch — matches what the diff publishes. AGENTS.md Post-Task Checklist §3: skip-changeset is for a diff that publishes nothing from any released package; this diff ships amended JSDoc in @objectstack/spec's .d.ts output, so patch is the floor and the ceiling (no kind, no key, no behaviour).
  • Clause-②: no on both the PR body and the changeset body — right: no capability is widened or narrowed at any consumer; the three verbs admit exactly whom they admitted on main. No (widening) / (narrowing) arm, so no narrowing face and no BREAKING banner is owed.
  • Metadata-protocol: none on both — right: nothing a metadata author can write changes.
  • Check Changeset on the head: success.

③ Boundary flags

  • Dev flag (os-dev-report, out_of_scope_findings, repeated in the PR's Acceptance notes): "the documented way out works in code but has no door a user can reach (a system-context resubmit needs server-side code). Is a guide line that relies on server-side code enough to satisfy option D?"
    Answer against the ruling text: yes, and it is the only thing D permits. The card quotes D verbatim as "leave the behaviour as it is and document the way out (chosen)", and its own "What" section names that way out as "a system-context resubmit opens a fresh pending round". The card closes with "⛔ No behaviour change." A system context is server-side by construction (rest-server.ts:2448: "isSystem flags are never set on inbound HTTP"), so the way out the maintainer accepted is a server-side one; opening a console or REST door for it would be a behaviour change the ruling forbids and would re-open the A/B debate the ruling closed. The documentation therefore satisfies D exactly when it says plainly that the exit is server-side code in a system context — which the guide does in those words and the sendBack docblock does ("only when server-side code will make that call"). Not escalated. If the maintainer later wants a user-reachable door, that is a new card with its own ruling, not a defect of this PR.
  • Dev deviations: spec vitest not run locally (left to CI Test Core, in progress at review time — see below); git fetch --unshallow in the shared store. Neither touches the tree under review.
  • No open_questions entry on the card beyond the flag above. The triage comment (6602125866) carries no question. The claim comment's Premise: holds was re-checked: on origin/main the recall input docblock (L665-668) and the recall method docblock (L1035-1040) still say returned is the submitter's alone, and no guide paragraph on main states the no-submitter path.
  • Check-runs on 74870830 at review time: success — Check Changeset, Check PR Size, Governed Surface Queue Guard, PR-body guards, Spec property liveness, Check Documentation Links, Flag docs affected by code changes, Build Docs, Build UI Deps, Live E2E UI, Dogfood Regression Gate, Lint & Repo Gates and every Lint · family, Type Check · source gates, Type Check · debt ledger, Plan Test Core shards, Auto Label, filter. Still in progress — Test Core (1-8/8), Test UI (1-16/16), Build Core, Build & E2E UI, Type Check · workspace, Type Check · consumer gates, Type Check UI. Skipped (opt-in or not applicable) — Dogfood Verify CLI, UI Published Artifacts, Temporal Conformance, Packed-tarball smoke, 18.x upgrade harness, Lint UI, Schedule gate. No check-run has failed. The in-progress families cannot be contradicted by a comments-only spec diff (non-comment token stream unchanged, verified by reading the hunks), but their conclusions are the gate verdicts and the seat should see them green before landing.

④ Design judgment

hits file: packages/spec/src/contracts/approval-service.ts. Judged against the code it cites on origin/main and against ruling D as quoted on the card.

Attempts to break it, each answered by the tree:

  1. Is anyone admitted on the returned arm after all? No — actsAsSubmitter fails closed for every actor; overrideAdmits is pending-only; inReviseWindow only widens the state check. Test battery 6361-6416 pins every reader class.
  2. Could the REST /resubmit route be the door the docblock says does not exist? No — it forwards context as resolved by resolveExecCtx, which never sets isSystem (rest-server.ts:2448, 2488); the console approval_resubmit action is gated on viewer.is_submitter (sys-approval-request.object.ts:775), and attachViewers cannot answer true for a null submitter.
  3. Could the system-context resubmit itself be refused? It passes 6202; it still owes assertLatestForRun, the colliding-pending check and assertRunResumable, which are the same guards every resubmit owes and the docblock's pre-existing text already covers ("LATEST returned request of its (run, node)"). resolveActor needs an actorId for a system caller — the contract's ApprovalResubmitInput.actorId is already a required string, so the docblock is not wrong, only the guide's shorthand is loose (noted in ①).
  4. Does round N+1 really open with no submitter, or does the system caller become the submitter? No switch happens (no resumedBy on either resume; runTriggerUser never falls through to the decider: approval-node.ts:103-105 "⛔ Not triggerUserId ?? userId"). The claim holds for runAs:'user' and runAs:'system' flows alike.
  5. Is a system-context resubmit the ONLY exit? For the request row and the flow's forward path, yes. An operator IAutomationService.cancelRun (automation-service.ts:1744, optional) also ends the parked run, but it discards the branch and leaves the row returned; it has no REST route either (none in packages/rest/src). So the docblock's sentence is true but not complete — see S2.

Must-fix: none.

Should-fix:

  • S1 — the same published file now contradicts itself on resubmit. The reissueContinuation docblock (head L1145-1147) still says "{@link resubmit} is still no way back — it is submitter-only", while the amended resubmit docblock (L1070-1072) says "by the submitter or a system context". One phrase ("submitter-or-system, and it owes the resubmit edge ...") restores consistency; the second half of that sentence (the edge argument) is still the load-bearing reason and stays.
  • S2 — the sendBack author rule is under-scoped for the common user-less case. "a flow whose trigger resolves no user declares the revise edge only when server-side code will make that call" is followable for schedule / time_relative / api triggers (always user-less) but not for a record_change flow, which the engine names the most common user-less run (runtime-identity.ts:508-514: fired by a system write it carries no user; fired by a user's write it carries one). An author of such a flow cannot drop the revise edge without taking send-back away from every user-submitted round. The docblock should say: for a trigger that never resolves a user, declare revise only when server-side code will resubmit; for a record-change flow, a round opened by a system write that is sent back parks until server-side code resubmits it (or an operator cancelRun discards the run, leaving the row returned). The guide's parallel sentence inherits the same gap; lane seat's call there.
  • S3 — pre-existing, two lines above the inserted paragraph, not this PR's hunk. The recall docblock's returned arm (L1037-1038) says "the run resumes down reject the same way", but a recall in the revise window calls cancelRun (approval-service.ts:5683-5694) and the guide says "A recall from the revise window cancels the run" (approvals.mdx:770-771). Since the PR edits this very docblock, folding the one-line correction in is cheap; otherwise it is a separate docs card. Not counted against this PR.

Verdict on the design: the three docblock additions are true at every cited point in the live tree, say nothing the ruling did not accept, and change no accept set. The gaps found are completeness and consistency of published prose, none of which misleads a consumer into a call the code refuses or omits a refusal the code makes — should-fix, not must-fix.

Implemented-by: claude/issue-29910-no-submitter-returned-docs
Reviewed-by: session_017ihL2ebbZkr2QAEG1x7UC8

VERDICT: PASS

Scope the "declare revise only with a server-side resubmit" rule to the
triggers that never carry a user (schedule, time-relative, api), and say
what a record-change flow fired by a system write faces: the sent-back
round waits for that resubmit, or an operator cancel discards the run
and leaves the request returned. The recall docblock's returned arm now
says the run is cancelled (the revise window has no reject edge), the
reissueContinuation docblock names the system context resubmit admits,
and the guide says a system-context resubmit must name an actorId.
Comments and prose only.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f4d534bd32cb18f6a350e67a7eb88469d78b53d2
Local-runs: none
Scope: packages/spec/src/contracts/approval-service.ts + packages/plugins/plugin-approvals/src/approval-service.ts (recall revise-window arm 5680-5705, resolveActor 2254-2258, resubmit 6202), packages/services/service-automation/src/run-resume.ts (cancelRun 883-925, user-less warning 190-197), packages/services/service-automation/src/runtime-identity.ts 500-530, packages/services/service-automation/src/plugin.ts 2035-2047 (record-change hook session), packages/runtime/src/domains/automation.ts 1195-1260 (isRunLifecycleWrite, platform-operator gate), packages/runtime/src/dispatcher-plugin.ts 2006-2008 and 961 (cancel route under /api/v1), packages/spec/src/contracts/automation-service.ts 1738-1744 (cancelRun). Remainder of the delta (content/docs/automation/approvals.mdx) is the lane seat's ACCEPT at TIER_DEFAULT; read to check it says the same as the docblocks.

This is a DELTA record over the PASS rendered on 74870830a7908f70235f91a0a11b07fef65dd41b. Judged: git diff 74870830 f4d534bd (2 files, +21 / -11; one commit, parent 74870830, no merge of main). Everything that record found true at 74870830 is untouched by the delta and is not re-argued here.

① Derived judgments

  • Accept set: still unchanged — right. The delta's hunks in packages/spec/src/contracts/approval-service.ts are all inside JSDoc (*-prefixed lines only; a filter of the diff for non-comment added or removed lines is empty). No signature, key, type or enum moves. The .changeset is unchanged and still '@objectstack/spec': patch.
  • Published-prose claims introduced by the delta, each checked against the live tree:
    • recall docblock, returned arm: "the run is cancelled (the revise window has no reject edge to resume down)" — right. approval-service.ts:5680-5694: if (inReviseWindow) { // the run is paused at the revise-window node, which has no reject out-edge to resume down — terminally cancel it instead ... this.automation!.cancelRun!(runId, ...) }. Replaces the pre-existing wrong sentence (my S3). Now agrees with the guide (approvals.mdx:770-771).
    • sendBack docblock: "a flow whose trigger never carries a user (schedule, time-relative, api) declares the revise edge only when server-side code will make that call" — right. runtime-identity.ts:510-511 names time_relative and api as supplying no user, and 282/354 the schedule; the engine's build-time lint flow-runas-unscoped treats these as the statically decidable user-less shapes (engine.ts:5965-5967).
    • sendBack docblock: "A record-change flow has no submitter only when a system write fired it" — right, in the engine's own words. run-resume.ts:196-197: "a record-change flow fired by a system write carries no user either (ADR-0049)"; runtime-identity.ts:508-510: user-less "most often a record-change flow fired by a write that carried no user". The record-change hook session carries the writer's userId (plugin.ts:2037), so a user's write always yields a submitter and only a no-user (system) write yields none.
    • sendBack docblock: "that round, sent back, waits for the call, or an operator's cancelRun discards the run and leaves the request returned" — right. cancelRun (run-resume.ts:883-925) only forgetSuspendedRun(run, 'cancelled') and records a cancelled history row; it never touches sys_approval_request, and plugin-approvals registers no run-cancelled listener (grep for run.cancelled / onRunCancelled / runCancelled in plugin-approvals and service-automation: none). So the row stays returned and the record stays unlocked (send-back already unlocked it).
    • reissueContinuation docblock: "{@link resubmit} is still no way back — it is by the submitter or a system context only, and it owes the resubmit edge where the stranded continuation was the revise one" — right, and it now matches the amended resubmit docblock (my S1). The load-bearing half (the edge argument) is kept verbatim.
  • Guide paragraph vs docblocks — consistent, and the two new guide-only claims hold: (i) "IApprovalService.resubmit with isSystem: true, and an actorId naming the acting identity, which a system caller must supply" — resolveActor (approval-service.ts:2254-2258) throws VALIDATION_FAILED: actorId is required for a system context with no actor (the actorId nit, resolved). (ii) "a platform operator's POST /api/v1/automation/:name/runs/:runId/cancel discards the run and leaves the request returned" — the route is mounted at ${base}/automation/:name/runs/:runId/cancel with base defaulting to /api/v1 (dispatcher-plugin.ts:2006, 961) and gated by isRunLifecycleWrite to platform-operator standing (domains/automation.ts:1195-1260: "a platform-operator verb ... a run belongs to the environment rather than to a user"). This is the one user-reachable exit for the parked run, and the guide names it correctly as the operator's door, not as a way to move the request on.
  • No new generated artifact or surface implied. Spec property liveness, Check Changeset, Governed Surface Queue Guard are green on f4d534bd.

② Semver level

  • Unchanged from 74870830: '@objectstack/spec': patch, Clause-②: no, Metadata-protocol: none on both the PR body and the changeset body — still right. The delta adds and corrects published JSDoc only; no key, type, behaviour or accept set moves; the docblock text ships in the .d.ts output (tsup dts pass), so patch is owed and skip-changeset would be wrong (AGENTS.md Post-Task Checklist §3).
  • Check Changeset on f4d534bd: success.
  • Nit, not a finding: the changeset body lists the recall / sendBack / resubmit docblocks; the delta also corrects one sentence in the recall returned arm (covered by "recall") and one phrase in the reissueContinuation docblock (not named). Harmless for a patch of comment text; the seat may add four words if it wants the changelog exact.

③ Boundary flags

  • The four items from my record on 74870830, each resolved without a new wrong claim:
    • S1 (reissueContinuation said resubmit is "submitter-only") — resolved; new wording verified above.
    • S2 (sendBack author rule under-scoped for record-change flows) — resolved; the rule is now scoped to schedule / time-relative / api, and the record-change case is stated with both real outcomes (server-side resubmit, or operator cancelRun discarding the run and leaving the row returned). The guide carries the parallel sentence and names the concrete operator door.
    • S3 (pre-existing "the run resumes down reject" in the recall returned arm) — resolved in this PR; verified against approval-service.ts:5680-5694.
    • actorId nit (guide's isSystem: true shorthand) — resolved in the guide; the spec's ApprovalResubmitInput.actorId was already a required string, so no docblock change was owed there.
  • The dev's original open question (is a server-side-only way out enough for ruling D?) was answered yes on 74870830 against the ruling text; the delta does not reopen it. The guide now additionally names the one user-reachable operator door (cancelRun), which discards rather than moves the run, so the answer stands: documenting the system-context resubmit as the way the request moves on is exactly what D ("leave the behaviour as it is and document the way out", "⛔ No behaviour change") permits. Not escalated.
  • No new dev flag or open_questions entry accompanies the delta commit (the card carries no new comment for f4d534bd at review time).
  • Check-runs on f4d534bd at review time: success — Check Changeset, Check PR Size, Governed Surface Queue Guard, PR-body guards, Spec property liveness, Check Documentation Links, Flag docs affected by code changes, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Lint · scoped · slot lookup & corpus, Auto Label, filter. In progress — Build Core, Build Docs, Build UI Deps, Live E2E UI, Dogfood Regression Gate, Lint · ESLint, Lint · docs & code guards, Lint · repo & tooling gates, Lint · CI release & engine gates, Lint · scoped · query-options erasure, Type Check · workspace / source gates / debt ledger / consumer gates, Test Core (1-3, 5-8 of 8), Plan Test Core shards. Queued — Lint & Repo Gates, TypeScript Type Check, Type Check UI, Test UI (all 16), Test Core (4/8), Build & E2E UI. Skipped (opt-in or not applicable) — Dogfood Verify CLI, UI Published Artifacts, Temporal Conformance, Packed-tarball smoke, 18.x upgrade harness, Lint UI, Schedule gate. No check-run has failed. The same families were green or in progress with no failure on 74870830; a comments-only delta cannot move them, but their conclusions are the gate verdicts and the seat should see them green before landing.

④ Design judgment

hits file: packages/spec/src/contracts/approval-service.ts, the delta's three docblock edits, judged against the code they cite on origin/main and against ruling D.

Attempts to break the new sentences:

  1. Does the revise-window recall really cancel rather than resume down reject? Yes — approval-service.ts:5680-5694 calls cancelRun, and assertReviseEdge only ever requires a revise edge; nothing gives the approval_revise node a reject out-edge. The corrected sentence is true.
  2. Is "schedule, time-relative, api" the complete set of always-user-less triggers? It is the engine's own set (runtime-identity.ts:508-511, engine.ts:5965-5967); autolaunched and screen flows start from a caller and carry one. manual runs carry the caller. No omission found.
  3. Can a record-change flow lack a submitter on a user's write? No — the hook session carries the writer's userId (plugin.ts:2037); runTriggerUser reads context.userId when no switch has recorded triggerUserId. Only a write with no user yields a null submitter, which is what the sentence says.
  4. Does cancelRun flip the returned row to cancelled (which would make "leaves the request returned" wrong)? No — cancelRun writes only the suspension store and the run history; the cancelled request status is written by the record-deleted sweep (approval-service.ts:5975-5985), a different path. No approvals listener on run cancellation exists.
  5. Is "an operator's cancelRun" reachable, or is it the same server-side-only shape as the resubmit? It is reachable: POST /api/v1/automation/:name/runs/:runId/cancel, gated to platform-operator standing. The docblock says "an operator's cancelRun", which is the contract verb (automation-service.ts:1744) the door calls; the guide gives the route. Consistent, and it does not overstate what the door does (discard, not move on).
  6. Does the new text widen or narrow any accept set a consumer could read off the contract? No — every sentence describes who the implementation already admits or refuses, with no new verb, parameter or status.

Must-fix: none.

Should-fix: none. (Nit only, in ②: the changeset body could name the reissueContinuation phrase it now also touches.)

Verdict on the delta: each of the four items from the 74870830 record is resolved by text that is true at every cited point in the live tree; no new claim is wrong; the accept set, semver level and declarations are unchanged and still right. The PASS stands on the new head.

Implemented-by: claude/issue-29910-no-submitter-returned-docs
Reviewed-by: session_017ihL2ebbZkr2QAEG1x7UC8

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8

席位核对 — ACCEPT · domain:devx#2 · Head-sha f4d534bd32cb18f6a350e67a7eb88469d78b53d2 · 2026-11-04T16:31Z

  • Shape: 3 files, +50/−4 (read from the PR's files list): packages/spec/src/contracts/approval-service.ts (+20/−4, JSDoc lines only), content/docs/automation/approvals.mdx (+20), .changeset/29910-no-submitter-returned-docs.md (+10). No signature, key, type or enum moves.
  • Against the ruling (option D: leave the behaviour, document the way out): the sendBack / recall / resubmit / reissueContinuation docblocks and the guide say how a request returned to a run with no submitter moves on (a system-context resubmit with an actorId) and what the operator's cancelRun does (discards the run, leaves the request returned). No behaviour change in the diff.
  • Contract review (packages/spec/src/**): Fable PASS on 74870830 (6635054232) with three should-fixes and an actorId nit; the patch round (report 6635625166) resolved all four; the same reviewer's delta PASS on this head (6635874795) verified each new sentence against the live tree, no must-fix, no should-fix.
  • Gates: every check-run on the head is success (57) or skipped (8); check-expected-skips OK, all 8 skips in main's roster. Local dispatch-gates --commands exit 0 with --ran reconciled (reports 6634778542, 6635625166).
  • Accepted deviations: the PR body's Verification section names the round-1 head 74870830; the delta is comments-only and the check-runs on this head answer the gates. The changeset body does not name the one-phrase reissueContinuation fix (reviewer's nit, harmless for a comment patch).
  • Out of scope: Skills-docs: n/a, Clause-②: no, Metadata-protocol: none.
  • Landing: '@objectstack/spec': patch (docblocks ship in the .d.ts). Ready, then auto-merge through the merge queue.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 4, 2026 16:33
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit 471add3 Nov 4, 2026
66 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-29910-no-submitter-returned-docs branch November 4, 2026 17:01
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/s tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs(approvals): document how a request with no submitter leaves the returned state

1 participant