Repository navigation
feat(automation): a paused screen says whether another screen follows, and the flow runner reads Next or Submit and counts steps - #26752
Conversation
Claude-Session: https://claude.ai/code/session_01WGufzh5Z4kFNVFmensPenL Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzh5Z4kFNVFmensPenL Co-authored-by: Claude <noreply@anthropic.com>
…unts for ScreenProgressSpec Claude-Session: https://claude.ai/code/session_01WGufzh5Z4kFNVFmensPenL Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 147 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 b2c0114d9305f27045f4785e168e95a469b3ffd2 && git checkout b2c0114d9305f27045f4785e168e95a469b3ffd2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 74db43ef3d64f8315af68952ec45d9c46942fbc7 094a249d92dcba0fb134ea601be98e95259eddcb && git checkout -B drift-repro 74db43ef3d64f8315af68952ec45d9c46942fbc7 && git merge --no-ff 094a249d92dcba0fb134ea601be98e95259eddcb
node scripts/docs-audit/affected-docs.mjs --json 74db43ef3d64f8315af68952ec45d9c46942fbc7
|
Contract reviewServed-tier: Inputs read: card #26657 (body, triage 6441476979, claim 6442765961, os-dev-report 6444639574), PR #26752 (body, 12-file list, net diff against merge-base Check-runs on the head at the time of this record: no run red. Completed ① Derived judgments
② Semver levelChangeset Clause-②: Metadata-protocol: ③ Boundary flagsPR author is
One thing the body says that the tree does not quite bear out, for the lane seat's TIER_DEFAULT read of the prose face (outside ④ Design judgmentpackages/spec/src/contracts/automation-service.ts —
packages/spec/src/api/automation-api.zod.ts —
Cited engine code, read to judge the contract (not reviewed):
Must-fix: none. Should-fix: the TSDoc's under-enumerated absence cases (contracts/automation-service.ts), and the label-routed-decision precision in Implemented-by: VERDICT: PASS |
The flows page opened with "every paused screen carries progress", which its own subflow paragraph and the contract contradict. Name the absent cases there and in the ScreenSpec.progress TSDoc: no flow name, a node that is not a screen of the registered flow, no start node, a child-run screen with no further screen, an older server. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzh5Z4kFNVFmensPenL
席位核对 · ACCEPT ·
|
Requested by Yibo Wang · project thread
Fixes #26657
Clause-②: yes (widening)
Metadata-protocol: none
Skills-docs: #26756
维护者速读
ScreenSpec.progress里说明「后面还有没有屏幕」(none/possible/certain),只有在每条路径都一致时才附带第几步、共几步;Console 的FlowRunner据此把按钮写成「下一步」或「提交」,并且只在总数确定时画「第 N 步,共 M 步」。progress时按钮照旧是「提交」。引擎侧只在屏幕暂停时读一次流程图。回滚即 revert 本 PR。packages/services/service-automation/src/builtin/screen-nodes.ts的readScreenProgress(分支、环、扇出与子流程的判定),packages/spec/src/contracts/automation-service.ts的ScreenProgressSpec,ui/packages/app-shell/src/views/FlowRunner.tsx的按钮与计步。pnpm --filter @objectstack/service-automation exec vitest run src/builtin/screen-progress.test.ts,预期 13 个测试全绿(线性、分支、环、审批式暂停、子流程各有一条)。What changes
The shape:
ScreenSpec.progress?: ScreenProgressSpec(packages/spec/src/contracts/automation-service.ts, with its wire spelling onTriggerFlowResponseSchemainapi/automation-api.zod.ts, bound to the interface by the existing compile-time identity pin):Why this shape:
isLastwould have to guess on a branch:falseclaims more screens follow when they may not,trueclaims the end when one may follow.'possible'says exactly what the graph knows. The runner reads both'possible'and'certain'as "Next", because a later screen can still come;'certain'stays distinct so a reader can tell the two apart without walking the graph.ScreenSpec. The three come from one computation and mean nothing apart; one optional key keeps the "no answer" state a single check.Metadata-protocol: none); no author can mis-declare it.The engine fills it (
packages/services/service-automation/src/builtin/screen-nodes.ts,readScreenProgress), reading the graph of the flow the run executes at the moment ascreenpauses:nextScreenis plain reachability for'none', and the fewest surely-pausing screens on any path to a point where the run may stop (a 0-1 BFS) for'certain'.'possible'— adecisionwith a branch that pauses on another screen and one that does not, a fault edge, a loop back. The engine never picks a branch.step/stepCountare computed over strongly connected components, as the SET of screen totals the paths give; a count is served only when that set has one member. A loop through a screen, a parallel fan-out (several unconditional successors, an inclusive decision), asubflow, or a screen the caller may already have answered makes the count vary, so it is left out.screennode that surely pauses (anobjectName,waitForInput: true, or declared fields withoutwaitForInput: false), the executor's own rule. A field-less pass-through screen is not counted. A screen whose fields the caller supplied (judgeHeadlessScreen) may not pause, so it is "maybe". A non-screen node that can pause (an approval, a wait) serves no screen (only ascreennode setsNodeExecutionResult.screen), so the stretch of screens may end there: a screen behind one is possible, never certain.subflowmay serve screens of its child, or none, so the screen before one answers'possible'with no total. A screen INSIDE a child run sees only the child graph: it keepsnextScreenwhen that graph shows another screen, and serves noprogresswhen it does not, because the parent may continue with screens of its own.The runner reads it (
ui/packages/app-shell/src/views/FlowRunner.tsx): the flat step's primary button readscommon.nextwhilenextScreenis'certain'or'possible'andcommon.submiton'none'or with noprogress; the step counter (form.stepOf, the wording the form wizard already uses) is drawn beside the flow's name only when bothstepandstepCountare served. Theobject-formstep's own button (flowRunner.saveAndContinue) is the form's and is unchanged.Docs:
content/docs/automation/flows.mdxgains "A paused screen says whether another one follows — Next, Submit and the step counter", with the authoring advice that follows from it (keep a wizard's screens on one straight path and branch after the last to get an exact counter).Tests
packages/services/service-automation/src/builtin/screen-progress.test.ts(13 pins). End to end through the engine: a linear three-screen wizard (certain 1/3,certain 2/3,none 3/3, then completes); an object-form step (none 1/1); a branched flow (possible, step 1, no total; past the branchnone 2/2); a looped flow (certainthenpossible, no counts, andcertainagain after looping back); a pause that serves no screen (none 1/1before an end,possiblebefore a screen); a pass-through screen not counted; a subflow (possiblebefore it; the child's first screencertainwith no counts; the child's last screen with noprogress). OverreadScreenProgressdirectly: parallel fan-out, fault paths, a caller-answered screen, an inclusive decision, no start node and an unknown node.packages/spec/src/api/automation-api.zod.test.ts: the trigger response keepsprogress, and refuses anextScreenoutside the closed set, a missingnextScreen,step: 0and a fractional step.ui/packages/app-shell/src/views/__tests__/FlowRunner.stepProgress-26657.test.tsx(6 pins): Next and "Step 1 of 3" oncertain; Next and no counter onpossiblewith no total; Submit and "Step 3 of 3" onnone; Submit and no counter with noprogress; a resume that pauses again relabels from the next screen; in zh the words are 下一步 / 提交 / 第 1 步,共 2 步.Acceptance notes
common.next,common.submitandform.stepOf, which every one of the ten bundles already carries (zh: 下一步 / 提交 / 第 {{current}} 步,共 {{total}} 步), so eachdefaultValueequals itsenvalue.skills/**is a Tier H governed surface; teaching the screen row ofskills/objectstack-automation/SKILL.md(and an eval) in this PR would make the whole PR Tier H. The docs half is here; the skills half is skills(objectstack-automation): the screen row does not teach that a paused screen's Next / Submit and step count are engine-served (follow-up owed by #26657) #26756.packages/spec/src/api/automation-api.zod.ts(the existingTriggerFlowDataMatchesContractidentity pin reds without it), its test, the docs page, and three regenerated spec artifacts (api-surface/contracts.json,export-origins/contracts.json, theapi/strictness count 446 → 447 for the new module-localz.object).flowNamefrom the engine's flow map (getFlow), the same map a run starts and resumes from, and checks the node is ascreenin it before answering.Verification
Head
653293e7bfor everything below except the last block. Every run went throughos-verify-lock, and each reading is the lock's ownVERDICTline.Engine (service-automation)
pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 src/builtin/screen-progress.test.ts→Tests 13 passed (13),VERDICT command-exit 0.pnpm --filter @objectstack/service-automation test→Test Files 213 passed (213),Tests 2744 passed (2744);typecheck(tsc --noEmitandcheck:test-typecheck) → OK, 0 errors.Spec
vitest run src/api/automation-api.zod.test.ts→Tests 68 passed (68);pnpm --filter @objectstack/spec typecheck→ exit 0 (VERDICT command-exit 0for the pair). TheTriggerFlowDataMatchesContractidentity pin compiles, so the wire spelling andAutomationResultagree.check:generatedfound three artifacts stale (api-surface/,export-origins/, theapi/strictness count);check:generated --fixrebuilt spec and regenerated exactly those (VERDICT command-exit 0), committed as their own commit.Runner (ui)
pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/views/__tests__/FlowRunner(ui root) →Test Files 16 passed (16),Tests 100 passed (100),VERDICT command-exit 0.pnpm --filter @object-ui/app-shell type-check(bothtsc --noEmitandtsc -p tsconfig.test.json), after building@object-ui/app-shell^...→VERDICT command-exit 0.pnpm check:new-line-citations(ui) →0 new citation(s).Ablations (one per changed behaviour, via scripts/ablation-replace.mjs)
if (fansOut || loops(own)) return { nextScreen };→if (fansOut) return { nextScreen };(anchor 1 → 0, blob1cecbf95085c→11426a9602b7).screen-progress.test.ts:Tests 1 failed | 12 passed— the looped flow, which then served a step on a screen inside a loop. Restored: blob equals HEAD,git diff HEADempty.const anotherScreen = progress !== undefined && progress.nextScreen !== 'none';→const anotherScreen = false;(blob89e6ec8b14af→862b624909c2). The new runner file:Tests 4 failed | 2 passed— every Next pin (en and zh). Restored: blob equals HEAD,git diff HEADempty.src, so nodistwas involved.Derived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat653293e7b: 108 commands, each run serially with its exit code recorded before any pipe.--ran:108 derived famil(ies) accounted for — 106 run, 2 NOT-MEASURED. All 106 exit 0. NOT MEASURED:spec check:skill-examplesandcheck:dual-build-cjs-loads, which exit 3 (prerequisite) without a whole-repopnpm build; CI measures both. PR-body gates simulated through a localGITHUB_EVENT_PATHbefore opening:check-changeset-no-major,check-adr-0087-registration,check-empty-changesetexit 0;check-closing-keyword-parity --bodybinds exactlyFixes #26657.Review follow-up at `094a249d9` (docs and TSDoc only)
progress; it and theScreenSpec.progressTSDoc now name the absent cases (no flow name, a node that is not ascreenof the registered flow, no start node, a child-run screen with no further screen, an older server).check-docs-section-name,check-issue-citations,check-affected-docs,spec gen:schemathenspec check:docs,check:docs-transcript-drift(after building@objectstack/lint...) →VERDICT command-exit 0;check-drift-comment,spec check:empty-state,check:liveness,check:variant-docs,check:docs-audit-scope,check:docs-redirects,check:docs-single-h1,check:docs-spec-enumerations,check:issue-citations→ each exit 0.Generated by Claude Code