Repository navigation
feat(spec): an approval step's excludeSubmitter option, default off (protocol half of #25573) - #25632
Conversation
…col half) Adds the step-level excludeSubmitter key to ApprovalNodeConfigSchema, optional (absent when off), with a describe that opens with the planned marker: the approval engine does not read it yet. Claude-Session: https://claude.ai/code/session_01PC4NPXPgPmpDRG9DdDnyXT Co-authored-by: Claude <noreply@anthropic.com>
…low ledger note The key parses on the node config and stays absent when off; it is refused on an approver row, on the flow node and on the flow. The ledger walk ends at flow/nodes.config, so the planned record lives in flow.json's _note. Claude-Session: https://claude.ai/code/session_01PC4NPXPgPmpDRG9DdDnyXT Co-authored-by: Claude <noreply@anthropic.com>
…at collection Claude-Session: https://claude.ai/code/session_01PC4NPXPgPmpDRG9DdDnyXT Co-authored-by: Claude <noreply@anthropic.com>
…ludeSubmitter Claude-Session: https://claude.ai/code/session_01PC4NPXPgPmpDRG9DdDnyXT Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PC4NPXPgPmpDRG9DdDnyXT Co-authored-by: Claude <noreply@anthropic.com>
…proval-exclude-submitter
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 98054b2ce24fd0165ad7aa140f9147370a3971e4 && git checkout 98054b2ce24fd0165ad7aa140f9147370a3971e4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 42d9fe8f5be01cf8822c324af6142103193199fe 4823c579361ad3bfd8025170edac4fabe1ab6a4c && git checkout -B drift-repro 42d9fe8f5be01cf8822c324af6142103193199fe && git merge --no-ff 4823c579361ad3bfd8025170edac4fabe1ab6a4c
node scripts/docs-audit/affected-docs.mjs --json 42d9fe8f5be01cf8822c324af6142103193199fe
|
席位核对ACCEPT · #25609 ·
Generated by Claude Code |
|
The decision card named in Generated by Claude Code |
Contract reviewServed-tier: On the scope: Inputs: card #25609 (body; comments 6382817354, 6383187041, 6383750916), rulings 6377731715, 6377842908 and the reconciliation 6377965040 on #25496, runtime card #25573 (body; comments 6382188555, 6382833455), the PR body, its file list and the net diff against ① Derived judgmentsAccept-set changes the diff implies, each judged:
The describe's statements, against the cited code:
The marker and the ledger:
② Semver level
③ Boundary flagsDev report 6383750916, each flag answered: open_questions
out_of_scope_findings
deviations
Check-runs on the head (46 runs), read at 2026-10-24T12:05Z: every concluded run is Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #25609
Clause-②: yes (widening)
Metadata-protocol: added
Ruling-ref: 6377731715
The protocol half of #25573: an approval step's
excludeSubmitteroption, default off. #25573 (the runtime half inplugin-approvals) remains open and isBlocked-by:this card.The ruling this implements
Ruling 6377731715 on #25496 (batch #388 item 4, the maintainer's 「同意」), item 5, verbatim:
The later ruling 6377842908 agrees on this row: 「B 步骤选项
excludeSubmitter,默认关;排除后无人可批按 ADR-0042 升级 … 默认关,不改现有审批行为;审批记录显示排除改了路由」.What changed
packages/spec/src/automation/approval.zod.ts:ApprovalNodeConfigSchemagainsexcludeSubmitter: z.boolean().optional(), placed directly afterapprovers.approversresolves to, whatever approver type produced each person. It is one switch per node: the approver row stays closed and refuses it.plugin-approvalshands the run's trigger user toopenNodeRequest(approval-node.ts,runTriggerUser) and records it assys_approval_request.submitter_id. The describe namessubmitter_id..optional(), not.default(false). A node that does not author the key parses to exactly the output it parsed to before. So stored approval nodes, and thenode_config_jsonsnapshot each request row keeps, are unchanged.[PLANNED — not yet enforced]and ends by telling the author that the approval engine does not read the key yet, so the submitter is NOT removed. It says to keep any existing guard (anexpressionapprover or a validation rule) until the engine honours the key. Percheck:doc-authoringRule 3 it carries no tracker id. The carrier, plugin-approvals: afieldorpositionapprover step has no "not the submitter" guard, so a manager who submits their own request can approve it #25573, is named in the TSDoc and in the ledger note.packages/spec/liveness/flow.json: aPLANNED RECORDsentence in_note. Why it is a note and not a row is under Measured below.authorable-surface/automation.jsonrowautomation/ApprovalNodeConfig:excludeSubmitter, andcontent/docs/references/automation/approval.mdx.authorable-defaults/is unchanged, because the key has no default.packages/spec/src/automation/approval-exclude-submitter-25609.test.ts(16 tests):trueand asfalse, stays absent when omitted, and a non-boolean is refused at the key's own path.FlowSchemaaccepts the key inside an approval node'sconfig. This is the parse thatos validate, compile and the metadata save door run.config, or on the flow is still refused, each at its own path.default.[PLANNEDmarker and namessubmitter_idandonEmptyApprovers..changeset/25609-approval-exclude-submitter.md:@objectstack/specminor, with both declaration lines.Measured (premise and the PM's mechanism assumptions)
origin/mainf7b0298968.git grep excludeSubmittergives 0 hits. Today the key is refused at every placement: insideconfig(customatnodes.1.config.excludeSubmitter), on the node (unrecognized_keysatnodes.1), on the flow (unrecognized_keysat the root) and on an approver row (customatnodes.1.config.approvers.0.excludeSubmitter).flowwalk ends atnodes.config, which isz.record(z.string(), z.unknown()), andchildShape()does not descend into it. I plantedchildren: { excludeSubmitter: { status: 'planned', authorWarn: true, … } }underflow.jsonnodes.configand rancheck:liveness. It exited 1 with✗ 1 UNCLASSIFIED … flow/nodes.config (declared children but property is not a container). I restored the file and confirmed the blob hash matched HEAD.authorWarnrow could reach this key's authors either.onEmptyApprovers,fallbackApprovers, …) has a row for the same reason.liveness/page.json'sPLANNED RECORDfor a component-level key): a_notesentence naming the carrier, plus the[PLANNED — …]describe marker. The checkable record is the authorable-surface row.check:livenessis green.Tests and proofs
Head:
4823c57936(the spec suite and typecheck were read there; the ablations ran on the pre-merge commits named below and touch no merged file).pnpm --filter @objectstack/spec test:Test Files 715 passed (715),Tests 21703 passed | 1 todo (21704).pnpm --filter @objectstack/spec typecheck: exit 0, includingcheck:test-typecheck, which compiles the new test file.scripts/ablation-replace.mjs, I renamed the declared key toexcludeSubmitterAblated: anchor count 1 → 0, blob27c795830a90→50ecb7c5d102. Result:Tests 10 failed | 6 passed (16). Every test that needs the key went red (§1 ×6, §2, §4, §5 ×2). The §2 control, the four §3 refusals and §1 "omitted stays absent" stayed green. The tool restored the file (blob == HEAD,git diff HEADempty).it(commitc6f43c7960) and re-ran the ablation for the per-test result above..optional()to.default(false). Result:Tests 2 failed | 14 passed: §1 "omitted stays absent" and §4 "no default" went red. Restored the same way.distleg: the test imports./approval.zodfromsrc.Gates
All of these were read at head
4823c57936, which carries one merge oforigin/main(481ac62116) throughscripts/pm/os-regen-merge.sh, with the spec rebuilt afterwards.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 114 commands from this diff. I ran every one and captured each exit code before any pipe:distfor lint, formula, client-react or objectql in a fresh worktree):check:doc-formula-expressions,check:doc-security-posture,check:skill-examples,check:docs-transcript-drift,check:dual-build-cjs-loads,check:lean-entry-closure. I built--filter=@objectstack/lint... --filter=@objectstack/client-react... --filter=@objectstack/objectql...(turbo, 37 tasks) and re-ran them; all 6 then exited 0 with measured verdicts. For example,✓ check:dual-build-cjs-loads — 112 published require entry point(s) across 69 package(s) loadand✅ 262 prose examples type-check across 3 surface(s).dispatch-gates --rananswers✓ dispatch-gates --ran: 114 derived famil(ies) accounted for — 114 run, 0 NOT-MEASURED (a DERIVED zero — all 114 recorded an exit code and none of them is 3).pnpm --filter @objectstack/spec check:generated:✓ All 14 generated artifacts are up to date. This neededgen:docsonce, forreferences/automation/approval.mdx.check:liveness: exit 0 with the key in place. With the planted row it exited 1 (see Measured).pnpm check:dev-prereqsexit 0,@objectstack/spec … built from the sources on disk.test:repo(the diff touches norepo-project test), and the repo-wide scans led bypnpm lint. CI check-runs on this PR are the record for every family CI runs.!andClause-②is(widening).Acceptance notes
fieldorpositionapprover step has no "not the submitter" guard, so a manager who submits their own request can approve it #25573): the runtime. That means removing the submitter, the empty-slate route or escalation, and the route note on the approval record. When it lands, it removes the describe's[PLANNED — not yet enforced]marker and last sentence, and rewrites theflow.json_notesentence.excludeSubmitter: trueis refused loudly at every door. After it, and until plugin-approvals: afieldorpositionapprover step has no "not the submitter" guard, so a manager who submits their own request can approve it #25573 lands, the key parses and is carried unread, and the describe is the only thing that tells the author. A release cut in that window ships a segregation-of-duties key that does nothing. That is the order the ruling chose ("spec first, thenplugin-approvals"); I record it here so release timing can take it into account.fieldorpositionapprover step has no "not the submitter" guard, so a manager who submits their own request can approve it #25573 (no spec change made here):onEmptyApprovers: 'auto_approve'. The ruling says an emptied set takes the node'sonEmptyApproversroute and is "never a silent pass". But underauto_approvethat route is exactly a silent pass. The existing describe says it "silently waves the record through". If the submitter is the only resolved approver, the exclusion empties the slate andauto_approvewould approve the submitter's own request. Two shapes resolve it:excludeSubmitter: truebesideonEmptyApprovers: 'auto_approve', in the style ofcheckFallbackApproversPairing;fieldorpositionapprover step has no "not the submitter" guard, so a manager who submits their own request can approve it #25573: does the exclusion also screen thefallbackApproversslate and theadmin_rescueadministrators when the submitter is among them? The ruling names only "the resolved approver set". The describe says the slateapproversresolves to.os lint'sapproval-approvers-may-resolve-emptymay want to countexcludeSubmitter: trueon a single-person slate (for example, onefieldapprover) as able to resolve empty. That is plugin-approvals: afieldorpositionapprover step has no "not the submitter" guard, so a manager who submits their own request can approve it #25573's or the lint lane's call.Generated by Claude Code