From 0089ffa35d44c6e41dfa3798f55b6407d62c3954 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 04:09:51 +0000 Subject: [PATCH] docs(approvals): name the manager step rung 1 of the approval ladder needs On an install set up from the README alone, every contract sent for approval opened rung 1 on `manager:undefined` and stayed there, locked: rung 1 resolves the contract owner's `sys_user.manager_id`, and nothing in the operator setup asked anyone to set it. The README now has a "Rung 1 of the approval ladder is a manager" section (Setup -> Users -> Set Manager for every business requester, the admin endpoint for scripts, what an unset manager costs and how a platform admin recovers it, and that no rung sends its approver a message when it opens), and the `pnpm demo` closing note carries the step. F5 keeps `onEmptyApprovers: 'admin_rescue'`: a `fallback` would need a fallbackApprovers list naming who takes an unstaffed rung, which DESIGN.md does not decide. The manager_review comment said the engine reads the submitter's manager; it reads the owner's, as the header says. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HihZ11bQSqjCgjzHbpv4M1 --- README.md | 43 ++++++++++++++++++++++++++++- scripts/demo.mjs | 10 +++++-- src/flows/contract-approval.flow.ts | 34 +++++++++++++++-------- 3 files changed, 72 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index f90ea10..d351465 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,8 @@ thirty days and the arrears lists are always in arrears. A seed cannot create a user, so `pnpm demo` produces exactly one account. To see the permission model of [DESIGN.md](./DESIGN.md) §04 do anything, create -accounts in **Setup → Users** and assign the seven positions: +accounts in **Setup → Users**, assign the seven positions, and give every business +requester a manager ([below](#rung-1-of-the-approval-ladder-is-a-manager)): | Position | How many | What it changes | |---|---|---| @@ -132,6 +133,35 @@ admin: it is `required: true`, so a name that resolves to nothing takes the row with it, and the dev admin is the only account that exists while the seed runs. Reassign it once the real accounts are there. +### Rung 1 of the approval ladder is a manager + +Every contract sent for approval opens on rung 1: the direct manager of the +contract's **owner** — the business requester who launched it, not the lawyer +who pressed **Send for Approval** — read from that account's +`sys_user.manager_id`. No seed, position or permission set fills that column, +so on an install set up from the tables above alone, no contract gets past +rung 1. + +Set it in **Setup → Users → the user → ⋯ → Set Manager**, for each of the three +business requesters and for anyone else who launches contracts. Any other +account in the organization will do — the persona mix has no line managers of +its own, so point all three at one of the accounts above (the executive, say). +The manager needs nothing more: `approve_contract`, the gate a rung acts +through, is in `clm_requester`, which every account holds. + +Skip it and the request opens on a slot nobody holds — `pending_approvers` +reads `manager:undefined` (upstream `objectstack-ai/objectstack#22558`) — and +the contract stays locked for approval. Setting the manager afterwards does not +reach a request that is already open: its approvers were resolved when the rung +opened. Only a platform admin moves it on, from **Setup → Approvals → Approvals +Inbox → All**, with **Override Reassign** (or Override Approve / Reject). + +No rung sends its approver a message when it opens — nothing in their inbox, +no email. They see it waiting in the Console: the bell counts the approvals +waiting on them and **我的合同 › Waiting on Me** lists them. A reminder sent on +a request does arrive as a message; the missing one on opening is the +platform's, not this app's. + ### Assigning a position from a script Clicking through Setup → Users needs nothing more than the table above. Automating it does: the @@ -169,6 +199,17 @@ empty lists, with nothing in the response that created it pointing at why. The s the platform's — `objectstack-ai/objectstack#16712`, still open — so until that lands, the spelling is the whole defence. +A manager is an admin operation, not a data write. **Set Manager** posts exactly this, as a platform +admin (the dev admin is one): + +```http +POST /api/v1/auth/admin/set-user-manager +{ "userId": "…", "managerId": "…" } +``` + +The data API refuses the column outright: a `PATCH /api/v1/data/sys_user/…` carrying `manager_id` +answers `403 PERMISSION_DENIED`, because it edits only `name`, `image` and `locale` on a user. + ### What the seeded rows do NOT carry, and why The demo is loaded as **system data**, not driven through the flows. Two diff --git a/scripts/demo.mjs b/scripts/demo.mjs index a9e3fac..0fa7de9 100644 --- a/scripts/demo.mjs +++ b/scripts/demo.mjs @@ -406,7 +406,7 @@ const expectedPort = (argv) => { const OPERATOR_SETUP_NOTE = [ '', ' ────────────────────────────────────────────────────────────────────────', - ' Before you open the app — one setup step, and one thing about the log', + ' Before you open the app — two setup steps, and one thing about the log', ' ────────────────────────────────────────────────────────────────────────', '', ' 1. The seeded rows have no people on them yet. Contracts are launched by', @@ -418,7 +418,13 @@ const OPERATOR_SETUP_NOTE = [ ' them in Setup → Users and run this again; the README names them and', ' says who gets what.', '', - ' 2. The `ERROR [SeedLoader]` lines are that same missing owner: one per', + ' 2. Then give each business requester a manager: Setup → Users → the', + ' user → ⋯ → Set Manager (any other account will do). Rung 1 of the', + ' approval ladder is the contract owner\'s manager; with none set, a', + ' contract sent for approval waits on nobody, locked, until a platform', + ' admin overrides it. The README\'s "Rung 1" section says more.', + '', + ' 3. The `ERROR [SeedLoader]` lines are that same missing owner: one per', ' contract whose requester account does not exist yet, expected on a', ' first boot, and nothing is lost to them. The loader defers `owner_id`,', ' finds no such account on its last pass and writes the row anyway with', diff --git a/src/flows/contract-approval.flow.ts b/src/flows/contract-approval.flow.ts index 482cf42..5610ae1 100644 --- a/src/flows/contract-approval.flow.ts +++ b/src/flows/contract-approval.flow.ts @@ -179,16 +179,23 @@ const RUNG_POLICY = { // RECORD_LOCKED even for the platform admin. The recovery is real — a // platform admin's `POST /api/v1/approvals/requests//approve` answered // 200, resumed the run and carried the contract to `approved`, while the - // same call from a non-approver non-admin answered 403 FORBIDDEN — but it - // is API-ONLY in Console 17.3.0: neither the inbox's All tab nor the - // request record page renders an approve/reject/reassign control for the - // override actor (#3424). That residual is a platform gap, recorded in the - // PR rather than papered over here: the lint's suggested fallback - // (`{ type: 'org_membership_level', value: 'owner' }`) would seat the org - // owner on the legal, finance, executive and GM rungs as a silent - // co-approver, and DESIGN.md §03 fixes the ladder at five NAMED rungs with - // the matrix deciding only which are climbed (§13 Q3 puts an extra approver - // in the customer overlay, not the standard product). + // same call from a non-approver non-admin answered 403 FORBIDDEN. On 17.3.0 + // that override was API-only in the Console (#3424); re-measured on 17.7.0 + // (#93), the platform admin's Setup → Approvals → Approvals Inbox → All tab + // offers Override Approve / Reject / Reassign / Recall on the request. + // + // The lint's suggested fallbacks stay unauthored. An extra + // `{ type: 'org_membership_level', value: 'owner' }` approver would seat the + // org owner on every rung as a silent co-approver; `'fallback'` needs a + // `fallbackApprovers` list naming who takes an unstaffed rung, a choice + // DESIGN.md does not make. §03 fixes the ladder at five NAMED rungs with the + // matrix deciding only which are climbed (§13 Q3 puts an extra approver in + // the customer overlay, not the standard product). + // + // The rung an install hits FIRST is the manager rung: its slate is + // `sys_user.manager_id`, runtime data no seed, position or permission set + // fills. Its remedy is operator setup, named in the README ("Rung 1 of the + // approval ladder is a manager"), not a policy here. onEmptyApprovers: 'admin_rescue' as const, lockRecord: true, approvalStatusField: 'approval_status', @@ -264,8 +271,11 @@ export const ContractApprovalFlow: Flow = { type: 'approval', label: 'Direct Manager', config: { - // No `value`: the engine resolves the submitter's `sys_user.manager_id` - // (`APPROVER_VALUE_BINDINGS.manager` is `{ source: 'auto' }`). + // No `value`: the engine resolves the contract OWNER's + // `sys_user.manager_id` (`record[value] ?? record.owner_id` — see the + // header), never the submitter's. Only an operator fills that column, + // which is why the README's operator setup names the step; re-measured + // on 17.7.0 (#93): unset, the request opens on `manager:undefined`. approvers: [{ type: 'manager' }], behavior: 'first_response', ...RUNG_POLICY,