Repository navigation
Commit b75129f
fix(plugin-approvals): an approval request record title reads the flow label (process_label), falling back to process_name (#24085)
Fixes #24007
Clause-②: no
The request's computed record title reads the flow's label in place of
`flow:NAME`. No export, type, accept set or served column moves.
## What changed
- **`sys_approval_request.display_title`**, the object's `nameField`, is
now `record.process_label != null ? record.process_label + ' · ' +
record.record_id : record.process_name + ' · ' + record.record_id`.
- A request opened by a flow labelled "Discount Sign-off" is titled
`Discount Sign-off · opp_45`.
- A row with no stored label keeps `flow:deal_desk · opp_42`, never
`null · …`.
- The guard is the idiom other platform objects already use
(`sys-approval-action.object.ts`, `sys-automation-run.object.ts`,
`sys-member.object.ts`).
- **The field comment** that said no null guard is needed now says why
one is.
- **`titleFormat` keeps `'{process_name} · {record_id}'`** (see below).
The comment above `nameField` says why, and names the renderer that
reads it first.
- **`sys-approval-display-title.test.ts`**: the request case is now two
pins, and the pairing is restated (below).
- **One `patch` changeset** for `@objectstack/plugin-approvals`.
No field description changed, so the translation bundles are not
regenerated, and Acceptance 4 does not apply. `check:i18n` reading: see
Gates.
## Premise, measured first (Acceptance 2's question)
On `origin/main` `b6805ac0f6`:
- `display_title` read `record.process_name + ' · ' + record.record_id`.
- `titleFormat` was `'{process_name} · {record_id}'`.
- `process_label` is `required: false`.
Can `process_label` still be null after the open write and the boot
fill? **Yes.** Measured on a real `ObjectQL` over `SqlDriver`
(better-sqlite3, in memory), with a throwaway probe that was not
committed:
- **A failed fill run leaves every unfilled row null.**
`backfillApprovalDisplayLabels` throws on a failed read. In the probe, a
`find` that refuses gives `threw: "datasource refused"`, and the row
stays `process_label: null`.
- `approvals-plugin.ts` logs that at `warn` and retries on the next
boot. Its own message says the cells "stay blank until a later boot
fills them".
- The fill also returns without running when the approval service is
absent (`if (!svc) return`).
- It runs on `kernel:ready`, so rows are readable before it finishes.
- **A row the chain cannot label stays null on every boot.** Take a
request whose `process_name` humanizes to nothing (`flow:`) and whose
config snapshot has no `__flowLabel`. The engine accepts that row:
`required` refuses only `''`.
- The first fill wrote 1 of the 2 seeded rows (`requestsFilled: 1`). The
`flow:` row kept `process_label: null`.
- A second fill wrote 0 rows.
- **The open write** stores `labelOrMachineName(flowLabel, processName)
?? null`. That is null only when `flow:` plus `flowName ?? nodeId`
humanizes to nothing: `labelOrMachineName(undefined, 'flow:')` is
`undefined`.
So `'{process_label} · {record_id}'` is not safe, and `titleFormat`
keeps `{process_name}`.
## Which renderers read `titleFormat` first
- **The global search palette: `searchAll` in
`packages/metadata-protocol/src/protocol.ts`.** Its `renderTitle` tries
`titleFormat` before `nameField`.
- `sys_approval_request` is in its sweep. `canServeApiOperation(enable,
'search')` is true, and the auto-default search fields include
`process_name` and `record_id`.
- Measured through `searchAll` on this branch at `1afade209a`: a row
labelled "Discount Sign-off" gets the palette title `flow:deal_desk ·
opp_45`. Its `nameField` value is `Discount Sign-off · opp_45`.
- Measured on `main` with the template swapped to `{process_label}`: a
row with no label gets the palette title `· opp_43`, with no source at
all.
- **objectui's title readers rank `nameField` above `titleFormat`**
(ADR-0079 order), read at objectui `daeb77f`. That covers
`@object-ui/core` `getRecordDisplayName`, the record page H1 in
`containers.tsx`, and `LookupField`.
- So the record page's H1 and breadcrumb, and the Action History list's
`request_id` cell, read `display_title`.
## The pairing, restated
`sys-approval-display-title.test.ts` held the formula and `titleFormat`
to the same text. They now name different columns, so the file asserts:
- **With no label stored,** both draw `{process_name} · {record_id}`.
This is the old assertion, kept, plus a check that `process_label` is
null on the row.
- **With a label stored,** the formula draws `{process_label} ·
{record_id}`, and `resolveRecordTitle` agrees. `titleFormat` draws
`{process_name} · {record_id}`.
## Tests
All runs went through `scripts/pm/os-verify-lock.sh`. Each verdict below
is the lock's own `VERDICT command-exit` line.
- **The pin file at `1afade209a`:** `pnpm --filter
@objectstack/plugin-approvals exec vitest run --maxWorkers=2
src/sys-approval-display-title.test.ts` gives `Tests 6 passed (6)`, exit
0.
- **The whole package at the final head `ff2cf67c91`**, after merging
`origin/main` `8133cc7805`, which brought #24064's `approval-service.ts`
change into this package:
- `vitest run --maxWorkers=2` gives `Test Files 81 passed (81)` and
`Tests 1105 passed (1105)`.
- Then `pnpm --filter @objectstack/plugin-approvals run typecheck` runs
`tsc --noEmit` on both configs, then `check:test-typecheck: OK`, with 27
pinned signatures held.
- Exit 0 for the whole chain.
- **The same package run before the merge, at `1afade209a`:** 81 files
and 1104 tests passed. Typecheck exit 0.
## Ablation (one-shot, nothing committed)
The test subject is imported by relative path
(`./sys-approval-request.object.js`), so vitest reads `src/`. No `dist/`
is involved, and no rebuild or dist preflight applies. Each leg went
through `scripts/ablation-replace.mjs` in wrap mode, with a shell `trap`
restore on top. Every restore was proven: the blob equals the HEAD blob
`8ac394bf6ba2`, and `git diff HEAD` is empty.
- **Leg A: drop the label branch.** The formula goes back to
`record.process_name + ' · ' + record.record_id`. Anchor 1 → 0, blob
`8ac394bf6ba2` → `69cfd16dcea2`.
- The authored-label pin goes red: `expected 'flow:deal_desk · opp_45'
to be 'Discount Sign-off · opp_45'`, 1 failed and 5 passed.
- **Leg B: drop the null guard** (`record.process_label != null ? `
becomes `true ? `). Anchor 1 → 0, replacement 0 → 1, blob →
`a5aee4f1c4ed`.
- The null-fallback pin goes red: `expected null to be 'flow:deal_desk ·
opp_42'`, 1 failed and 5 passed.
- The direction differs from the card's "not `null · …`" framing. CEL
refuses `null + string`, so the unguarded formula yields a null title,
not the text `null · …`. Either way the pin catches it.
- **A first leg-A attempt was a no-op.** Its replacement text already
occurred inside the anchor, so `ablation-replace` refused it
(replacement count 1 → 1) and restored, with no test run. It was redone
with `--delete` on the label branch.
## Gates
**Local set.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at the final head `ff2cf67c91` derives 65
commands. All 65 were run there, each exit captured before any pipe, and
all 65 exited 0.
**Reconciliation.** `--ran` (each line records `:: exit N`) reports `65
derived, 65 run, 0 NOT-MEASURED, 0 UNRUN`, and the zero is derived from
the recorded codes.
**Verdict lines worth naming:**
- `check-i18n-bundles: OK (10 package(s) — all bundles in sync …)`, with
`plugins/plugin-approvals in sync (7 bundle(s))`.
- `check-adr-0087-registration: this PR adds no declared-breaking
changeset`.
- `No empty-frontmatter changeset introduced by this diff`.
- `check-nul-bytes: OK`.
- `check:type-check-debt … none above its recorded number`.
- `check:cross-package-test-inputs OK`.
- `check-test-source-alias OK`.
**A first union at `1afade209a` had two exit-3 results.** `check:i18n`
and `check:dual-build-cjs-loads` reported `PREREQUISITE NOT MET`,
meaning nothing was measured. Both were measured green in the final
union, after the prerequisite build.
**Not run locally (CI's runs):** `test:repo`, the type-check lanes, the
path-scheduled CI jobs and the roster families the derivation names. No
exported symbol was renamed or removed, so `check-platform-checklist`
was not owed.
`origin/main` moved to `8a83cebc15` after the merge. That is one
TSDoc-only commit, in `packages/spec/src/contracts/http-server.ts`,
touching nothing this PR reads, so it was not merged in.
## Acceptance notes
- **The palette still shows the machine source.** The global search
palette ranks `titleFormat` above `nameField`, which the spec's own
`titleFormat` description contradicts ("an explicit nameField now takes
precedence", `packages/spec/src/data/object.zod.ts`). So on this branch
the palette still titles a labelled request `flow:deal_desk · opp_45`.
That is the renderer named above, and the cost of Acceptance 2's second
arm. It is reported to the seat as a finding, not fixed here:
`metadata-protocol` is outside this card's surface.
- **An underscore-only flow name stores `''` as `process_label`.**
`prettifyMachineName` returns `''`, not `undefined`, for such a name
(`flow:_`); `FlowSchema.name`'s pattern admits `_`. With an empty
authored label, the open write stores `''`, which the new `!= null`
guard reads as a label. Measured on the chain function only
(`labelOrMachineName('', 'flow:_')` is `''`). No real producer names a
flow that way. Carrier: none.
- **The changeset says "action history list".** The Action History
list's `request_id` cell draws this title through the lookup's target
`nameField`. That was read from the declaration, not measured on screen.
---
_Generated by [Claude
Code](https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 051aea6 commit b75129f
3 files changed
Lines changed: 60 additions & 10 deletions
File tree
- .changeset
- packages/plugins/plugin-approvals/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
25 | 33 | | |
26 | 34 | | |
27 | 35 | | |
| |||
90 | 98 | | |
91 | 99 | | |
92 | 100 | | |
93 | | - | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
94 | 121 | | |
95 | 122 | | |
96 | 123 | | |
| |||
99 | 126 | | |
100 | 127 | | |
101 | 128 | | |
| 129 | + | |
102 | 130 | | |
103 | 131 | | |
104 | 132 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
53 | | - | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
54 | 61 | | |
55 | 62 | | |
56 | 63 | | |
| |||
128 | 135 | | |
129 | 136 | | |
130 | 137 | | |
131 | | - | |
132 | | - | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
133 | 143 | | |
134 | 144 | | |
135 | 145 | | |
136 | | - | |
| 146 | + | |
137 | 147 | | |
138 | 148 | | |
139 | 149 | | |
| |||
0 commit comments