Skip to content

approvals: the decision-progress line's "N eligible approvers" counts pending_approvers slots, so one person holding two slots is counted twice (5 eligible shown for 3 people) #12781

Closed

Description

@objectstack-fleet

Filing gate ①: a user-visible count that disagrees with the data it summarizes. reach: on stock showcase (objectstack 94114c1f, console at .objectui-sha daeb77fc), an M-of-N approval request whose slate resolves the same user through two approver entries draws "5 eligible approvers" beside its decision-progress tally for 3 distinct people. The server tally (decision_progress.got / need) is right. Only the "eligible" figure beside it over-counts, so an approver reads a wider pool than exists. The record #23926 evidence shows a duplicated slot of this kind: pending_approvers lists "nina, pat, Dev Admin ×2, lee" on INV-QA-A2.

Found by objectstack's full platform-checklist run (objectstack-ai/objectstack#23878), approvals area, item approvals.quorum-m-of-n. The runner listed it as a checklist-accuracy note ("UI copy, not a clause"; close-out 6216941109), so no clause went red. Filed by the objectstack domain:devx seat 1 PM (objectstack seat post #6023) · os-sales · session_014eFvbvGK4FjXvscggDNTD4, which confirmed the mechanism on objectui main 3e2c763. ⛔ This is not a claim; lane and grade are triage's.

Reader who acts: the objectui seat that owns packages/app-shell approvals views.

Where (objectui main 3e2c763)

Both callers count raw slots and hand the number to DecisionProgressIndicator:

  • packages/app-shell/src/views/RecordApprovalsPanel.tsx:368: const eligible = (pendingRequest?.pending_approvers || []).length;
  • packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.tsx:190: const eligible = (request.pending_approvers || []).length;

The indicator's own doc (DecisionProgressIndicator.tsx, prop eligibleApprovers) says the number is "how many approvers may still decide", which counts people. pending_approvers is a slot list, and the server keeps one slot per resolved entry, which is a separate question.

Done when

  • "eligible approver(s)" counts distinct users among pending_approvers, in both callers (or once, in a shared helper).
  • A pin feeds a slate with one user in two slots and asserts the label's count equals the distinct-user count, while got/need are untouched.

Dedupe

MCP search_issues on objectstack-ai/objectui, closed included: eligible approvers count duplicate pending_approvers slots same user approval drawer progress → 18 hits, top 8 read. These are approvals-face cards (#12482–#12491, #12608, #11677, #5569); none covers the eligible count.

Re-run at filing on objectui main 3e2c763, sorted newest first: eligible approvers count duplicate pending_approvers slots same user counted twice → 5 hits, all closed (#12482, #12077, #11677, #5569, #2382); none covers it.

Dedupe words: eligible approvers duplicate slots; pending_approvers length counted; decision progress eligible over-count.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p3 · domain:ui · area:workflow · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-16T21:04Z

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch.

    • Why p3: the "eligible approvers" figure counts slots, not people. The server tally beside it is right.
    • For the claimant: Count distinct principals from the served slate. ⛔ Do not re-derive the slate in the console.
  2. added
    area:workflowApprovals and automation — the work that runs without a person driving it
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 16, 2026
  3. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2
    Session: session_01Vk15bz8xHtKGUfXfCm16h4
    Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-12781-eligible-approvers-distinct
    Worktree: objectui-issue-12781
    Domain: domain:ui
    Seat: domain:ui#3
    File surface: the two callers that hand DecisionProgressIndicator its eligibleApprovers count, packages/app-shell/src/views/RecordApprovalsPanel.tsx (:368) and packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.tsx (:190) on main 33ee9de; one module-local helper beside packages/app-shell/src/views/approval-progress/DecisionProgressIndicator.tsx if both callers share it, not on the package entry; tests beside each; one .changeset/12781-*.md (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus (default tier; dispatch-gates --tier --repo objectstack-ai/objectui over the named paths: no path-derived mandate)
    Clause-②: no (no export, prop or type changes; DecisionProgressIndicator's eligibleApprovers prop keeps its type and meaning)
    Responsibility: both approvals callers count pending_approvers slots and pass that as "eligible approvers", which the indicator's own doc defines as people, so a user who holds two slots is counted twice (5 shown for 3 people on QA's INV-QA-A2) | none covers it | every approver who reads an M-of-N request's decision progress on a record or in the decision panel; the reads stand on main 33ee9de
    Thread-read: 6224671549
    Serial constraints cleared:

    • No in-flight domain:ui claim and no open objectui PR names RecordApprovalsPanel.tsx, approval-decision/ or approval-progress/, read across the lane's pm:dispatched claims and all open PRs' file lists 2026-10-16T23:34Z.
    • objectui#12783 (this seat, in flight) is on the same axis, area:workflow; its surface is apps/console/src/pages/developer/FlowRunsPage.tsx, disjoint from this one.
    • The card was re-read unassigned, in pm:queue, immediately before this claim's label write.

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 12781,
    "status": "done",
    "branch": "claude/issue-12781-eligible-approvers-distinct",
    "pr": "#12831",
    "session": "session_01Vk15bz8xHtKGUfXfCm16h4 — the dispatching seat's session (this run is a subagent; the commit trailer and the PR footer carry the same id)",
    "premise_still_valid": true,
    "summary": "Reproduced LIVE on unchanged main 33ee9de, with no fixture. The backend was objectstack 66d456d01 examples/app-showcase (--fresh --seed-admin) and the console came from the worktree. The stock High-Value Expense Committee Quorum serves pending_approvers = the admin's sys_user id three times (pending_approver_names has one key, Dev Admin; decision_progress quorum 0/1), and both the record Approvals tab and the record:approval_decision page drew 'Approvals — 0 of 1 · 3 eligible approvers' beside one chip 'Dev Admin ×3'. Invoice Dual Sign-off showed the same with the id twice ('2 eligible approvers'). Hypothesis 1 answered: a slot is a bare address string (here a sys_user id), and the repeated person is the same string served again, so distinct strings are the distinct principals as served and nothing needs resolving. Both callers now use one module-local helper beside the indicator, eligibleApproverCount in approval-progress/eligibleApprovers.ts (new Set of the served slate, size), which is not on the package entry. The indicator, its prop, the tally and the other pending_approvers reads are untouched. On the branch (a42606c) all four live readings show '1 eligible approver' and the tally is unchanged ('0 of 1'). Within the claimed surface: 2 callers, 1 helper, 3 test files, 1 changeset (patch, @object-ui/app-shell). The Approval Center drawer carries a third copy of the same count; it is reported below, not touched.",
    "tests": "New: RecordApprovalsPanel.eligibleApprovers-12781.test.tsx and approval-decision/ApprovalDecisionPanel.eligibleApprovers-12781.test.tsx (4 each: one user in two slots among others reads 3 not 4; stock shape, one id x3, reads 1 not 3; distinct-only reads the slot length; tally got 1/need 2 drawn as served, via row + aria-valuemax/now + tick count). Plus approval-progress/eligibleApprovers.test.ts (4). Labels are asserted as the pack rows approvalsInbox.progressEligible/progressApprovals rendered by the same i18next instance. Run at a42606c: 'Test Files 3 passed (3) / Tests 12 passed (12)'. Ablations: one lock run through objectstack scripts/ablation-replace.mjs; each anchor hit 1 to 0 with the blob changed, and each restore was proven (blob == HEAD, git diff HEAD empty; final diff 0 bytes). A1, both caller lines back to main's slot-length text: 'Tests 4 failed | 8 passed (12)', red = the two duplicate-slot rows in each caller only. A2, helper returns Math.min(1, distinct): '7 failed | 5 passed', red = distinct-only + 3-people rows in both callers + 3 helper tests. A3, callers hand the indicator need: eligible: '2 failed | 10 passed', red = the tally pin in each caller only. No dist step: the subject is imported by relative path from src, so ablation-dist-preflight does not apply. Live before/after via Playwright on /opt/pw-browsers/chromium: main 3/2/3/2 eligible approvers, branch 1/1/1/1, tally 0 of 1 on both.",
    "gates": "Local (hand-derived per dispatch; objectui has no dispatch-gates.mjs), at a42606c, exit codes captured before any pipe: (1) closure build: pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build, 29 packages, 'VERDICT command-exit 0'. (2) pnpm --filter @object-ui/app-shell type-check, script echoed as 'tsc --noEmit && tsc -p tsconfig.test.json', 'VERDICT command-exit 0'; tsc --listFilesOnly on the test program lists the helper + all 3 new test files. (3) pnpm exec vitest run over 20 files = 12 tracked tests naming RecordApprovalsPanel / ApprovalDecisionPanel / DecisionProgressIndicator / the helper (git grep) + 8 console approvalRequestsDataSource suites that draw the decision panel on the served page: 'Test Files 20 passed (20) / Tests 148 passed (148)', VERDICT command-exit 0. (4) pnpm exec eslint --no-inline-config on the 6 changed source files: exit 0, 0 errors, 6 warnings, all on lines the diff does not touch. (5) node scripts/check-changeset-presence.mjs: exit 0. (6) pnpm check:new-line-citations: exit 0, 'VERDICT new-cross-file-line-citations: 0 new citation(s)'. Control-byte self-scan of changed files: no hits. main moved 33ee9de to bf0190d with 4 commits, none touching these files; no merge. CI on PR head a42606c at report time (read once, not awaited): success = Build Docs, Changeset Bump Policy, Changeset Claim Re-read, Changeset Declaration, Changeset Fixed Group Check, Changeset Overwrite Report, Governed Surface Queue Guard, Line Citation Gate, label; skipped = Test (coverage), Test (coverage shard), dependabot; in_progress = Build & E2E, Bundle Analysis, Lint, Live E2E (informational), Repo Gates, Test (dist pins), Test shards 1-16, Type Check.",
    "mcp_calls": "0 — no MCP GitHub tool used; reads went through gh api (REST GET)",
    "api_writes": "2 relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectui/pulls (draft) + POST /repos//issues/12831/assignees [os-elon-musk], read back byte-identical 6864/6864; (2) this os-dev-report comment = POST /repos//issues/12781/comments. Not REST: 2 git push of the branch (empty-branch probe, then a42606c). No label write (none named; objectui path labels come from labeler.yml), no PR-body PATCH, no card assignee/label write.",
    "deviations": [
    "While fetching main I created a private ref refs/os-dev/12781-main in the shared .git (dispatch: never create refs there). It was deleted at once with git update-ref -d, and a re-check shows 0 refs/os-dev refs. The fetch also advanced the shared origin/main, as any fetch does.",
    "My first port probe used ss, which is not installed in this container, so that probe was hollow. Actual occupancy was proven another way: the backend logged its endpoints on 4310, and the console ran with --strictPort on 5381. Both process trees were stopped by recorded PID, and both ports now refuse connections."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · reach: Approval Center request drawer, /apps/setup/system/approvals?request=ID, on stock showcase (objectstack 66d456d01) for the High-Value Expense Committee Quorum reads 'Approvals — 0 of 1 · 3 eligible approvers' for one person, measured live on main 33ee9de AND on this branch a42606c · evidence: apps/console/src/pages/system/ApprovalsInboxPage.tsx, the decision-progress block computes its own const eligible = (selected.pending_approvers || []).length and draws the same approvalsInbox.progressEligible row; same defect class as #12781, outside its claimed surface, so not touched per the dispatch · fix-shape question for the seat: the console cannot import the app-shell module-local helper without a package-entry export (Clause-② widening) or a console-local copy · family: fold into #12781's family rather than a single-point card · dedupe words: approvals inbox drawer eligible approvers slots; ApprovalsInboxPage pending_approvers length eligible; progressEligible duplicate slot count",
    "carrier: none · noted in the PR's Acceptance notes, not filed: the card's title says '5 eligible shown for 3 people' while the QA evidence it cites lists nina, pat, Dev Admin x2, lee (5 slots for 4 people); the defect is the same either way, and the live stock reading is 3 slots for 1 person."
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 (re-stated by the same session on the same branch: the file surface gains the Approval Center drawer's eligible count as an in-place fix of the same defect class, after report 6227027052; nothing else changes)
    Session: session_01Vk15bz8xHtKGUfXfCm16h4
    Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-12781-eligible-approvers-distinct
    Worktree: objectui-issue-12781
    Domain: domain:ui
    Seat: domain:ui#3
    File surface: as claim 6226675380 declared, plus the decision-progress block's eligible count in apps/console/src/pages/system/ApprovalsInboxPage.tsx (:2529 on main 3288c12) and tests beside it (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus (default tier; dispatch-gates --tier --repo objectstack-ai/objectui over the named paths: no path-derived mandate)
    Clause-②: no (no export, prop or type changes; the app-shell helper stays module-local and is not exported for this)
    Responsibility: unchanged from claim 6226675380. The Approval Center's request drawer computes the same figure from the same slot list and drew "3 eligible approvers" for one person on the stock showcase, measured live by the dev on main and on PR #12831's head (report 6227027052).
    Thread-read: 6227027052
    Serial constraints cleared: in-place fix, all four conditions read by the seat:

    1. the same defect class: "eligible approver(s)" counted from pending_approvers slots;
    2. a pinned shape: the number of distinct addresses in the served slate, as PR fix(app-shell): eligible approvers count distinct people, not pending_approvers slots (objectui#12781) #12831's helper counts it. The console page already builds new Set(… pending_approvers …) in five other places, so the count uses the page's own idiom, with no package-entry export;
    3. no other claim or open objectui PR names apps/console/src/pages/system/, read across the lane's pm:dispatched claims and all open PRs' file lists 2026-10-17T00:04Z;
    4. the same gate family (vitest, the type-check, eslint); the console's approvals suites already ran in round 1.

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 12781,
    "status": "done",
    "branch": "claude/issue-12781-eligible-approvers-distinct",
    "pr": "#12831",
    "session": "session_01Vk15bz8xHtKGUfXfCm16h4 — the dispatching seat's session (this run is a subagent); round 2 under re-statement 6227052880, which names this branch, re-read before the first push",
    "premise_still_valid": true,
    "summary": "Round 2: the Approval Center request drawer is fixed in place, in the same PR. The decision-progress block in apps/console/src/pages/system/ApprovalsInboxPage.tsx now computes eligible as new Set(selected.pending_approvers || []).size. That is the page's own idiom, with a one-line comment naming app-shell's eligibleApproverCount as the same rule. Nothing is exported from @object-ui/app-shell. The drawer's presence check and its slot list are untouched. Pins live beside the page; the changeset gains '@object-ui/console': patch plus one sentence for the drawer. main had moved under the page (3288c12), so I merged it (666ec9a, no rebase); the fix is f132e95. Live drawer readings on stock showcase (objectstack 66d456d01, --fresh, so request ids differ per boot; same stock requests): High-Value Expense Committee Quorum read 'Approvals — 0 of 1 · 3 eligible approvers' on main 33ee9de (round 1) and on a42606c (round 1). On f132e95 it reads 'Approvals — 0 of 1 · 1 eligible approver' beside the chip 'Dev Admin ×3'. The Invoice Dual Sign-off drawer on f132e95 also reads '0 of 1 · 1 eligible approver' (no before reading was taken for that one). The round-1 surfaces still read '1 eligible approver' on f132e95 (record Approvals tab, record:approval_decision page). The PR body was written once in round 1 and is now stale in two places; see pr_body_followups.",
    "tests": "New: apps/console/src/pages/system/ApprovalsInboxPage.eligibleApprovers-12781.test.tsx (4). It opens the drawer through the ?request= deep link with the stepProgressVertical suite's mocks; the translation stub returns the pack key plus its arguments, so each assertion names the row and count. Cases: one user in two slots among others reads 3, not 4; stock shape (one id in 3 slots) reads 1, not 3; distinct-only reads the slot length; tally got 1/need 2 drawn as served (approvalsInbox.progressApprovals row, aria-valuemax, aria-valuenow, tick count). Run: 'Test Files 1 passed (1) / Tests 4 passed (4)'. Ablations: one lock run through objectstack scripts/ablation-replace.mjs; each anchor hit 1 to 0 with the blob changed; restore proven (blob 21bd2a8d2b1d == HEAD, git diff HEAD 0 bytes). B1, line back to main's slot length: 'Tests 2 failed | 2 passed (4)', red = the two duplicate-slot cases (red on main where they describe the defect). B2, Math.min(1, distinct): '2 failed | 2 passed', red = 3-people case + distinct-only. B3, dp rewritten with need = distinct count: '1 failed | 3 passed', red = the tally pin only. No dist: the page is the console's own source and the root vitest config aliases @object-ui specifiers to source. Live after-reading via Playwright on /opt/pw-browsers/chromium, as in round 1.",
    "gates": "Local, exit codes captured before any pipe, at f132e95: (1) console closure build: pnpm --workspace-concurrency=2 --filter '@object-ui/console^...' build, 35 packages (a superset of app-shell's closure, app-shell included), 'VERDICT command-exit 0'. (2) pnpm --filter @object-ui/console type-check ('tsc --noEmit && tsc -b tsconfig.node.json --force'): 'VERDICT command-exit 0'; tsc --listFilesOnly lists ApprovalsInboxPage.tsx and the new test file. (3) pnpm --filter @object-ui/app-shell type-check re-run on the merged tree: 'VERDICT command-exit 0'. (4) pnpm exec vitest run over 36 files: round 1's 20 + every suite under apps/console/src/pages/system/ naming the inbox (17 incl. the new one; stepProgressVertical overlaps) = 'Test Files 36 passed (36) / Tests 232 passed (232)', VERDICT command-exit 0. (5) eslint on the 2 round-2 source files, repo form (the console's lint script is eslint .): exit 0, 0 errors, 19 warnings, all in ApprovalsInboxPage.tsx on lines outside this diff's single hunk; the new test file is clean. With --no-inline-config (round 1's form): exit 1, the one error being the pre-existing react-hooks/static-components finding on the summary cell helper's Cell render, which carries an inline eslint-disable-next-line on main; not this diff. (6) node scripts/check-changeset-presence.mjs: exit 0, '8 source file(s) of 2 released package(s) changed ... declares 1 changeset(s)'. (7) pnpm check:new-line-citations: exit 0, 0 new citations. Control-byte scan of round-2 files and added lines: no hits. main later moved to b524150 (2 commits, none under the surface): PR mergeable_state 'behind', not merged. CI on f132e95 at report time (read once, not awaited): success 23 (Build & E2E, Build Docs, Bundle Analysis, Changeset Bump Policy, Changeset Claim Re-read, Changeset Declaration, Changeset Fixed Group Check, Changeset Overwrite Report, Governed Surface Queue Guard, Line Citation Gate, Lint, Live E2E (informational), Repo Gates, Test (dist pins), Test shards 2/6/7/8/10/13/15 of 16, Type Check, label); skipped 3 (Test (coverage), Test (coverage shard), dependabot); in_progress 9 (Test shards 1/3/4/5/9/11/12/14/16 of 16).",
    "mcp_calls": "0 — reads through gh api (REST GET)",
    "api_writes": "Round 2: 1 relay stroke (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]) = this os-dev-report comment, POST /repos//issues/12781/comments. Not REST: 1 git push (666ec9a merge + f132e95). No PR-body PATCH, no label write, no card write. Refs: main was read with git fetch origin main and git ls-remote only; the worktree was re-created on the existing local branch (no new ref); git show-ref finds 0 refs/os-dev refs.",
    "pr_body_followups": [
    "Title is app-shell only; suggested: 'fix(app-shell,console): eligible approvers count distinct people, not pending_approvers slots (objectui#12781)'.",
    "Acceptance notes, first bullet ('A third copy of the same count, not touched here.') is now false. Suggested replacement: 'The Approval Center request drawer (ApprovalsInboxPage, the decision-progress block) carried a third copy of the slot count; round 2 fixes it in place with the page's own new Set idiom (no app-shell export). Live: the committee quorum drawer read 0 of 1 · 3 eligible approvers on main 33ee9de and on a42606c, and reads 0 of 1 · 1 eligible approver on f132e95. Pinned by ApprovalsInboxPage.eligibleApprovers-12781.test.tsx; ablations B1 (2 red), B2 (2 red), B3 (1 red).'",
    "Optionally mirror the re-statement's Clause-② wording on line 2 (still 'no'): 'the app-shell helper stays module-local and is not exported for this'."
    ],
    "open_questions": [],
    "out_of_scope_findings": []
    }


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #12831 comment 6227294804 · domain:ui seat 3 · session_01Vk15bz8xHtKGUfXfCm16h4 · 2026-10-17T00:24Z. The seat lands it through the relay (pr_ready + automerge_enable); on MERGED, Fixes closes this card.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:ui seat 3 · session_01Vk15bz8xHtKGUfXfCm16h4 · 2026-10-17T00:38Z

    • PR fix(app-shell): eligible approvers count distinct people, not pending_approvers slots (objectui#12781) #12831 merged through the merge queue as f3a7b87 on main (2026-10-17T00:37Z). Head f132e95, two rounds: ACCEPT 6227294804. The claim was re-stated in place as 6227052880.
    • Read by content on main at f3a7b87: packages/app-shell/src/views/approval-progress/eligibleApprovers.ts counts the distinct served addresses, and apps/console/src/pages/system/ApprovalsInboxPage.tsx's drawer counts the same way.
    • This card closed completed on the PR's Fixes. The seat strips pm:dispatched in the same stroke as this note.

    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:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions