Skip to content

app-showcase: the expense sign-off approval (会签) ends without writing its outcome back — EXP-2001 still reads "Submitted" after both groups approved #25469

Closed

Description

@marchtian

Requested by 田騏 · project thread

Found during an exploratory walkthrough of the showcase.

Environment

  • objectstack main @ b9c7a8f, PORT=3000 pnpm dev (showcase, fresh DB), objectui main @ 6fb31dc

Steps

  1. As the dev admin, open the pending approval 费用报告签批 · EXP-2001 from the home page's 待审批 card, choose 通过, add a comment, confirm. The request shows 会签进度 1 / 2 (manager 1/1, finance 0/1).
  2. Sign in as auditor.demo@example.com, open the same request from the notification, approve.

Actual

  • The approval request moves to 已批准 and its timeline lists both approvals, which is right.
  • The expense report itself is unchanged: the request page still shows the record's Status: Submitted, and
    GET /api/v1/data/showcase_expense_report returns EXP-2001 → status: "submitted".

Why

showcase_expense_signoff (src/automation/flows/index.ts) is start → dual_signoff (approval) → approved | rejected, and both outcomes are bare end nodes. Nothing on either branch updates the record, so the business object never learns the decision. The node also sets lockRecord: true; I did not check whether the lock is released after the decision.

Expected

A reader of the showcase sees the approval's result on the record it governed, for example an update_record on each branch setting status to approved / rejected. As it stands the marquee 会签 demo ends with a "Submitted" expense report that has, in fact, been approved, which reads as a platform defect to anyone trying the demo. The other approval demos that end on bare end nodes (the committee quorum flow sits right below this one) may want the same treatment.

Activity

  1. added
    area:workflowApprovals and automation — the work that runs without a person driving it
    bugSomething isn't working
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Oct 23, 2026
  2. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    Contributor

    Triage: lands in examples/app-showcase/src/automation/flows/index.ts (showcase_expense_signoff, the dual_signoff node) — showcase metadata, routed to domain:server as the subsystem it exercises (approvals), per the precedent on earlier app-showcase cards; rationale: the platform already has the mechanism: an approval node's approvalStatusField mirrors each request transition onto the business record (checklist approvals.status-mirror-field). The flagship 会签 demo declares neither that nor an update_record branch, so an approved expense report keeps reading Submitted. Fix = declare the mirror (or an update_record per branch) on this flow, and check the committee quorum flow beside it for the same gap.
    Path: workflow · 审批走得完 | approvals.per-group-signoff (P1, passes) / approvals.status-mirror-field (not declared by this demo) | step ②
    Grade: priority:p2 — runs, but the demo people copy ends in a wrong record state; no data loss.
    Type: Bug

    Session: session_01AkmasAfXt3cu83xGzkuKhe (objectstack triage, run on the maintainer's request in the project thread)


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    Contributor

    Triage: lands in examples/app-showcase/src/automation/flows/index.ts (showcase_expense_signoff, the dual_signoff node) — showcase metadata, routed to domain:server as the subsystem it exercises (approvals), per the precedent on earlier app-showcase cards; rationale: the platform already has the mechanism: an approval node's approvalStatusField mirrors each request transition onto the business record (checklist approvals.status-mirror-field). The flagship 会签 demo declares neither that nor an update_record branch, so an approved expense report keeps reading Submitted. Fix = declare the mirror (or an update_record per branch) on this flow, and check the committee quorum flow beside it for the same gap.
    Path: workflow · 审批走得完 | approvals.per-group-signoff (P1, passes) / approvals.status-mirror-field (not declared by this demo) | step ②
    Grade: priority:p2 — runs, but the demo people copy ends in a wrong record state; no data loss.
    Type: Bug

    Session: session_01AkmasAfXt3cu83xGzkuKhe (objectstack triage, run on the maintainer's request in the project thread)


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 1 · domain:services seat 1 (seat post #6021, project run by the Sales account) · S mechanical card, in-seat subagent · 2026-10-23T18:24Z
    Session: session_016g7nYP9wdNx1GDa5XDr7Uy
    Account: os-sales (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-25469-showcase-signoff-mirror
    Worktree: objectstack-issue-25469
    Domain: domain:server
    Seat: domain:services#1
    File surface: examples/app-showcase/src/automation/flows/index.ts (showcase_expense_signoff, the dual_signoff node: declare the approval node's approvalStatusField mirror, or an update_record per branch), and the committee quorum flow beside it if it has the same gap. Plus its showcase pin. ⛔ packages/spec is not edited. Stop on breach; explain in the report.
    Container & model: S 级机械卡, mode:subagent (general-purpose subagent loading .claude/agents/os-dev.md as its role file), model: default tier, effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the showcase's own flagship 会签 flow declares no write-back | an approved expense report keeps reading Submitted | anyone who copies the demo, today
    Thread-read: none
    Serial constraints cleared: no open PR touches the showcase flows file at claim (13 open PRs read at 2026-10-23T18:15:00Z on 842a0aef9d).
    Resume-from: none

  5. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
     "issue": 25469,
     "status": "done",
     "branch": "claude/issue-25469-showcase-signoff-mirror",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/25525",
     "session": "session_016g7nYP9wdNx1GDa5XDr7Uy",
     "premise_still_valid": true,
     "summary": "Both showcase_expense_signoff (dual_signoff, per_group) and showcase_committee_quorum (committee, quorum) ended their approve/reject arms on bare end nodes, so the report kept reading submitted. Each now runs an update_record per outcome arm (approve -> status approved, reject -> status draft, the revert-on-reject precedent of showcase_invoice_signoff) under runAs system. approvalStatusField was measured and rejected: plugin-approvals mirrorStatusField writes the request vocabulary verbatim (pending on open; approved/rejected on decide; recalled on recall/dead-run sweep; returned on send-back; rejected on revision-limit auto-reject), has no mapping (spec key is a bare z.string; a mapping would need packages/spec), and skips with a warn any value the field's options do not declare. showcase_expense_report.status declares draft/submitted/approved/reimbursed, so a mirror would warn on every submit and leave a rejected report reading Submitted; the spec also advises a readonly mirror target and status is submitter-edited. Landing: examples/app-showcase/src/automation/flows/index.ts plus new pin examples/app-showcase/test/expense-approval-write-back.test.ts; base dc3e5b9d7b, head 3e661a73f6.",
     "tests": "head 3e661a73f6. New pin test/expense-approval-write-back.test.ts (real ObjectKernel + ObjectQL + automation + approvals over sqlite, real objects/flows, decisions via ApprovalService.decide, describe.each over both flows: approve / reject / CONTROL with the pre-fix bare-end shape leaving status submitted): 6/6 passed. Ablation via scripts/ablation-replace.mjs on committed HEAD: anchor e2 dual_signoff -> mark_approved replaced with target approved, anchor x1 -> x0, blob a47c39e35483 -> 78466ece32b2; result Tests 1 failed | 5 passed (approve case: expected [start, dual_signoff] to include mark_approved); restored blob == HEAD, git diff HEAD empty. Source-imported via relative ../src, no dist leg. Showcase typecheck + full test under the lock: VERDICT command-exit 0, Test Files 38/38, Tests 461/461; new test is in the tsc program (--listFiles 1 hit). Dependency closure build pnpm --filter '@objectstack/example-showcase^...' build: VERDICT command-exit 0.",
     "gates": "dispatch-gates --commands: 45 commands, each run and recorded with exit code; 44 exit 0; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (unbuilt packages outside the closure: studio, client-react, embedder-openai, ...) = NOT MEASURED locally. --ran: '45 derived famil(ies) accounted for - 44 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)'. PR #25525 check-runs on 3e661a73f6, all completed: success - Lint & Repo Gates, TypeScript Type Check (+ workspace/source gates/debt ledger/consumer gates lanes), Test Core (1-6/6 + aggregate), Build Core, Governed Surface Queue Guard, Check Changeset, Check PR Size, Dogfood Regression Gate, Check Documentation Links, all Lint lanes (ESLint, PM dispatch-gates, CI release & engine gates, repo & tooling, docs & code guards, scoped x3), claim/single-writer/Part-of checks; skipped by design - Temporal Conformance, Dogfood Verify CLI, Dogfood shards, Console Pin Gate, Build Docs, opt-in harnesses.",
     "dogfood_battery": "n/a - no ! in the PR title and no (narrowing) arm",
     "mcp_calls": "0",
     "api_writes": "3 - git push (direct, not REST); relay pr_create via fleet-write/dispatch.mjs (POST /repos/objectstack-ai/objectstack/dispatches -> pulls create + issues/25525/assignees os-sales + issues/25525/labels skip-changeset); this os-dev-report comment via post-stamped.mjs (issues/25469/comments)",
     "open_questions": [],
     "out_of_scope_findings": [
      "carrier: none - noted in PR Acceptance notes, not filed: docs/qa/platform-checklist/areas/approvals.json approvals.status-mirror-field knownGaps still says no showcase flow declares approvalStatusField; stays true by design (mirror vocabulary does not fit the report status options).",
      "carrier: none - noted in PR Acceptance notes, not filed (read-only inference, not measured): showcase_expense_signoff start gate has no amount ceiling, so a report >= 5000 matches both expense flows' start conditions on submit; the seed routes the demos to different records deliberately."
     ]
    }
  6. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    Contributor

    Landed · domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · 2026-10-23T19:51Z

    reading value
    PR #25525 MERGED through the queue (ACCEPT 6373071000), head 3e661a73f6
    origin/main carries 1ff43b67ea

    State: Fixes #25469 closed the card. pm:dispatched is now off and the assignee is cleared.


    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

No one assigned

    Labels

    area:workflowApprovals and automation — the work that runs without a person driving itbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions