Skip to content

feat(spec,plugin-approvals,app-shell): the approvals get serves a request's own route, and both approval step lines draw it - #26976

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-04q6o5
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-04q6o5

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread
Fixes #26668

Clause-②: yes (widening)
Metadata-protocol: none
Skills-docs: none — flow_route is a served read member of the approvals API, not anything a metadata author writes; no skill teaches flow_steps (git grep over skills/ and content/docs/: 0 hits), and the approvals guide gains a paragraph naming flow_route here (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 和审批单页「不画步骤线」的规则一并删除。
  • 为什么改:合同审批走「直属上级 → 仅财务」时,服务端只给主干「直属上级 · 法务 + 财务会签 · 分管领导 · 总经理」,全部是「待办」,本单所在的「财务」根本不在其中。fix(app-shell): the approval step line draws the route the request's run took, not the flow graph's first branch #26666 只能让页面不画;现在由服务端给出本单真实路线,两处步骤线恢复。
  • 风险与回滚:契约是新增字段(加宽),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/main 7a2818c5.

  • The served row. attachFlowSteps walks the graph from start, taking approve, else true, else the first edge, and states each step against that one trunk. On the real engine, a contract_approval-shaped flow routed manager → finance only serves flow_steps with every trunk entry upcoming and finance_only absent (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 as done, a rung the run never visited.
  • The history the row can draw from. The request row names its run (flow_run_id), its node (flow_node_id), status and created_at; every step the run opened is its own sys_approval_request row 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.
  • The readers. After fix(app-shell): the approval step line draws the route the request's run took, not the flow graph's first branch #26666 no surface draws 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-shell useRecordApprovals.ts), ApprovalStepLine's step type, and the console's approvalsApi.ts row type. No API doc or skill names it.

The shape, option (a): flow_steps stays the trunk, and ApprovalRequestRow.flow_route is added. Why not (b), redefining flow_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 draws flow_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 provisional added assumed an authorable key; re-judged from the diff, following #26361's yes (widening) / none for an added approvals read).

flow_route (contract text in packages/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 rules visitedApprovalSteps had, now on the server, read over every row of the run, not the viewer's list read.
  • The request's own node: done once approved, else current, as flow_steps marks it.
  • upcoming, for a pending request only: from the node's approve edges 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 a decision, 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).
  • Single reads only, like flow_steps: listRequests serves neither. The run's rows are read as the system: the route carries step ids, labels and states, which flow_steps already served to the same reader.

Producer. packages/plugins/plugin-approvals/src/approval-route.ts holds the rule as three pure functions (passedSteps, stepsAhead, flowRoute). ApprovalService.attachFlowRoute reads the run's rows and calls it from attachFlowSteps, on the one flow lookup. Steps are named as flow_steps names them (#24863): the reader's label, else the node's authored label, else the label the step's request stored.

Readers.

  • RecordApprovalsPanel draws pendingRequest.flow_route.steps. visitedApprovalSteps is deleted.
  • ApprovalDecisionPanel (the request page, record:approval_decision) draws request.flow_route.steps again, 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 now flow_route's, and its header says both callers hand it the served route.
  • ApprovalRequestLite and the console's row type declare flow_route; the app-shell README and the approvals guide say the same.

Tests

  • Real service path, new flow-route.integration.test.ts (6). A real ObjectQL over SqlDriver (sqlite), the real AutomationEngine with its logic nodes, the real approval node and ApprovalService; every request opened by the engine, every decision resuming the run. A contract_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.
    • Off the trunk, manager → finance only: flow_route is manager done, finance current, ahead: 'undecided' (the executive gateway is next), and none of the trunk's rungs. flow_steps is served as before (all upcoming, own node absent).
    • At the first step, a gateway right after it: [manager current], undecided, never the trunk's joint rung.
    • A branch that rejoins the trunk: manager, legal only (done), executive (current); the trunk still says the joint rung was done.
    • A decided request: its route ends at itself, every step done, ahead: 'decided'.
    • On the trunk (the linear flow): at every step flow_route.steps deep-equals flow_steps, ahead: 'end'.
    • The list read serves neither member.
  • The rule, new 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 (decision even 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.
  • ui (re-pinned or removed).
    • RecordApprovalsPanel.visitedRoute-26563.test.tsx is removed, with the function it pinned. RecordApprovalsPanel.servedRoute-26668.test.tsx replaces 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.tsx re-pinned: its fixtures are served routes again (done, current, upcoming), and the connector-tint case pins the upcoming segment's bg-border again.
    • ApprovalDecisionPanel.stepLine-12491.test.tsx re-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)
  • Reproduction on origin/main's producer: approval-service.ts restored to 7a2818c5's blob (92744064111b, attachFlowRoute 0 hits) under an EXIT trap, then flow-route.integration.test.ts: Tests 5 failed, 1 passed (6), each failure expected undefined to deeply equal … on flow_route; the reported-route case's flow_steps assertion (all upcoming, own node absent) passed before it. The passing case is the list-read control. Restored with git checkout HEAD --: blob 1aa185105009 equal to HEAD, git diff HEAD -- approval-service.ts empty. 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); then typecheck (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_route is an interface member; no api-surface, docs or authorable-surface artifact moves).
  • ui, the four touched suites: Test Files 4 passed (4), Tests 20 passed (20). Every approval test file in packages/app-shell and apps/console (by file name, plus every test naming RecordApprovalsPanel, ApprovalDecisionPanel, ApprovalStepLine, useRecordApprovals, approval-decision, approval-progress, flow_steps or flow_route): Test Files 61 passed (61), Tests 697 passed (697), VERDICT command-exit 0. The whole packages/app-shell suite was not run locally; CI's Test UI shards run it.
  • pnpm --filter @object-ui/app-shell run type-check and pnpm --filter @object-ui/console run type-check: VERDICT command-exit 0 (tsconfig.test.json --listFilesOnly lists the three touched app-shell tests; the console project lists its touched test).

Acceptance notes

  • The line does not mark an undecided tail. When ahead is undecided the 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 new approvalsInbox.* 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.
  • Who sees which steps changed on the record panel. The client-side rebuild could only draw rows the list read served the viewer (participants only), so a finance controller saw a one-step line, which draws nothing. The served route lists the run's passed steps to anyone who can read the request. These are step ids, labels and states; flow_steps already served every step label of the flow, with done states along the trunk, to the same reader.
  • A step a reject edge routes to, or a rung sent back for revision, is not done. Only approved rows count as passed, the rule visitedApprovalSteps had. A route through a rejected rung shows only its approved rungs.
  • Ordering is by the run's created_at on 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_steps keeps 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/objectstack derived 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 one pnpm build under 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 --ran reconciliation: ✓ 110 derived famil(ies) accounted for — 110 run, 0 NOT-MEASURED. Local-only check-dev-prereqs: exit 0.

  • origin/main gained 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.
  • No dogfood battery: the title carries no ! and Clause-② carries no (narrowing) arm.
  • No skills/** edit: not a governed (Tier H) diff. No ADR-0087 marker: nothing stored changes meaning; check-adr-0087-registration --base origin/main exited 0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzgrMKmuKm7tjxa5v95

… 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
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 27, 2026
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-approvals, @objectstack/spec, touching 19 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/approvals.mdx (via flow_route (symbol, a field of interface ApprovalRequestRow), flow_route (literal, a string literal in FlowRoute), sys_approval_request (literal, a string literal in attachFlowRoute))
  • content/docs/automation/flows.mdx (via ApprovalService (symbol, a top-level class), sys_approval_request (literal, a string literal in attachFlowRoute))
  • content/docs/automation/workflows.mdx (via sys_approval_request (literal, a string literal in attachFlowRoute))
  • content/docs/plugins/packages.mdx (via sys_approval_request (literal, a string literal in attachFlowRoute))
  • content/docs/ui/translations.mdx (via sys_approval_request (literal, a string literal in attachFlowRoute))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via sys_approval_request (literal, a string literal in attachFlowRoute))
  • content/docs/releases/v17/17-6.mdx (via ApprovalService (symbol, a top-level class), sys_approval_request (literal, a string literal in attachFlowRoute))
  • content/docs/releases/v18/18-1.mdx (via ApprovalService (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 anchor(s) matched too much of the corpus to be a work list: created_at (symbol, 39 pages), created_at (literal, 39 pages)
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 55 of 207 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 147 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 49b505915a37270ddf12d0ed4a6fd67f9907fb74 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c5a88d1e4d0ed32fb211671f1fbbd3b3199c7e38 — the merge of head b97005bf14417759f5fdf57e4cb90c41622774bd into base 49b505915a37270ddf12d0ed4a6fd67f9907fb74, 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 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

⚠️ 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 49b505915a37270ddf12d0ed4a6fd67f9907fb74 → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4efa377e203fb1b515195033c56e57c104b90739
Local-runs: none
Scope: packages/spec/src/contracts/approval-service.ts + packages/plugins/plugin-approvals/src/approval-route.ts, packages/plugins/plugin-approvals/src/approval-route.test.ts, packages/plugins/plugin-approvals/src/approval-service.ts, packages/plugins/plugin-approvals/src/flow-route.integration.test.ts, ui/apps/console/src/services/approvalsApi.ts, ui/apps/console/src/services/approvalRequestsDataSource.servedPageStepLine-12491.test.tsx, ui/packages/app-shell/README.md, ui/packages/app-shell/src/hooks/useRecordApprovals.ts, ui/packages/app-shell/src/views/RecordApprovalsPanel.tsx, ui/packages/app-shell/src/views/RecordApprovalsPanel.servedRoute-26668.test.tsx, ui/packages/app-shell/src/views/RecordApprovalsPanel.stepProgressVertical.test.tsx, ui/packages/app-shell/src/views/RecordApprovalsPanel.visitedRoute-26563.test.tsx (deleted), ui/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.tsx, ui/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.stepLine-12491.test.tsx, ui/packages/app-shell/src/views/approval-progress/ApprovalStepLine.tsx; cited code read on origin/main 7bb80845: packages/services/service-automation/src/dag-traversal.ts (traverseNext), packages/spec/src/automation/flow.zod.ts (FlowNodeAction, the edge schema), packages/plugins/plugin-approvals/src/approval-node.ts (resume labels, auto-approve), packages/plugins/plugin-approvals/src/sys-approval-request.object.ts, packages/drivers/driver-sql/src/sql-driver.ts (formatOutput datetime canon), the pre-change attachFlowSteps. Prose faces (.changeset/26668-approval-served-route.md, content/docs/automation/approvals.mdx) are the dispatching seat's ACCEPT, not this record.

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

  1. Accept set / public face. ApprovalRequestRow (@objectstack/spec/contracts) gains one optional served member flow_route?: { steps, ahead: 'end' | 'undecided' | 'decided' }; flow_steps is byte-identical in shape and producer, its doc rewritten to say it is the trunk and that a step line draws flow_route, never it. Widening of a published read contract; nothing an author writes moves. Correct as declared.

  2. ⛔ No guessed path — judged on the producer (approval-route.ts, attachFlowRoute).

    • Passed steps come ONLY from the run's own sys_approval_request rows: the service reads where: { flow_run_id: row.flow_run_id } as the system, and passedSteps keeps a row only if it is another node, approved, opened before the request (created_at compare) and, per node, its latest round. The current step is row.flow_node_id. The graph is never consulted for either. Correct.
    • Steps ahead (stepsAhead): from an approval node the approve-labelled out-edges, else every non-fault out-edge — exactly the engine's rule in traverseNext (claim by branchLabel, else every out-edge, fault excluded). It follows ONE edge with no condition and returns undecided at a decision node (even with one edge), a conditioned edge, more than one out-edge, a node already seen, a node not in the graph, or a subflow / map / loop / any node with regions. A single isDefault edge is followed, which is what the engine does when no conditional sibling exists. The trunk is never substituted: nothing in the module reads the true edge or out[0]. Correct.
    • Shapes probed, with what ahead says: a re-entered step (loop back) stops at the seen node, undecided, after listing the steps up to it; a rejected-then-resubmitted run (ADR-0044) lists no returned / rejected row, lists a re-approved node once at its latest round, and never walks the revise / reject edges because the node's approve edges exist; a parallel or fan-out (parallel_gateway, or any node with two unconditioned out-edges) stops undecided — two pending requests of one run each see the other's approved rows as done, which are facts of the run; a run with no history rows yields passed [] and the walk alone; a request with no flow_run_id or no flow_node_id gets no flow_route (documented); a flow edited after the request started is read at its current version — the request's own node missing from the graph gives { steps: [passed, own], ahead: 'undecided' }, a passed node missing is labelled from the stored current_step_label, a deleted flow gives no flow_route at all (no guess, a gap); a non-pending request (approved, rejected, returned, cancelled, recalled) is passed + own (done once approved, else current) with ahead: 'decided' and no walk.
    • decided is served only when row.status !== 'pending', and then never with a walked tail. Never served without a decided path.
    • Not a guess but an omission, noted for the record: an auto-approved rung (审批流无法在「运行中」动态确定审批人——审批人在提交时即被 $trigger 快照锁定(动态路由 / 动态会签受阻) #3447, empty slate, no request row) is absent from the passed steps.
  3. Two fields. Readers of the rail now read ONLY flow_route.steps: RecordApprovalsPanel (pendingRequest?.flow_route?.steps, the pending request being the one row the hook enriches by getRequest, so the single-read member reaches it), ApprovalDecisionPanel (request.flow_route?.steps, at any status), and ApprovalStepLine's step type is now flow_route's. The two type declarations (ApprovalRequestLite in app-shell, the console's approvalsApi.ts row) carry both members, each doc saying the line draws flow_route and nothing draws flow_steps; the app-shell README and the approvals guide say the same; the spec doc on flow_steps says it. git grep on origin/main finds no other reader: no packages/client type, no OpenAPI or content/docs/references output names flow_steps (the only other hit is a liveness-evidence string naming attachFlowSteps, whose producer is unchanged). Nowhere do the two fields disagree without the doc saying which the UI reads. The spec doc's stop list omits the two extra conservative stops the code has (missing node, regions); the guide's matches the spec. Not a disagreement.

  4. Declarations. Clause-②: yes (widening) — right (one added optional member of a published contract). Metadata-protocol: none — right: the rule (AGENTS.md Post-Task Checklist step 3) grades the change to what a metadata author can write; flow_route is a served read member, no key, value or type an author writes moves. The claim's added was marked provisional and assumed an authorable key; the dev's re-judgment from the diff stands. Precedent feat(plugin-approvals,rest,client): a record-anchored approval lock read, { locked } for every caller who can read the record #26361 holds: it added the optional getRecordLock? member to IApprovalService in the same contract file under yes (widening) / none. Skills-docs: none — the third line is not owed at all (kind none, arm widening); git grep over skills/ for flow_steps, flow_route, the step line or progress strip: 0 hits, so the guide's new paragraph makes no skill text false. ADR-0087: no marker owed — no breaking changeset (no !, no (narrowing), patch everywhere), and the gate reads the breaking declaration alone. Changesets: one entry per touched published package (@objectstack/spec, @objectstack/plugin-approvals, @object-ui/app-shell, @object-ui/console), all patch, each body carrying both declaration lines.

  5. Stopgap deletion (fix(app-shell): the approval step line draws the route the request's run took, not the flow graph's first branch #26666). visitedApprovalSteps and the request page's no-line rule are deleted; ApprovalStepLine's markup is untouched. Tests, one by one: RecordApprovalsPanel.visitedRoute-26563.test.tsx removed with its subject — its four panel pins are re-pinned in RecordApprovalsPanel.servedRoute-26668.test.tsx (the reported route with no trunk rung; a one-step route draws none; the viewer served only the pending row now sees the whole route, inverted on purpose because the route no longer comes from the list read; the rejoin-trunk case moved to the server where the rule now lives, integration case 3), its seven unit pins on the deleted function are re-pinned as passedSteps cases in approval-route.test.ts (order, outcomes that are not a pass, later rows, the current node's earlier rounds, round dedup, missing node or time); RecordApprovalsPanel.stepProgressVertical.test.tsx re-pinned to served-route fixtures, its connector-tint case now pinning both tints (green into done/current, bg-border into upcoming) where it pinned all-green under the no-ahead regime; ApprovalDecisionPanel.stepLine-12491.test.tsx re-pinned from "no line" to the drawn route, adding no-route-draws-nothing and line-above-tally-above-decisions, the old tally control kept inside the ordering case; the console servedPageStepLine-12491 (the real served page) re-pinned likewise, its queryByRole('list') null-check dropped because a list now legitimately exists. No retained assertion is weakened. A trunk request renders the same for every reader: integration case 5 pins flow_route.steps deep-equal to flow_steps at every step of a linear flow with ahead: 'end', and both panels draw one ApprovalStepLine from that one member.

  6. Tests pin the real service path. flow-route.integration.test.ts boots a real ObjectQL over SqlDriver, the real AutomationEngine with its logic nodes (the gateway is a real decision), the real approval node and ApprovalService; every request is opened by the engine and every decision resumes the run; the route is read through svc.getRequest. The dev's repro on origin/main's producer reports 5 of 6 red: the green one is case 6, "the list read serves no route, as it serves no flow_steps" — green on both sides by construction (the list read never served either member), which is what the dev reports; case 1's flow_steps assertion is likewise green on both sides on purpose. The unit file covers the branch kinds a live run reaches slowly.

  7. Gates. The dev's 110 locally derived gates are the dev's reading and do not carry to the head; the head's check-runs are the verdict. Readings of /commits/4efa377e…/check-runs: at 2026-10-27T23:57Z 40 runs, 29 in progress, 0 failures; at 2026-10-28T00:05Z 60 runs, 28 in progress (the 16 Test UI shards, 8 Test Core shards, Lint UI, Type Check UI, two Type Check lanes), 26 success, 6 skipped, 0 failures. Final reading before this record was written, at 2026-10-28T00:09Z: 61 runs, 41 success, 6 skipped, 0 failure, 14 still in progress (Test UI shards 2, 7, 8, 9, 12 and 14 of 16; Test Core shards 2, 3, 4, 5, 6 and 7 of 8; Lint UI; Type Check UI). An in-progress gate is an honest reading, not a pass: no check on the head has failed, and the landing pre-check (every check green) is the owning seat's to re-read before the queue; this record does not stand in for it.

② Semver level

patch on all four touched published packages. Right by the rule: Metadata-protocol: none takes patch, and a non-protocol widening ships patch whatever its Clause-② arm. No minor is owed because no authorable surface moved; no BREAKING banner, no ADR-0087 disposition, no tombstone. Changeset bodies and the PR body carry the same two declaration lines.

③ Boundary flags

  • Dev flag, "the line does not mark an undecided tail": accepted. The served row says ahead: 'undecided'; the line ending at the last listed step draws no guess. A marker is a ui follow-up if wanted, not a defect.
  • Dev flag, "who sees which steps changed on the record panel" (the run's rows are read as the system): accepted. What crosses is step ids, labels and states, which flow_steps already served to the same reader; no approver, payload or comment data rides it.
  • Dev flag, ordering by created_at with a same-millisecond tie: accepted; a run opens its next request only after the previous decision. created_at is written as an ISO string and formatOutput canonicalises Field.datetime to ISO on read on every SQL dialect, so the rule's typeof === 'string' guard holds off sqlite too.
  • Dev flag, the full packages/app-shell suite not run locally: the 16 Test UI shards on the head are the reading (see ① 7).
  • Deviation, Metadata-protocol re-judged from the claim's provisional added to none: the re-judgment is right (① 4).
  • Deviation, branch claude/project-thread-04q6o5 rather than claude/issue-26668-*: platform-assigned, written as measured in the claim; Implemented-by names it.
  • Observation, non-blocking: stepsAhead seeds seen with the current node only, so a walk that re-enters a step already in passedSteps would list that id twice and ApprovalStepLine keys rows by s.id. Reachable only on an unconditioned approve-edge cycle, which the engine would itself loop on; no guess is drawn. A follow-up may seed seen with the passed ids.
  • Observation, non-blocking: the passedSteps docblock says rows of other runs are ignored; the function does not filter by run — the caller's where: { flow_run_id } does. Doc nit.
  • Observation: attachFlowRoute adds one system read per single get (limit: 1000), display-only and caught, as attachFlowSteps is.
  • Author: PR user.login is objectstack-fleet[bot]; no relay login to name.
  • Governed surfaces: none of the 18 paths; 1,544 changed lines, under the 3,000 threshold; not Tier H. The schema face alone makes this review owed. Landing stays the owning seat's, through the queue, once every check on the head is green.

Implemented-by: claude/project-thread-04q6o5
Reviewed-by: session_01WGufzgbJhFoXYsLSe5WSPP

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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 (席位终稿)

  • 改了什么:审批单读取新增 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 的客户端临时方案和「不画步骤线」规则一并删除。
  • 为什么改:审批走「直属上级 → 仅财务」这类分支时,服务端只给主干,而且全部标「待办」,本单所在的步骤根本不在其中,步骤线只能不画或画错。
  • 风险与回滚:只新增字段,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 文档。
  • 两分钟验证:pnpm --filter @objectstack/plugin-approvals exec vitest run src/flow-route.integration.test.ts,预期 6 项全绿;报告中那条路线只有「Manager(done)· Finance(current)」两步,ahead 为 undecided。
  • 需要你做的:无,席位落地。

席位意见:接受。契约复核 PASS(6456957540),确认「不猜路线」成立:已走步骤只来自本次运行的审批单行,后续步骤遇到判断、条件、分叉、回环或子流程即停。复核指出的一句不实注释已在 b97005bf 改正,这一提交只改注释。

席位核对 · ACCEPT · domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP · 2026-10-28T00:44Z

Head: b97005bf14417759f5fdf57e4cb90c41622774bd · card #26668 · claim 6455118705

  1. 形态: a draft PR to main whose only closing keyword is Fixes #26668. It has 18 paths, +1127 / −418. Everything sits inside the claim surface: the spec contract, plugin-approvals approval-route.ts and its service wiring, the two app-shell rail readers and the step-line component, their tests, approvals.mdx, the plugin README and changesets. check-governed-merges --pr 26976 reports NOT governed, 1545 lines, under 3000, so the ordinary queue landing applies. The assignee is os-support-ai.
  2. 前提实测: the premise holds, measured by the dev on origin/main 7a2818c5. A contract_approval-shaped request routed manager → finance only serves every trunk entry upcoming and leaves finance_only absent. A route that rejoins the trunk is served the joint rung as done, a rung the run never visited. Five of the six route pins are red on main. The sixth, the list-read control, is green on purpose, and the review confirmed it.
  3. 门禁 check-runs (on this head): 63 runs: 57 success, 6 skipped (opt-in or path selector), none red or pending, read at 2026-10-28T00:43Z. Mergeable state is clean. The contract-tier review PASSED on 4efa377e (6456957540). This head adds one comment-only commit on top of it: the seat read the compare diff, which changes one docblock line in approval-route.ts and no code. The review therefore carries to this head. The dev's local runs: spec, plugin-approvals and app-shell suites green, and dispatch gates 110 derived, 110 run, 0 NOT-MEASURED.
  4. 接受的偏差: (a) The claim provisionally gave Metadata-protocol: added; the dev re-judged it to none. The review agrees: flow_route is a served read member and nothing authorable, the same pair as feat(plugin-approvals,rest,client): a record-anchored approval lock read, { locked } for every caller who can read the record #26361 in the same file. Clause-②: yes (widening), and all four published packages take a patch. (b) The branch is the platform-assigned claude/project-thread-04q6o5, as written in the claim. (c) Observations accepted without change. A step re-entered on an unconditioned approve cycle could be listed twice; that path is reachable only on a cycle the engine itself would loop on, and no guess is drawn. Auto-approved rungs that have no request row are absent from the route; no row exists, so this is no guess either. The record panel now shows passed step labels to viewers who did not take part, but those labels are the same data flow_steps already served to the same reader.
  5. 范围外去向: nothing is filed. ahead: 'undecided' has no visual marker on the line, which the review judged a possible ui follow-up rather than a defect. The seat does not file it unless the ui lane asks. The PR body says nothing of plugin-approvals: the approval request page's step rail lists one fixed path, so a request routed to the finance-only rung shows step 2 as the joint legal + finance rung #26563 is left, because fix(app-shell): the approval step line draws the route the request's run took, not the flow graph's first branch #26666 completed it and this PR draws the line again.
  6. 落地条款: the seat checked the prose faces against the diff on this head: the PR body, the changesets, the flow_route contract doc, which says the line never draws flow_steps, approvals.mdx and the README. The passedSteps docblock is now true. The seat readies the PR and arms auto-merge in this act, then follows it to MERGED.

Generated by Claude Code

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/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants