Skip to content

approvals: an expression approver that yields the same user id twice opens two co_sign slots for one person — the slate is not deduped #28684

Closed

Description

@objectstack-fleet

domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

Carries item (c) of #28658; #28658 keeps items (a) and (b), which wait on a maintainer decision (the published contract says a decision output's type/multiple only shape the input widget). Part of #28658 · in-flight derived sub-card (execution-duties.md: in-scope derivative of a claimed card, seat-owned, priority inherited p2, dispatched directly).

What happens

Measured on #28658 at framework 6e11a929 (stock showcase, showcase_dynamic_approval): approving stage 1 with {"outputs":{"next_reviewers":["ADMIN_ID","ADMIN_ID"]}} makes stage 2's co_sign node open two approver slots for the same user. One person then has to sign twice, or a co-sign quorum counts one person as two signatures.

This needs no contract change: the expression approver already accepts "ids (string / CSV / string array)" (plugin-approvals/src/approval-service.ts, the expression branch, about L2715–2760), and nothing published says a slate may name one person twice.

Wanted

  • The approver slate built from an expression approver names each resolved user once (dedupe after resolution, including after out-of-office delegation, so two ids delegated to the same person also yield one slot). Order of first appearance is kept.
  • Check whether the static approver kinds and the graph resolveAs kinds already dedupe; if one of them has the same hole, fix it at the same seam and name it in the PR, otherwise say they are covered.

Tests

  • Stage 2 opened from ["A","A"] has one slot for A; ["A","B","A"] has two slots, A then B.
  • Two ids whose OOO delegation routes to the same delegate yield one slot.
  • Ablation: remove the dedupe, show red, restore.

Not here

Refusing an unknown id, a scalar for multiple: true, or duplicates at the decision itself: #28658 (a)/(b), pending the decision.

Dedupe: searched co_sign duplicate approver, expression approver dedupe, same user twice slot — no open card.

Activity

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

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

    Claim: PM loop round 1 · card thread · 2026-11-01T13:27Z
    Session: session_01FEkQ5oc56xChD6Wrc4cGSt (the seat); the card thread is session_01FEkQ5onyCFb1sK8Uvu76U7
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-v6mwdv (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#5
    Ruling-ref: 「定级你可以自己决定」 (Leehom, 2026-11-01T12:11Z); in-flight derived sub-card of #28658 (item (c)), seat-owned, priority inherited p2, dispatched directly; the same card thread continues, its #28658 claim (6558537451) is superseded for (c) by this one.
    File surface: packages/plugins/plugin-approvals/src/approval-service.ts (approver slate resolution: the expression branch and, if they share the hole, the static/graph kinds at the same seam) and its tests; a @objectstack/plugin-approvals patch changeset. ⛔ No decision-output validation (#28658 (a)/(b), pending decision), ⛔ no packages/runtime/**, packages/spec/** or ui/**.
    Container & model: S, mode:thread, model and effort as the thread was opened
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the code is @objectstack/plugin-approvals' approver slate resolution · no platform path dedupes it today · reached by any expression approver yielding a repeated id, used today by showcase_dynamic_approval
    Thread-read: none
    Premise: holds (read at origin/main 2678600d: the expression branch pushes each resolved id into slots with no dedupe; measured on #28658 at 6e11a929)
    Serial constraints cleared: #28657 (seat 1) is in packages/runtime/src/domains/actions.ts, not plugin-approvals. #28658 is out of flight (decision box), so nothing else edits approval-service.ts.
    Resume-from: none — fresh from origin/main

    State: created in pm:dispatched, assigned to os-project-manager, recorded by this claim. Order: pm-orders/server/order-28684.md in the project.

  3. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-project-manager · session_01FEkQ5onyCFb1sK8Uvu76U7

    os-dev-report
    {
    "issue": 28684,
    "status": "done",
    "branch": "claude/project-thread-v6mwdv",
    "pr": "#28705",
    "session": "session_01FEkQ5onyCFb1sK8Uvu76U7",
    "premise_still_valid": true,
    "summary": "ApprovalService.expandApprovers now names each resolved person once per approver entry, after OOO delegation, in first-appearance order, and records each per_group tag once per person through one helper. Premise measured beyond the card: the tally already reads Object.keys(__approverGroups), so a duplicate slot does not make a person sign twice; the real effects were a doubled pending_approvers entry and, under per_group, a doubled group tag that let one approval count twice toward minApprovals (test 4, ablation 2: 'expected true to be false'). Ruling 2 check: expression user and graph resolveAs kinds and static field had the hole and are fixed at the same loop; static user/manager are single-valued; static graph kinds already return a Set. Across two approver entries the slate is unchanged (documented #24751 shape, display only), noted in Acceptance notes.",
    "tests": "head 28e36c0 (code commit 09b3856; the main merge brought no plugin-approvals change). New describe in plugin-approvals approval-service.test.ts, 7 tests: base blob → 'Tests 6 failed | 1 passed' (test 7 is a control, green on both), head → 'Tests 7 passed'. Ablation 1 (slot dedupe off, both branches, via ablation-replace, injected count 2 on disk) → 6 failed; ablation 2 (tag dedupe off, mutant count 1) → 3 failed incl. per_group 'expected true to be false'; each restored with git checkout HEAD, git diff HEAD empty, blob hash matched. The suite imports ./approval-service.js from source, so no build sits in the ablation path. pnpm --filter @objectstack/plugin-approvals test: Test Files 106 passed (106), Tests 1373 passed (1373); typecheck exit 0 (check:test-typecheck OK). Live probe on a stock showcase (full pnpm build, pnpm dev -- --fresh): #28658 reproduction with next_reviewers [ADMIN,ADMIN] → base (base blob + plugin-approvals rebuild, dist-preflight --absent) co_sign pending_approvers [ADMIN,ADMIN]; head [ADMIN]. Commands in PR body and pm-orders/server/28684-pr-body.md.",
    "gates": "head 28e36c0: dispatch-gates --commands derived 63 families; all run with exit codes; --ran: '63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED (a DERIVED zero — all 63 recorded an exit code and none of them is 3)'. 62 exit 0 first pass; check-plugin-teardown-shape --self-test exit 3 (shallow clone) → after git fetch --depth=1 of 621a487 exit 0, 48 cases pass. Scoped lint (eslint packages/plugins/plugin-approvals --no-inline-config) exit 0. dist predates the final main merge (no plugin-approvals change in it). CI check-runs: in_progress at report time. Record: pm-orders/server/28684-gates-ran.txt.",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
    "mcp_calls": "0",
    "api_writes": "3 — git push (branch); relay pr_create → #28705 (run 41316121616, read-back identical, no assignees, no labels: the order names none and the PR carries a changeset); POST issues/28684/comments (this report). Earlier on #28658 in this thread: 1 comment (report 6558577952).",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none · noted, not filed — two approver entries naming one person still store that person once per entry in pending_approvers until the first vote (documented in remind's #24751 comment; the tally is already per person). Product call on display only; in PR Acceptance notes.",
    "carrier: #28658 · decision-door refusal of unknown ids / scalar for multiple:true / duplicates stays with #28658 (a)/(b), pending the maintainer decision."
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

    ACCEPT → PR #28705 comment 6559412079 · head 28e36c06 · 2026-11-01T14:40Z. Landing: pr_ready + automerge_enable through the relay (run 41317292246); the merge queue closes this card.

  5. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

    Landed · 2026-11-01T14:58Z · PR #28705 merged through the merge queue as 6cbdef1e44; this card closed as completed by its Fixes line.

    reading value
    PR state merged, 6cbdef1e44
    card closed (completed); pm:dispatched removed in this stroke, domain:* / area:* kept

    #28658 stays open in needs-user-decision (items (a)/(b)), as intended.

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

Metadata

Metadata

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