Repository navigation
feat(spec)!: refuse at publish a save_notice that a suspending node reaches - #28838
Conversation
A save_notice reachable along the flow's edges from a node that always suspends the run (screen, wait, approval, approval_revise) is refused at the flow parse, naming that node. On that path the write has already answered, so the node can only refuse at run time and, after an approval, the decision strands its run. The persisted round-trip test now asks its approval through a subflow, the shape the parse cannot see, and pins the parse refusal at registerFlow. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5omF1nCbQTGgW1nbRA
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5omF1nCbQTGgW1nbRA
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 168 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 0d38f4407e18802119b2cb28bac5dfba9f193332 && git checkout 0d38f4407e18802119b2cb28bac5dfba9f193332
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2a4c0034ed828e7b2203bcf120e484c52e3b6088 bb9723f5d03561fa6aa5a69e11af7b4777b27712 && git checkout -B drift-repro 2a4c0034ed828e7b2203bcf120e484c52e3b6088 && git merge --no-ff bb9723f5d03561fa6aa5a69e11af7b4777b27712
node scripts/docs-audit/affected-docs.mjs --json 2a4c0034ed828e7b2203bcf120e484c52e3b6088
|
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Must-fix: none. Check-runs on the head at review time: 26 success, 13 skipped, 0 failure, 27 still in progress ( Implemented-by: VERDICT: PASS |
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH 席位核对 · ACCEPT · PR #28838 · 2026-11-01T23:40ZHead-sha: 维护者速读(席位终稿)
核对六项
|
domain:server#3 · os-project-manager · session_01FEkQ5omF1nCbQTGgW1nbRA
Fixes #28806
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: follow-up #28787 —
skills/**does not teachsave_noticeyet; that open card carries the teaching, which should state this placement rule beside the trigger rule维护者速读
save_notice节点能沿连线从approval、wait、screen或approval_revise之后到达,现在发布时就会被拒绝,返回INVALID_METADATA422,位置在nodes.<i>.type。以前它能通过发布,但每次审批决定都会让流程卡住(RESUME_FAILED)。save_notice挪到它前面,或者在它后面改用notify节点。save_notice照常通过。FLOW_PAUSE_CAPABLE_NODE_TYPES。subflow、map只在子流程挂起时才挂起,发布时看不到,所以不计入,仍由运行时按名字拒绝。save_notice前面没有挂起节点,HotCRM main 里没有save_notice。Metadata-protocol实测为narrowed(派单预计none):按 AGENTS.md,作者能写的值被拒就是 narrowed,因此 spec 是minor。详见下文和报告的 deviations。What changes
Spec: the placement door refuses a
save_noticethat a suspending node reaches (packages/spec/src/automation/save-notice-placement.ts). After the trigger check passes, the door walks the flow's top-leveledges. It takes every node that a node of an always-suspending type reaches from its successors, where that suspending node is itself reachable from the start. Asave_noticein that set is refused:FLOW_PAUSE_CAPABLE_NODE_TYPES(screen,wait,approval,approval_revise), the list of types that pause on every execution.flow.zod.tspasses it in, so the door's module imports nothing back fromflow.zod.ts.subflowandmapdeclaresupportsPause: truebut pause only when the child flow they name pauses. That is a different record, which the parse does not hold, so they are not counted. That is the same classification ruling D made for region bodies. Asave_noticeafter one still meets the run-time refusal by name.save_noticeinside aloop/parallel/try_catchbody is judged by its top-level container, and the refusal is anchored at the nested node's own path.ctx.addIssue({ code: 'custom' })atnodes.<i>.type, which the metadata door answers asINVALID_METADATA422, the same as the trigger case.objectstack validate, the save door andregisterFlowrefuse it alike.Test re-anchored:
plugin-approvals/src/save-notice-approval-round-trip.integration.test.ts(#28786). Its flow placed the secondsave_noticeright after the approval, which is the shape this PR refuses, soregisterFlownow throws in its setup. The test keeps its purpose:subflow(start → save_notice → subflow(approval) → save_notice). The parse accepts that graph. The parent suspends with its child, is persisted insys_automation_run, and resumes from the store when the decision completes the child.save_noticestill refuses by name. Through a subflow the decision answers the approvals' existing subflow-parent strand:resumed: true, withresumeFailure: { code: 'RESUME_FAILED', runId: <parent>, status: 'stranded' }andresumeErrornaming the refusal. It does not throw. That is existing behaviour, read and not changed.registerFlowon the original automation: save_notice follow-ups from #28251 — the array triggerType refusal wording, a persisted approval round-trip test, and the lint node-type lists #28786 graph.Why
#28806: an AI writing "notify after approval" with
save_noticeproduced a graph that published and then stranded every decision. The author now learns at publish time.Verification
spec/src/automation/save-notice-node.test.tswere red before the change (8 failed, 21 passed of 29) and green after (29/29). The 3 controls passed on both sides.metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts, automation: a save_notice placed after an approval, wait or screen node passes publish and then strands every decision with RESUME_FAILED #28806 block). In publish and draft mode they assert{ code: 'INVALID_METADATA', status: 422 },[['custom', 'nodes.2.type']], the sentence verbatim and nothing stored. The control (notice ahead of the approval) is stored.scripts/ablation-replace.mjs, restore proven byblob == HEAD;distby hand, restore proven by blob hash):registerFlowpin (specdist).subflowto the pausing list: the subflow control went red.service-automation/dist, the round-trip test went red (blob505babccrestored).examples/**andpackages/dogfood: onesave_notice(examples/app-showcaseshowcase_task_completed:start → script → save_notice → notify → end), with no suspending node. HotCRMorigin/main(a598256): nosave_notice.dispatch-gates.mjs --commandsderived 84;--ranreconciles 84 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0.vitest runof@objectstack/spec,@objectstack/metadata-protocol,@objectstack/plugin-approvals,@objectstack/service-automation,@objectstack/trigger-record-changeand@objectstack/example-showcase: exit 0 each. The round-trip file's two cases run in about 0.3 s of test time.(narrowing)arm), on headbb9723f5underos-verify-lock.sh: closure buildVERDICT command-exit 0;pnpm --filter @objectstack/dogfood testVERDICT command-exit 0, Test Files 380 passed and 3 skipped (383), Tests 3114 passed and 28 skipped (3142), no failure.eslint --no-inline-configoverpackages/spec,packages/metadata-protocolandpackages/plugins/plugin-approvalsexits 0.Acceptance notes
Metadata-protocol: narrowed, not thenonethe order expected. AGENTS.md defines "a value refused" asnarrowed, and a flow graph an author could save before is now refused at the save door. The seat-1 contract review on fix(metadata-protocol)!: refuse an object whose datasource names no datasource at save and publish #26912 ruled the same way for a save door refusing what it stored before. Under ruling A the level follows the kind, so@objectstack/specisminor. No other package's shipped files move.RESUME_FAILED/ stranded-decision posture, the save-notice contract and the wirenoticeare unchanged. Noui/**and no hotcrm changes.save_noticeafter one still meets the run-time refusal.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: no generated artifact moves (check-generatedand the spec artifact gates pass on this head).content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code