Skip to content

Commit ee3ae03

Browse files
docs(approvals): an entry that resolves to nobody leaves a slot per its kind, and an empty or literal-only slate goes to onEmptyApprovers (#22657)
Fixes #22584 Clause-②: no The lifecycle step "The request opens, the run pauses" in `content/docs/automation/approvals.mdx` said: > An entry that resolves to nobody is not an error: the request opens with an empty `pending_approvers` and nothing can move it, so the run parks forever. The runtime does neither half of that. This PR replaces the sentence with a statement per approver kind, following the triage direction on the card (`6093078259`, confirmed on unlock by `6093154668`). Docs only, with no runtime change: the group types keeping their literal is the seat's ruling on #22558. ## The new text (`:388`–`:406` at `a35518bce`) What an entry that resolves to nobody leaves on the slate depends on its kind: - **A person entry** (`manager`, `field`) adds no slot. An `expression` that yields no ids adds none either. A `user` entry never resolves to nobody, because its value is the slot. - **A group entry** (`position`, `team`, `department`, `org_membership_level`) keeps its `type:value` literal as a slot. So do a `queue` entry and an `expression` value that `resolveAs` re-expands into a group with nobody in it. - A `position:NAME` slot is decided by whoever is staffed into that position later. - No person ever holds any other literal, so only an administrator can decide it. This bullet links to the admin-override callout under "Acting on requests in the console". - Beside entries that do resolve, a literal is a slot like any other, so a `behavior` that needs every slot (`unanimous`) waits on it too. - **A slate left empty, or holding only literals,** goes to the node's `onEmptyApprovers` policy. This bullet links to where the policy's four values are listed. Under the default, `admin_rescue`, the request opens on that slate and waits for an administrator, or for a holder of a `position:` literal on it. The page writes the placeholders as angle-bracket names inside code spans, as it already does elsewhere. This body writes them as `type:value` and `position:NAME` because GitHub's body sanitizer drops angle-bracket fragments. The rewrite links to the page's own `onEmptyApprovers` text and admin-override callout instead of repeating them. ## What the runtime does, read at `origin/main` `cc305a3cf` `cc305a3cf` is this branch's base, and `b98e984434` (the commit the direction names) is its ancestor: `git merge-base --is-ancestor b98e984 HEAD` exits 0. Every quote below is from `packages/plugins/plugin-approvals/src/approval-service.ts` unless another file is named. - **Person kinds add no slot.** - `:958` `const PERSON_APPROVER_TYPES = new Set(['manager', 'field']);` (the generic type annotation is left out here). - `:2071` `if (PERSON_APPROVER_TYPES.has(type)) {` … `return [];` - **`user` never falls back.** `:1995` `if (type === 'user') {` returns its value through OOO delegation. - **Every other kind falls through to its literal,** including `queue`: `:2117` ``return [`${type}:${a.value}`];`` - **`expression`.** An empty result normalizes to `raw = []` and adds no slot. A `resolveAs` value whose expansion is empty keeps a literal: `:2258` ``slots.push({ id: `${resolveAs}:${key}`, subGroup: key });`` - **The empty test counts literals as empty.** - `:3092` `if (!approvers.some(a => a && !a.includes(':'))) {` - `:3093` `const emptyPolicy = (input.config as any).onEmptyApprovers ?? 'admin_rescue';` - `fail` throws `NO_APPROVERS` (`:3094`). - `auto_approve` opens nothing (`:3101`). - `fallback` replaces the slate when the fallback resolves to a concrete person, and otherwise degrades to `admin_rescue` (`:3119`). - `admin_rescue` opens with `pending_approvers: approvers.join(','),` (`:3237`). - **Who can take a literal.** - `approver-address.ts:64` `const POSITION_ADDRESS_PREFIXES = ['position:'] as const;` - The acting addresses are the user id, the account's own email, and the `position:` address of a held position (`actingAddresses`, `:116`). - A `team:`, `department:`, `org_membership_level:` or `queue:` slot is held by no one. Only the admin override decides it. - **The tally.** `:3366` `return original.length > 0 && original.every(a => approved.has(a));` So a literal on a `unanimous` slate must be approved too, by a staffed holder or by the admin override. PM mechanism assumptions 1 and 2 held. The one drift is a line number: the admin-override callout was at `:667`–`:679` on the base, not `:651`–`:653`, because PR #22625 added lines above it. It is now at `:685`–`:697`. ## The other line changed on the page - **`:441`–`:442`.** "`position:finance_manager` — the form an entry falls back to when it resolves to no users" now reads "a group entry". Since `b98e984434` a person entry no longer falls back to a literal, so the rewrite above makes "an entry" false. No other line on the page is made false by the rewrite. I read these and left them alone: - The `manager` callout (`:100`–`:103`) and the notification callout (now `:480`–`:483`) describe the default policy correctly. - The admin-override callout is accurate. It names `position`, `team` and `department` and leaves out `org_membership_level`. The rewrite and the `role` callout (`:63`–`:67`) cover that type. ## Grep: no other hand-written page repeats "parks forever" or the empty-slate claim At `a35518bce`: ```text $ git grep -n -o -i "parks forever" content/docs/references/api/automation-api.mdx:157:parks forever content/docs/references/automation/control-flow.mdx:123:parks forever content/docs/references/automation/control-flow.mdx:186:parks forever content/docs/references/automation/flow.mdx:78:parks forever content/docs/references/automation/flow.mdx:149:parks forever packages/spec/src/automation/flow.zod.ts:831:parks forever ``` All six hits are one claim. It is the `waitEventConfig` describe at `packages/spec/src/automation/flow.zod.ts:831`, which `content/docs/references/**` generates five rows from. It is about a `wait` node declared without `waitEventConfig`, which is a different claim and true of a different node. The references tree is generated, so it is not hand-edited here. The second grep looks for the empty-slate claim itself across the hand-written trees, with the generated references excluded: ```text $ git grep -c -i -E "nothing can move it|empty .?pending_approvers|parks? (forever|indefinitely)" -- content/docs skills docs ":!content/docs/references/**" (no output, exit 1) control leg: the same grep at the base cc305a3 cc305a3:content/docs/automation/approvals.mdx:2 (exit 0) ``` Two nearby sentences are not this claim: - `content/docs/capabilities/approvals.mdx:20` says a manager-only step "resolves to nobody and the request waits". That is true under the default policy. A different problem on that page is in the Acceptance notes. - `skills/objectstack-automation/references/state-machines-and-approvals.md:351` describes `onEmptyApprovers` correctly. ## Verification (final HEAD `a35518bce`) - **Derived gates.** I ran `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` with no paths. It took the change set from merge base `cc305a3cf` (1 path) and derived 40 commands, the same 40 the dispatch named. - All 40 ran on `a35518bce`, and each exited 0. - The `--ran` reconciliation printed "40 derived, 40 run, 0 NOT-MEASURED, 0 UNRUN". - On the first pass, at `29420c9e6`, four gates exited 3 (PREREQUISITE NOT MET). `@objectstack/lint` and `@objectstack/client-react` were not built. After those named builds the four went green, and then all 40 re-ran on the final HEAD. - **`check:doc-anchors` reads the new in-page link.** - I used `scripts/ablation-replace.mjs` to change the anchor to `#acting-on-requests-in-the-consolex` (anchor hits 1 → 0, blob `b331cd3cea3f` → `54bbb25e11d3`), at committed HEAD `29420c9e6`. - The gate went red: `content/docs/automation/approvals.mdx:399 #acting-on-requests-in-the-consolex … renders no heading with id "acting-on-requests-in-the-consolex"`. - Restore: the blob equals HEAD's (`b331cd3cea3f`) and `git diff HEAD` is empty. - **`pnpm --filter @objectstack/spec exec vitest run --project repo --maxWorkers=2`** (the spec project that scans `content/docs`) exited 0 with 54 files / 915 tests passed. - It started on `29420c9e6`, and the `a35518bce` edit landed while it ran. That edit changes one phrase in one bullet ("listed at the end of" → "whose four values are listed under"). - It was not re-run on the final HEAD. This is a declared narrowing: the 40-gate union above did re-run there. - **Not run locally:** CI's `Build Docs` job and the `Test Core` shards, which are path-scheduled and run in CI's own shell, and the repo-wide lint. Those runs belong to CI. ## Acceptance notes - **`content/docs/capabilities/approvals.mdx:17`–`:18` is stale.** It says "no Console field, data-API write or auth admin endpoint sets that column" (`sys_user.manager_id`). But `POST /api/v1/auth/admin/set-user-manager` exists (`packages/platform-objects/src/identity/sys-user.object.ts:378`, the `set_user_manager` row action), and this PR's own page documents it. - This is outside this claim's file surface, and it is a different defect from this card's. It is reported to the seat, not fixed here. - **`:376` says `org_membership_level` "silently resolves to nobody".** The runtime now warns when a graph type expands to nobody, and `os lint` flags a position name authored there, so "silently" is soft. The rewrite does not make it false, so it is unchanged. This is an observation only. - **The `onEmptyApprovers` text sits inside "Approving across organizations",** although it is not about cross-org routing. The new link names that section. The text is not moved. --- _Generated by [Claude Code](https://claude.ai/code/session_01Q7Fy4uVkvBWgj9CLihdATJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b9f3cab commit ee3ae03

1 file changed

Lines changed: 23 additions & 5 deletions

File tree

‎content/docs/automation/approvals.mdx‎

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -383,9 +383,27 @@ a queue entry resolves to nobody. Route to a `team`, `department`, or
383383
Set Manager** — see [the `manager` callout above](#3-the-approval-node). `position`, `department`,
384384
`org_membership_level` and `expression` may additionally name which
385385
organization's directory resolves them — see
386-
[Approving across organizations](#cross-org-approvers). An entry that resolves
387-
to nobody is not an error: the request opens with an empty `pending_approvers`
388-
and nothing can move it, so the run parks forever.
386+
[Approving across organizations](#cross-org-approvers).
387+
388+
What an entry that resolves to nobody leaves on the slate depends on its kind:
389+
390+
- **A person entry** (`manager`, `field`) adds no slot. Nor does an
391+
`expression` that yields no ids. A `user` entry never resolves to nobody: its
392+
value is the slot.
393+
- **A group entry** (`position`, `team`, `department`, `org_membership_level`)
394+
keeps its `<type>:<value>` literal as a slot, and so do a `queue` entry and an
395+
`expression` value that `resolveAs` re-expands into a group with nobody in it.
396+
A `position:<name>` slot is decided by whoever is staffed into that position
397+
later. No person ever holds any other literal, so only an administrator can
398+
decide it (the admin-override callout under
399+
[Acting on requests in the console](#acting-on-requests-in-the-console)).
400+
Beside entries that do resolve, a literal is a slot like any other, so a
401+
`behavior` that needs every slot (`unanimous`) waits on it too.
402+
- **A slate left empty, or holding only literals,** goes to the node's
403+
`onEmptyApprovers` policy, whose four values are listed under
404+
[Approving across organizations](#cross-org-approvers). Under the default,
405+
`admin_rescue`, the request opens on that slate and waits for an
406+
administrator, or for a holder of a `position:` literal on it.
389407

390408
### The approver finds it in their queue
391409

@@ -420,8 +438,8 @@ curl -b cookies.txt \
420438
```
421439

422440
`approverId` accepts a user id, an email, or a `<type>:<value>` approver literal
423-
(`position:finance_manager` — the form an entry falls back to when it resolves
424-
to no users) — and takes several values (comma-separated or repeated) to cover
441+
(`position:finance_manager` — the form a group entry falls back to when it
442+
resolves to no users) — and takes several values (comma-separated or repeated) to cover
425443
a person's identities in one call. A position literal matches under
426444
`position:<name>`, the one spelling the service admits a holder of that
427445
position under as their own identity. The deprecated pre-rename prefix is no

0 commit comments

Comments
 (0)