Skip to content

fix(plugin-approvals): an approval request record title reads the flow label (process_label), falling back to process_name - #24085

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24007-approval-request-title-label
Oct 17, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24007-approval-request-title-label

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 fix(plugin-approvals): name the record by object label and id in every approval notification body #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

display_title now reads process_label + ' · ' + record_id, falling back to
process_name when no label is stored, so the record page heading, its
breadcrumb and the action history request cell show the flow's label
instead of flow:NAME. titleFormat keeps {process_name}: a template cannot
fall back, and process_label can be null after the boot fill.

Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Oct 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 8a83cebc15a5efedff290e2c208abfba08fca9d6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e03394bb58815105f445bbbab197c414cae6b83e — the merge of head ff2cf67c91eda33273d5f33f4d60ae4c01015728 into base 8a83cebc15a5efedff290e2c208abfba08fca9d6, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e03394bb58815105f445bbbab197c414cae6b83e && git checkout e03394bb58815105f445bbbab197c414cae6b83e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8a83cebc15a5efedff290e2c208abfba08fca9d6 ff2cf67c91eda33273d5f33f4d60ae4c01015728 && git checkout -B drift-repro 8a83cebc15a5efedff290e2c208abfba08fca9d6 && git merge --no-ff ff2cf67c91eda33273d5f33f4d60ae4c01015728

node scripts/docs-audit/affected-docs.mjs --json 8a83cebc15a5efedff290e2c208abfba08fca9d6

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:services seat 2 · session_018VqJPGA1NDNSCZsnecsChP · 2026-10-17T00:38Z

Reviewed against GitHub at head ff2cf67c91, from the diff and the dev's report on #24007. Clause-②: no: one formula on a platform object's computed title changes, and no export, type, accepted value or stored column moves. So this is the seat's own review of record.

Form

  • Draft, base main. The first line is Fixes #24007, then Clause-②: no.
  • 3 files, +60 / −10, plus one patch changeset for @objectstack/plugin-approvals.
  • check-governed-merges.mjs --pr 24085: NOT governed.

The change. display_title, the request's nameField, reads record.process_label != null ? process_label + ' · ' + record_id : process_name + ' · ' + record_id. A labelled request is titled by its flow's label (Discount Sign-off · opp_45). A row with no label falls back to process_name, never to null.

titleFormat keeps {process_name}. The dev MEASURED process_label as nullable after the boot fill: a failed fill run leaves rows null, and a flow whose name humanizes to nothing stays null on every boot. A placeholder template cannot fall back, so this is the card's Acceptance 2 decided on evidence. Both comments say so, and name the titleFormat-first reader.

Pins. sys-approval-display-title.test.ts keeps formula and template identical where no label is stored, and pins where they differ. The ablations, each with its restore proven by blob:

  • deleting the label branch reds the authored-label pin;
  • dropping the null guard reds the null-fallback pin. The unguarded title is null: CEL refuses null + string.

Filed by the seat from this report: #24087. The global search palette's searchAll renders titleFormat before nameField, the reverse of ADR-0079 and the spec's own describe. That is why this request's palette title still shows flow:….

Gates and CI

  • Gates: 65 derived, 65 exit 0, reconciled with --ran.
  • The package passes 81 files and 1,105 tests. Typecheck exits 0.
  • CI at ff2cf67c91: 40 check-runs, 34 success, 0 failure, 6 skipped (the roster's six). All required contexts are green.

ACCEPT. This act marks the PR ready and enables auto-merge through the fleet relay. It lands through the merge queue.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 17, 2026 00:39
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 17, 2026 00:39
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 17, 2026
Merged via the queue into main with commit b75129f Oct 17, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24007-approval-request-title-label branch October 17, 2026 01:01
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/s tests tooling

Projects

None yet

2 participants