Skip to content

feat(approvals): quorum + per-group sign-off (会签) + decision attachments (#3266)#3268

Merged
os-zhuang merged 2 commits into
mainfrom
claude/enterprise-delegation-ooo-proxy-1v3m8l
Jul 19, 2026
Merged

feat(approvals): quorum + per-group sign-off (会签) + decision attachments (#3266)#3268
os-zhuang merged 2 commits into
mainfrom
claude/enterprise-delegation-ooo-proxy-1v3m8l

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3266 (Phase 1 of gap #5). Node-internal multi-approver semantics — the钉钉/Salesforce-style patterns — delivering "legal AND finance both approve" and "M-of-N" with zero flow-engine changes. Topology-level parallel branches remain a separate engine ADR (#3267); weighted voting + approval-matrix governance stay enterprise (objectstack-ai/cloud#861).

Why this shape

Investigation showed serial multi-step, comments, recall, reassign, and OOO are already done; the real gaps were quorum and per-group (会签). Every mainstream admin-facing platform (钉钉/飞书/Salesforce/ServiceNow) delivers these as node-internal rules, not parallel gateways — which also sidesteps the "one pending request per record" / record-lock / status-mirror complications that true parallel branches introduce.

Changes

Spec (approval.zod.ts)

  • behavior += quorum (M-of-N) and per_group (one — or minApprovals — from each group); new minApprovals threshold; optional group label on each approver. A single rejection is always a veto. Corrected the stale "quorum is enterprise" comment.

Engine (approval-service.ts)

  • expandApprovers refactored into resolveApproverSpec + a group collector, so each resolved approver (OOO-substituted included) is tagged with its group; the map is snapshotted at open so the tally is stable.
  • decideNode: unified quorum/per_group/unanimous finalization via isApprovalSatisfied; thresholds clamp to the resolvable count / group size so a mis-set value can never deadlock. reassign transfers a slot's group so the new approver still counts.

Attachments

  • sys_approval_action gains a Field.file attachments column; decide/comment accept + persist file references, threaded through the contract, the REST decide/comment routes, and the client approve/reject helpers.

ShowcaseExpenseSignoffFlow: manager + finance per_group 会签 on a submitted expense report.

Tests & verification

  • 9 new unit tests (quorum boundary/clamp/veto; per_group each-group, two-in-one-group, minApprovals=2, OOO-substituted-member counting; attachments). 132 green in plugin-approvals; full monorepo build clean.
  • Browser-verified end-to-end on the running showcase (real SQLite): submitting an expense report opened a per_group request pending on both a manager and an auditor; the manager's approval held the node (finalized=false, pending=[auditor]); the auditor's approval finalized it (approved, flow resumed); the finance decision's audit row persisted attachments:["file_receipt_scan"].

Scope boundary

🤖 Generated with Claude Code


Generated by Claude Code

…nts (#3266)

Node-internal multi-approver semantics — the钉钉/Salesforce-style patterns that
deliver "legal AND finance both approve" and "M-of-N" without any flow-engine
change (topology-level parallel branches remain #3267). Community-core; weighted
voting + approval-matrix governance stay enterprise (objectstack-ai/cloud#861).

Spec (approval.zod.ts):
- behavior enum += 'quorum' (M-of-N) and 'per_group' (one — or minApprovals —
  from EACH group), plus a `minApprovals` threshold and an optional `group`
  label on each approver. A single rejection is always a veto. Corrected the
  stale "quorum is enterprise" comment.

Engine (approval-service.ts):
- expandApprovers refactored to resolveApproverSpec + a group collector, so each
  resolved approver (OOO-substituted included) is tagged with its group; the
  map is snapshotted at open so the tally is stable.
- decideNode: unified quorum/per_group/unanimous finalization via
  isApprovalSatisfied; thresholds clamp to the resolvable count / group size so
  a mis-set value can never deadlock. reassign transfers a slot's group so the
  new approver still counts.

Attachments (#3266):
- sys_approval_action gains a `Field.file` `attachments` column; decide/comment
  accept + persist file references, threaded through the contract, REST
  decide/comment routes, and the client approve/reject helpers.

Showcase: ExpenseSignoffFlow — manager + finance per_group 会签 on a submitted
expense report.

Tests: 9 new (quorum boundary + clamp + veto, per_group each-group /
two-in-one-group / minApprovals=2 / OOO-substituted member counts, attachments);
132 green in plugin-approvals.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ypkQikZ55oWUHUnMebwXA
@vercel

vercel Bot commented Jul 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 19, 2026 10:57am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/client, @objectstack/plugin-approvals, @objectstack/rest, @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via packages/client, @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/data-flow.mdx (via @objectstack/client)
  • content/docs/api/environment-routing.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/client, @objectstack/rest, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/plugin-approvals, packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/client)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/client, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/client)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/client, @objectstack/plugin-approvals, @objectstack/rest, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/client)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/client, @objectstack/plugin-approvals, @objectstack/rest, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-approvals, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

… quorum/per_group (#3266)

CI follow-ups on the #3266 spec change:
- Regenerate content/docs/references/automation/approval.mdx (generated from the
  spec schema; the new behavior enum / minApprovals / approver group made it stale).
- approval.test.ts: the "rejects an unknown behavior" case hard-coded 'quorum'
  as its unknown value — now a valid behavior. Switch it to 'weighted' and add
  positive coverage that quorum / per_group / minApprovals / grouped approvers parse.
- approvals.mdx: document the behavior matrix (first_response / unanimous /
  quorum / per_group), the veto rule, minApprovals clamping, and decision attachments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ypkQikZ55oWUHUnMebwXA
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 19, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review July 19, 2026 11:26
@os-zhuang
os-zhuang merged commit 4b659d0 into main Jul 19, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/enterprise-delegation-ooo-proxy-1v3m8l branch July 19, 2026 11:26
os-zhuang pushed a commit that referenced this pull request Jul 19, 2026
…contract mapping (#3266)

Browser verification caught a #3268 gap: the `attachments` column landed on
sys_approval_action and decide() persisted it, but rowFromAction never mapped
it — every listActions consumer (REST /actions, the console timeline) saw
none while the raw engine row carried the fileIds. Map it (and type it on the
ApprovalActionRow contract) so the audit trail actually exposes what was
attached. Regression test added: 137 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ypkQikZ55oWUHUnMebwXA
os-zhuang added a commit that referenced this pull request Jul 19, 2026
…p links (#3274)

objectui#2678 P1.5 backend: getRequest attaches decision_progress (unanimous/quorum got/need; per_group per-group breakdown from the open-time snapshot) for pending multi-approver requests; notify() centrally rewrites inbox actionUrls to /system/approvals?request=<id> deep links; rowFromAction now maps decision attachments through the listActions contract (browser-caught #3268 gap). 137 tests green.
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/m tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P1] approvals: quorum (M-of-N) + grouped per-group sign-off (会签) + decision attachments

2 participants