Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .changeset/17579-approver-type-manager-describe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
'@objectstack/spec': patch
---

`ApproverType` qualifies `manager` in its `.describe()` instead of offering it as a bare allowed value

`ApproverType` carried **no** `.describe()` at all, so the generated reference
page rendered `## ApproverType` with nothing but an `### Allowed Values` list:
`manager` — the one rung an author cannot operate on a stock install — read
exactly like the nine members that work. `{ type: 'manager' }` resolves
`sys_user.manager_id`, and that column still has no product write surface
(re-measured on this tree: the identity write guard's managed-update whitelist
for `sys_user` is `{name, image, locale}`; the column carries `readonly: true`;
no `packages/plugins/plugin-auth` source writes it). An author who chose it got
a chain that passed `validate` and `lint` and then stalled on its first
submission.

The new describe says what is true about `manager` and **points** at the remedy
rather than restating it: `MANAGER_ONLY_REMEDY` / `MANAGER_ONLY_ROUTES` in
`packages/lint/src/validate-approval-approvers.ts` remain the single
authoritative copy of the population routes, and that file's `DEPENDENCY`
docblock now names this new string among the lines that go stale if the column
ever gains a write surface. A pointer cannot drift into disagreement with what
it points at, which is why no third copy of the 667-character remedy was added.

⛔ No member is added, removed or renamed, and no behaviour changes: the enum's
accept set is byte-identical and `check:api-surface` is green on the rebuilt
`dist/*.d.ts`.

**Why this ships, and why `patch`.** `@objectstack/spec`'s published `files[]`
carries `dist`, `json-schema` and `src/**/*.zod.ts`, and the new string is
measured in all three on the built tree — `dist/automation/index.js` and
`.mjs` (2 files, against a lit control of an existing describe from the same
module, also 2), four `json-schema/` documents (`ApproverType.json`,
`ApprovalNodeApprover.json`, `ApprovalNodeConfig.json`, `objectstack.json`) and
the shipped `approval.zod.ts` source. Prose only, no surface widening ⇒
`patch`.

The `packages/lint` half is a docblock comment and is deliberately **not**
graded: that package publishes `dist` only, and the new sentence is absent from
it (0 files) while a runtime string from the same source file is present in 4
and a pre-existing comment from the same docblock is absent in 0 — so comments
are stripped by construction and nothing published moves there.
6 changes: 4 additions & 2 deletions content/docs/references/automation/approval.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const result = ApprovalDecision.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **type** | `Enum<'manager' \| 'position' \| 'department' \| 'team' \| 'field' \| 'expression' \| 'org_membership_level' \| 'role' \| 'user' \| 'queue'>` | ✅ | |
| **type** | `Enum<'manager' \| 'position' \| 'department' \| 'team' \| 'field' \| 'expression' \| 'org_membership_level' \| 'role' \| 'user' \| 'queue'>` | ✅ | Approval step approver type. `manager` is a directory-sync dependency rather than something an author configures here: it resolves the submitter's `sys_user.manager_id` at runtime, and that column has no product write surface, so until an operator populates it from outside the product a manager step resolves to nobody and the request waits. `os lint` reports that at authoring time as `approval-approvers-may-resolve-empty` and carries the graded population routes and the full remedy; the Approvals guide states the same remedy in prose. |
| **value** | `string` | optional | User id / membership tier / position / team / department / field — per `type`; for `expression`, a CEL expression over `current.*` / `trigger.*` / `vars.*` |
| **resolveAs** | `Enum<'user' \| 'department' \| 'position' \| 'team'>` | optional | How an `expression` result is expanded into approvers (default 'user') |
| **group** | `string` | optional | Group label for per_group sign-off (e.g. "legal", "finance") |
Expand Down Expand Up @@ -81,7 +81,7 @@ const result = ApprovalDecision.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **type** | `Enum<'manager' \| 'position' \| 'department' \| 'team' \| 'field' \| 'expression' \| …>` | ✅ | |
| **type** | `Enum<'manager' \| 'position' \| 'department' \| 'team' \| 'field' \| 'expression' \| …>` | ✅ | Approval step approver type. `manager` is a directory-sync dependency rather than something an author configures here: it resolves the submitter's `sys_user.manager_id` at runtime, and that column has no product write surface, so until an operator populates it from outside the product a manager step resolves to nobody and the request waits. `os lint` reports that at authoring time as `approval-approvers-may-resolve-empty` and carries the graded population routes and the full remedy; the Approvals guide states the same remedy in prose. |
| **value** | `string` | optional | User id / membership tier / position / team / department / field — per `type`; for `expression`, a CEL expression over `current.*` / `trigger.*` / `vars.*` |
| **resolveAs** | `Enum<'user' \| 'department' \| 'position' \| 'team'>` | optional | How an `expression` result is expanded into approvers (default 'user') |
| **group** | `string` | optional | Group label for per_group sign-off (e.g. "legal", "finance") |
Expand Down Expand Up @@ -112,6 +112,8 @@ const result = ApprovalDecision.parse(data);

## ApproverType

Approval step approver type. `manager` is a directory-sync dependency rather than something an author configures here: it resolves the submitter's `sys_user.manager_id` at runtime, and that column has no product write surface, so until an operator populates it from outside the product a manager step resolves to nobody and the request waits. `os lint` reports that at authoring time as `approval-approvers-may-resolve-empty` and carries the graded population routes and the full remedy; the Approvals guide states the same remedy in prose.

### Allowed Values

* `manager`
Expand Down
9 changes: 9 additions & 0 deletions packages/lint/src/validate-approval-approvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,15 @@ const GROUP_ROUTED_TYPES = new Set(['position', 'team', 'department']);
* a system-context write. Update them in the same change that opens the write
* surface — and re-take the three grades above, which are readings of this
* tree, not standing facts.
*
* Two other carriers assert the same fact and go stale with these strings, so
* the list is theirs too: the `manager` callout in
* `content/docs/automation/approvals.mdx`, and `ApproverType`'s `.describe()`
* in `packages/spec/src/automation/approval.zod.ts` (rendered verbatim into
* `content/docs/references/automation/approval.mdx`). Neither RESTATES the
* remedy — both point back here, which is why there is still exactly one copy
* to edit — but both assert that the column has no product write surface, and
* that is the sentence which stops being true.
*/
// ⛔ The tracker ids stay in the comments above and never in this string:
// `check:doc-authoring` Rule 3 — a runtime string reaches authors, operators and
Expand Down
28 changes: 27 additions & 1 deletion packages/spec/src/automation/approval.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,33 @@ export const ApproverType = z.enum([
// (see NON_AUTHORABLE_APPROVER_TYPES). Re-admit only together with a real
// ownership-queue implementation (queue entity + membership + claim).
'queue',
]);
])
// The generated reference page renders THIS string under `## ApproverType`;
// the JSDoc above the enum renders nowhere (`renderSchemaSection` prints
// `mainDef.description`, which only `.describe()` / `.meta({description})`
// fills). Until this string existed the page listed `manager` as a bare
// allowed value, so the one rung an author cannot operate on a stock install
// read exactly like the nine that work.
//
// ⛔ It POINTS at the remedy, it does not restate it. The remedy's single
// authoritative copy is `MANAGER_ONLY_REMEDY` / `MANAGER_ONLY_ROUTES` in
// `packages/lint/src/validate-approval-approvers.ts`, mirrored for readers by
// the Approvals guide; a third copy here would be one more line to keep in
// step, and a pointer cannot drift into disagreement with what it points at.
// That file's `⛔ DEPENDENCY` docblock now names this string among the lines
// that go stale if `manager_id` ever gains a product write surface.
//
// ⛔ Tracker ids stay out of the string itself — it reaches authors and
// generated surfaces, neither of whom can resolve one (`check:doc-authoring`
// Rule 3).
.describe(
'Approval step approver type. `manager` is a directory-sync dependency rather than something ' +
"an author configures here: it resolves the submitter's `sys_user.manager_id` at runtime, and " +
'that column has no product write surface, so until an operator populates it from outside the ' +
'product a manager step resolves to nobody and the request waits. `os lint` reports that at ' +
'authoring time as `approval-approvers-may-resolve-empty` and carries the graded population ' +
'routes and the full remedy; the Approvals guide states the same remedy in prose.',
);
export type ApproverType = z.input<typeof ApproverType>;

/**
Expand Down
Loading