Skip to content

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

Closed

Description

@objectstack-fleet

Filing gate: ① user-visible defect, reproduced in the browser on @objectstack 18.2.0 (found in the hotclm 18.2 upgrade pass, objectstack-ai/hotclm#155); also visible under 18.1 (hotclm regression screenshot r18-fin-approve-open.png).

Symptom. hotclm's contract_approval flow has six approval nodes behind decisions: manager_review → (legal_finance_joint | legal_only | finance_only | none) → executive_signoff? → gm_signoff?. A 250,000 order routed route_finance = true, route_legal_head = false, so it goes manager → finance_only. On the request page the rail reads:

1 直属上级 · 2 法务负责人 + 财务负责人(会签) · 3 分管领导 · 4 总经理

while the current step header says 合同审批 · 财务负责人 and the slate waits on the finance controller. The rail names a rung this request is not on, and shows two rungs (分管领导, 总经理) the route flags say it will skip.

Expected. The rail shows the steps this request takes or has taken: the decided branch, not the first branch of each decision; rungs a decision skips are left out or marked skipped.

Business reason. An approver reads the rail to know who else signs; a finance controller is told legal co-signs with them and that two executive rungs follow, none of which is true for this contract.

Reader. plugin-approvals / console approvals UI lane.

Duplicate search. objectstack issues, all states, "approval request step rail branch / path": 0 direct hits; nearest #25501 and #24281 (step id vs label on the same page, closed).

Activity

  1. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:ui · area:workflow · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-27T08:56Z

    ⛔ Not a claim, ⛔ not a dispatch.

    • p2: the approval request page's step rail (RecordApprovalsPanel.tsx / useRecordApprovals.ts) lists one fixed path, so a request routed past rungs still shows them. First measure whether the request read already carries the resolved route or the visited nodes. If it does, the rail reads that. If it does not, the read belongs to plugin-approvals (domain:server, a child card), and the rail shows only the visited and current steps until then. ⛔ Never a guessed path.

    Generated by Claude Code

  2. added
    area:workflowApprovals and automation — the work that runs without a person driving it
    bugSomething isn't working
    and removed on Oct 27, 2026
  3. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01QsR2jTfvjNTaR29ehLk26T
    Account: zhuangjianguo
    Branch: claude/project-thread-v6vydb (platform-assigned card-thread branch; a card thread can push only its assigned branch, and the name carries no issue number, so this claim is the identity record)
    Worktree: objectstack-issue-26563
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: the approval request step rail in ui/packages/app-shell/src/views/approval-progress/** and/or ui/packages/app-shell/src/views/RecordApprovalsPanel.tsx, ui/packages/app-shell/src/hooks/useRecordApprovals.ts, their tests, one root changeset; ⛔ packages/plugins/plugin-approvals/** (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate; reason: the maintainer's standing rule 「只认领不需要 fable 的卡」, 2026-10-27T10:39Z), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the rail (app-shell, this lane) draws one fixed path · the platform path that may cover it: the approval request read, unmeasured (a plugin-approvals child card if it carries no route) · reached by every approver and requester of a branched approval flow (hotclm contract_approval)
    Thread-read: 6437696106
    Premise: holds (action, card-reference and work-item surfaces re-read on origin/main 8c0306f5 at 2026-10-27T10:50Z)
    Serial constraints cleared: none: no open PR or in-flight claim on these files (scan 2026-10-27T10:50Z); #26402 touched useRecordApprovals today and has merged

    Dispatched at 2026-10-27T10:54Z to the card thread on that branch; the order lives in the thread's opening message.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim amendment · seat domain:ui#4 · session_01QsR2jTfvjNTaR29ehLk26T · 2026-10-27T10:57Z

    File surface (replaces the claim's line): ui/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.tsx (the request page stops drawing the flow_steps rail), its ApprovalDecisionPanel.stepLine-12491.test.tsx, the console approvalRequestsDataSource.servedPageStepLine-12491.test.tsx (both pin the old drawing), the record panel rail in ui/packages/app-shell/src/views/approval-progress/** / RecordApprovalsPanel.tsx / hooks/useRecordApprovals.ts, their tests, one root changeset; ⛔ packages/plugins/plugin-approvals/** (stop on breach; explain in the report)

    Why: the thread measured that GET /approvals/requests/:id carries neither a resolved route nor visited nodes. Its flow_steps comes from plugin-approvals attachFlowSteps, which walks one trunk of the flow graph, so both its done and upcoming entries are guessed. The rail is built from facts only: the run's own decided requests plus the current one. A plugin-approvals child card for a route-carrying read follows from the report.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 26563,
      "status": "done",
      "branch": "claude/project-thread-v6vydb",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/26666",
      "session": "session_01QsR2jThx2NXgeJ4uCUi1qo",
      "head_sha": "4743eb8d",
      "premise_still_valid": true,
      "summary": "Measured per triage 6437696106: GET /api/v1/approvals/requests/:id carries neither a resolved route nor visited nodes. Its flow_steps is plugin-approvals ApprovalService.attachFlowSteps walking the flow graph from start (approve, else true, else first edge) and marking done/current/upcoming against that trunk; the trunk's done entries are guesses too wherever a branch rejoins. Stop condition applied: the record approvals panel now builds the step line from facts only (visitedApprovalSteps: requests of the same flow_run_id on another node, approved, opened before the pending one, latest round per node, then the pending step, nothing ahead); the approval request page's decision panel draws no step line (its row proves only its own step, which the header names). The plugin-approvals route read is reported below for the seat to file.",
      "tests": "head 4743eb8d, all under os-verify-lock from ui/: four touched suites Test Files 4/4, Tests 24/24; every approval test in app-shell + console (68 files) Tests 765/765, 7m00s; full app-shell suite (1757 files) NOT MEASURED locally, left to CI Test UI shards. Ablation 1 (ablation-replace.mjs, panel steps -> pendingRequest?.flow_steps, anchor 1->0, blob ddd69d0e14eb->347c203eb6ff): visitedRoute-26563 Tests 4 failed / 7 passed (the 7 are direct unit cases), restored blob == HEAD, git diff HEAD empty. Ablation 2 (ApprovalDecisionPanel.tsx restored to merge-base blob f61253e4b319 under EXIT trap): decision panel + console served-page suites 6 failed / 3 passed, restored to HEAD blob 4bf7bdb3f704, status clean. type-check: @object-ui/app-shell (tsc + tsconfig.test.json, --listFilesOnly lists the 3 touched tests) exit 0; @object-ui/console (--listFilesOnly lists the touched test) exit 0.",
      "gates": "local: dispatch-gates --commands --repo objectstack-ai/objectstack derived 34 at 4743eb8d (merge base fb91e9793); all 34 run, exit codes captured before any pipe, all 0; check:type-check-debt under the verify lock (VERDICT command-exit 0); PR-body gates read the body via GITHUB_EVENT_PATH; --ran: '34 derived famil(ies) accounted for — 34 run, 0 NOT-MEASURED', exit 0. CI on 4743eb8d at report time: 19 check-runs, 16 in_progress, 1 success, 2 skipped.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "line_budget": "+487 / -267 = 754 changed lines across 10 files (under the 3000 human-merge threshold); no skills/** path",
      "files_changed": [
        ".changeset/26563-approval-step-line-visited-route.md",
        "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.stepProgressVertical.test.tsx",
        "ui/packages/app-shell/src/views/RecordApprovalsPanel.tsx",
        "ui/packages/app-shell/src/views/RecordApprovalsPanel.visitedRoute-26563.test.tsx",
        "ui/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.stepLine-12491.test.tsx",
        "ui/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.tsx",
        "ui/packages/app-shell/src/views/approval-progress/ApprovalStepLine.tsx"
      ],
      "deviations": [
        "file surface: ApprovalDecisionPanel.tsx and both 12491 step-line tests are outside the original claim; the seat amended the claim (comment 6440197798) before the push",
        "file surface: ui/packages/app-shell/README.md (two approval paragraphs) edited under ui/AGENTS.md #2 docs-driven; not in the claim's surface",
        "ApprovalStepLine.tsx header: a stale paragraph about the approvals inbox sheet's copy of the stepper removed (no such copy exists in ui/apps/console); repaired opportunistically in a touched file",
        "changeset bumps @object-ui/console as well as @object-ui/app-shell (app-shell is private; the change ships in the console bundle), following the 26473 changeset precedent",
        "narrowed local tests: the full app-shell suite (1757 files) was started and stopped by its recorded PID after it held the lock for more than 10 min; replaced by the 68 approval test files; CI Test UI covers the whole package",
        "origin/main moved 12 commits after the branch point, none touching this diff; not merged before the PR (CI runs the merge ref)",
        "harness attribution reminder asks for a model-named Co-Authored-By trailer; the commit carries the repo's model-free pair instead (os-dev rule), pre-push check:commit-card-trailers passed"
      ],
      "mcp_calls": "0 — no MCP GitHub tool called; card, claim and PR reads went through plain REST GETs",
      "api_writes": "2 relay strokes so far, all as objectstack-fleet[bot]: fleet-write pr_create (POST /repos/objectstack-ai/objectstack/pulls draft -> #26666, plus its assignees leg POST /issues/26666/assignees), read-back 10783 bytes identical; this os-dev-report comment (POST /repos/objectstack-ai/objectstack/issues/26563/comments). git push is not a REST write.",
      "open_questions": [
        {
          "question": "Should the approval request page draw a step line before the plugin-approvals route read lands?",
          "options": [
            "A: no line (this PR); the row proves only its own step, which the header names",
            "B: the decision panel also reads the record's request list (useRecordApprovals) and draws visitedApprovalSteps; a second read on the page, and the list serves only the viewer's own rungs, so the reported finance controller still sees one step (no line)",
            "C: a one-step current-step line (the seat's suggestion); ApprovalStepLine's own gate draws nothing under two steps, and it repeats the header"
          ],
          "recommendation": "A. Four axes: business need, the reported viewer gets no line under B or C anyway; long term, the route belongs in the served read, so B adds a client read the child will delete; AI-error resistance, A keeps one source (served route) to come back to; startup scope, A adds nothing."
        }
      ],
      "out_of_scope_findings": [
        "class: a · reach: the card's own browser repro on @objectstack 18.2.0 (hotclm contract_approval routed manager -> finance_only: the request page's rail read 直属上级 · 法务负责人 + 财务负责人(会签) · 分管领导 · 总经理), produced by the named server producer ApprovalService.attachFlowSteps (plugin-approvals), which every GET /api/v1/approvals/requests/:id serves · evidence: attachFlowSteps walks from the start node taking the approve, else true, else first edge, and states every step against that trunk; for a request whose node is off the trunk every entry is upcoming and its own node is absent; the spec contract text (packages/spec/src/contracts/approval-service.ts, ApprovalRequestRow.flow_steps) reads 'The owning flow's approval steps in graph order', so serving the route is a contract change (redefine or add a field) · Seam: spec:ApprovalRequestRow.flow_steps → runtime:ApprovalService.attachFlowSteps | renderer:ApprovalStepLine · fix lane domain:server; once landed, the request page draws the served route again and visitedApprovalSteps is deleted · dedupe: approval flow_steps route; attachFlowSteps visited nodes; approval step rail branch; flow_run_id visited approval steps",
        "carrier: none · noted, not filed: ApprovalStepLine's 'upcoming' styling has no caller until the served route returns"
      ]
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:workflowApprovals and automation — the work that runs without a person driving itbugSomething isn't workingdomain:uipm:dispatchedpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions