Repository navigation
feat(spec,plugin-approvals,app-shell): the approvals get serves a request's own route, and both approval step lines draw it - #26976
Conversation
… route, flow_route ApprovalRequestRow gains flow_route: the steps the request's flow run passed (from the run's own request rows), its own step, and, for a pending request, the steps ahead the flow's graph alone decides, with `ahead` saying whether the route ends, stops at an undecided branch, or stops at a decided request. flow_steps stays the graph's trunk and its doc says a step line never draws it. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzgrMKmuKm7tjxa5v95
…nd the client-side route rebuild goes The record approvals panel drew a line rebuilt from the list read's request rows (visitedApprovalSteps), and the approval request page drew none. Both now draw the pending or bound request's flow_route as the approvals get serves it; visitedApprovalSteps is deleted and its suite is replaced by one over the served route. The step-line layout, decision panel and console served-page suites are re-pinned to the served route. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzgrMKmuKm7tjxa5v95
…de decision_progress Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzgrMKmuKm7tjxa5v95
📓 Docs Drift CheckThis PR changes 2 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
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 c5a88d1e4d0ed32fb211671f1fbbd3b3199c7e38 && git checkout c5a88d1e4d0ed32fb211671f1fbbd3b3199c7e38
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 49b505915a37270ddf12d0ed4a6fd67f9907fb74 b97005bf14417759f5fdf57e4cb90c41622774bd && git checkout -B drift-repro 49b505915a37270ddf12d0ed4a6fd67f9907fb74 && git merge --no-ff b97005bf14417759f5fdf57e4cb90c41622774bd
node scripts/docs-audit/affected-docs.mjs --json 49b505915a37270ddf12d0ed4a6fd67f9907fb74
|
Contract reviewServed-tier: Inputs: card #26668 (body; comments 6442524346, 6445757080, 6446765160, 6455118705, 6456589397), card #26563 and PR #26666 (body, files), PR #26976 (body, 18 files, the one comment 6456593091, the net diff against main at the head), the head's check-runs. Read-only throughout; no worktree, build, test or gate run. ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…the run's own rows Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzgrMKmuKm7tjxa5v95
维护者速读 (席位终稿)
席位意见:接受。契约复核 PASS(6456957540),确认「不猜路线」成立:已走步骤只来自本次运行的审批单行,后续步骤遇到判断、条件、分叉、回环或子流程即停。复核指出的一句不实注释已在 席位核对 · ACCEPT ·
|
Requested by Yibo Wang · project thread
Fixes #26668
Clause-②: yes (widening)
Metadata-protocol: none
Skills-docs: none —
flow_routeis a served read member of the approvals API, not anything a metadata author writes; no skill teachesflow_steps(git grepoverskills/andcontent/docs/: 0 hits), and the approvals guide gains a paragraph namingflow_routehere (content/docs/automation/approvals.mdx)Nothing is left of #26563, which #26666 already completed: the part #26666 had to drop, the request page's line, is drawn again here.
维护者速读
GET /api/v1/approvals/requests/:id)新增flow_route:本单所在流程运行已经走过的审批步骤(取自同一次运行自己的审批单行,不取流程图)、本单当前步骤,以及仅对待审单、仅在流程图自己就能确定时列出的后续步骤;遇到尚未决定的分支即停,并以ahead: 'undecided'明说。flow_steps(流程图主干)原样保留,文档写明步骤线不画它。业务记录审批面板与审批单页面都改画flow_route;fix(app-shell): the approval step line draws the route the request's run took, not the flow graph's first branch #26666 的客户端临时方案visitedApprovalSteps和审批单页「不画步骤线」的规则一并删除。flow_steps字节不变,老客户端不受影响;每次单条读取多一次按flow_run_id的查询(最多 1000 行,系统身份)。业务记录面板上,未参与前序步骤的查看者现在也能看到这些步骤(以前因列表读只给参与者而缺失);步骤名与主干原本就已对同一读者可见。回滚 = revert 本 PR。packages/plugins/plugin-approvals/src/approval-route.ts的stepsAhead(哪些节点会让后续步骤停为 undecided);②packages/spec/src/contracts/approval-service.ts的flow_route文档(契约原文);③packages/plugins/plugin-approvals/src/flow-route.integration.test.ts(真实引擎上的报告路线)。pnpm --filter @objectstack/plugin-approvals exec vitest run src/flow-route.integration.test.ts,预期 6 个测试全绿,其中报告路线的flow_route只有「Manager(done)· Finance(current)」两步,ahead为undecided。What changes
Measured first, on
origin/main7a2818c5.attachFlowStepswalks the graph fromstart, takingapprove, elsetrue, else the first edge, and states each step against that one trunk. On the real engine, acontract_approval-shaped flow routed manager → finance only servesflow_stepswith every trunk entryupcomingandfinance_onlyabsent (pinned as the first assertion of the reported-route case below, green on both sides on purpose). A route that rejoins the trunk (manager → legal only → executive) is served the joint rung asdone, a rung the run never visited.flow_run_id), its node (flow_node_id),statusandcreated_at; every step the run opened is its ownsys_approval_requestrow with the same four. The automation engine writes no step log for a run while it is paused (ApprovalResumeSurface.getRun: "the engine writes no execution-log entry until a run reaches a terminal state"), so the run's own request rows are the only history there is. The route is read from them.flow_steps: the record approvals panel rebuilt its line client-side from the list read (visitedApprovalSteps), and the request page drew no line. Its other readers are type declarations only:ApprovalRequestLite(app-shelluseRecordApprovals.ts),ApprovalStepLine's step type, and the console'sapprovalsApi.tsrow type. No API doc or skill names it.The shape, option (a):
flow_stepsstays the trunk, andApprovalRequestRow.flow_routeis added. Why not (b), redefiningflow_steps: the field ships in published contracts and clients outside this tree may read it as the trunk, so redefining it changes the meaning of a served member under them. Adding a member leaves every existing reader byte-identical. The two fields do disagree off the trunk, so the contract says which one a step line reads:flow_steps' doc now says it is the trunk and that a step line drawsflow_route, never it. Clause-② consequence: an added optional member of a published contract,yes (widening). Metadata-protocol consequence: none. The member is served by a read, and nothing a metadata author writes changes (the claim's provisionaladdedassumed an authorable key; re-judged from the diff, following #26361'syes (widening)/nonefor an added approvals read).flow_route(contract text inpackages/spec/src/contracts/approval-service.ts).done: earlier requests of the same run, on another node,approved, in the order the run opened them. A node approved in several rounds is listed once, at its latest round. These are the rulesvisitedApprovalStepshad, now on the server, read over every row of the run, not the viewer's list read.doneonce approved, elsecurrent, asflow_stepsmarks it.upcoming, for a pending request only: from the node'sapproveedges on, through nodes with exactly one non-fault out-edge and no condition. What I chose for steps after the current one, and why: the walk lists a step only when the graph alone puts it on this request's path once each step approves. It stops at adecision, a conditioned edge, several out-edges (a fan-out has no one order), a loop back, a missing node, or a node that runs other nodes (subflow,map,loop, or any node with regions). Evaluating a gateway's condition against the record now would be a guess: the record or the run's variables can change before the run gets there, and conditions can read outputs of decisions not yet made. Assuming each step approves is the reading a progress line has always had; a reject edge ends the request rather than routing it.ahead:end(the walk reached the flow's end),undecided(it stopped at a branch; nothing past it is listed, and the trunk is never listed in its place),decided(the request is decided; its route ends at itself).flow_steps:listRequestsserves neither. The run's rows are read as the system: the route carries step ids, labels and states, whichflow_stepsalready served to the same reader.Producer.
packages/plugins/plugin-approvals/src/approval-route.tsholds the rule as three pure functions (passedSteps,stepsAhead,flowRoute).ApprovalService.attachFlowRoutereads the run's rows and calls it fromattachFlowSteps, on the one flow lookup. Steps are named asflow_stepsnames them (#24863): the reader's label, else the node's authored label, else the label the step's request stored.Readers.
RecordApprovalsPaneldrawspendingRequest.flow_route.steps.visitedApprovalStepsis deleted.ApprovalDecisionPanel(the request page,record:approval_decision) drawsrequest.flow_route.stepsagain, below the snapshot summary and above the tally, at any status. The no-line rule is deleted.ApprovalStepLine's markup is unchanged; its step type is nowflow_route's, and its header says both callers hand it the served route.ApprovalRequestLiteand the console's row type declareflow_route; the app-shell README and the approvals guide say the same.Tests
flow-route.integration.test.ts(6). A realObjectQLoverSqlDriver(sqlite), the realAutomationEnginewith its logic nodes, the real approval node andApprovalService; every request opened by the engine, every decision resuming the run. Acontract_approval-shaped flow (manager, then a gateway to joint / legal only / finance only / none, then an executive above 500000, then a general manager above 1000000) and a linear three-step flow.flow_routeis managerdone, financecurrent,ahead: 'undecided'(the executive gateway is next), and none of the trunk's rungs.flow_stepsis served as before (allupcoming, own node absent).[manager current],undecided, never the trunk's joint rung.done), executive (current); the trunk still says the joint rung wasdone.done,ahead: 'decided'.flow_route.stepsdeep-equalsflow_steps,ahead: 'end'.approval-route.test.ts(16).passedSteps: order, outcomes that are not a pass (returned,rejected,pending,cancelled), rows opened later, the current node's earlier rounds, round dedup, rows with no node or time.stepsAhead: a linear tail (end), a non-approval node crossed, and each stop (decisioneven with one edge, conditioned edge, fan-out,subflow/map/loop/ regions, loop back, missing node), fault edges ignored.flowRoute: pending, decided at each outcome, no node.RecordApprovalsPanel.visitedRoute-26563.test.tsxis removed, with the function it pinned.RecordApprovalsPanel.servedRoute-26668.test.tsxreplaces it (5): the reported route drawn as served with no trunk rung, upcoming steps drawn muted and numbered, a viewer the list serves only the pending row still sees the whole route, a server that serves no route draws no line whatever the trunk reads, a one-step route draws none.RecordApprovalsPanel.stepProgressVertical.test.tsxre-pinned: its fixtures are served routes again (done, current, upcoming), and the connector-tint case pins the upcoming segment'sbg-borderagain.ApprovalDecisionPanel.stepLine-12491.test.tsxre-pinned from "no line" to the drawn route: the reported row draws manager → finance and none of the trunk's other rungs; a decided row draws its route; a row with no route draws nothing; the line sits above the tally and the tally above the decisions.approvalRequestsDataSource.servedPageStepLine-12491.test.tsx(console, the real served page) re-pinned: the off-trunk row draws its served three-step route, current marked, and none of the trunk's other rungs; a row with no route draws none; decided and person-submitted rows draw their routes; a one-step route draws none.Test readings, head 4efa377 (every run under scripts/pm/os-verify-lock.sh)
origin/main's producer:approval-service.tsrestored to7a2818c5's blob (92744064111b,attachFlowRoute0 hits) under an EXIT trap, thenflow-route.integration.test.ts: Tests 5 failed, 1 passed (6), each failureexpected undefined to deeply equal …onflow_route; the reported-route case'sflow_stepsassertion (allupcoming, own node absent) passed before it. The passing case is the list-read control. Restored withgit checkout HEAD --: blob1aa185105009equal to HEAD,git diff HEAD -- approval-service.tsempty. No test file kept for it.approval-route.test.ts+flow-route.integration.test.ts: Test Files 2 passed (2), Tests 22 passed (22), VERDICT command-exit 0.pnpm --filter @objectstack/plugin-approvals test: Test Files 104 passed (104), Tests 1321 passed (1321); thentypecheck(tsc --noEmit, the scripts project,check:test-typecheck: OK): VERDICT command-exit 0.pnpm --filter @objectstack/spec test: Test Files 738 passed (738), Tests 22101 passed, 1 todo (22102), VERDICT command-exit 0.pnpm --filter @objectstack/spec check:generated: all 14 generated artifacts up to date (flow_routeis an interface member; no api-surface, docs or authorable-surface artifact moves).packages/app-shellandapps/console(by file name, plus every test namingRecordApprovalsPanel,ApprovalDecisionPanel,ApprovalStepLine,useRecordApprovals,approval-decision,approval-progress,flow_stepsorflow_route): Test Files 61 passed (61), Tests 697 passed (697), VERDICT command-exit 0. The wholepackages/app-shellsuite was not run locally; CI's Test UI shards run it.pnpm --filter @object-ui/app-shell run type-checkandpnpm --filter @object-ui/console run type-check: VERDICT command-exit 0 (tsconfig.test.json --listFilesOnlylists the three touched app-shell tests; the console project lists its touched test).Acceptance notes
aheadisundecidedthe line ends at the last listed step, as the fix(app-shell): the approval step line draws the route the request's run took, not the flow graph's first branch #26666 line ended at the pending step. Drawing a "decided later" marker needs a newapprovalsInbox.*pack row in every locale; it is left out of this diff. The served row says it; whether the line shows it is a ui follow-up if wanted.flow_stepsalready served every step label of the flow, withdonestates along the trunk, to the same reader.done. Onlyapprovedrows count as passed, the rulevisitedApprovalStepshad. A route through a rejected rung shows only its approved rungs.created_aton its request rows. Two requests of one run opened in the same millisecond would order by the store's answer; a run opens its next request only after the previous decision, so this needs a sub-millisecond decide-and-resume.flow_stepskeeps its producer and its pins (approval-service.test.ts,reader-locale-display-labels.integration.test.ts) unchanged.Verification
Derived gate sweep, head 4efa377
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 110 commands from the change set. Each ran serially with its exit code captured before any pipe, and no build ran alongside: 107 exited 0, and three exited 3 (PREREQUISITE NOT MET:spec check:skill-examples,check:dual-build-cjs-loads,check:i18n, which read a full build). After onepnpm buildunder the lock (VERDICT command-exit 0), those three ran again and exited 0 (267 prose examples type-check; the CJS load floors held;check-i18n-bundles: OK).dispatch-gates --ranreconciliation:✓ 110 derived famil(ies) accounted for — 110 run, 0 NOT-MEASURED. Local-onlycheck-dev-prereqs: exit 0.origin/maingained commits after the branch point; none touches a file of this diff (git diff --name-only HEAD...origin/main, filtered for approval paths: empty). CI runs on the merge ref.!andClause-②carries no(narrowing)arm.skills/**edit: not a governed (Tier H) diff. No ADR-0087 marker: nothing stored changes meaning;check-adr-0087-registration --base origin/mainexited 0.🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzgrMKmuKm7tjxa5v95