Skip to content

fix(plugin-approvals): an approval request is titled by its flow's label, never the raw record id or the flow's machine name - #24708

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-24665-approval-request-title
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-24665-approval-request-title

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24665

Clause-②: no

objectstack-ai/objectui#12484 remains blocked until the console pin bump.

What changes

The served approval request page (sys_approval_request_detail) authors no header, so the console titles it from the object's declaration over the row the approvals get serves: the nameField value, then the deprecated titleFormat only where that value is empty (ADR-0079). The pointer was display_title, a formula. A formula has no stored column and rowFromRequest serves none, so every page fell through to {process_name} · {record_id}: flow:NAME · RECORD_ID as its H1.

packages/plugins/plugin-approvals/src/sys-approval-request.object.ts:

  • nameField and displayNameField become process_label, the stored flow-label column. The approvals get already serves it (rowFromRequest, through the display-label chain: the column, else the config snapshot's __flowLabel, else the humanized machine name). So the page's H1 is the flow's label in every subject state, and the get serves the declared nameField value. No change to approval-service.ts and no new key on ApprovalRequestRow: the route needs no spec change.
  • titleFormat becomes {process_name}, without {record_id}. A renderer reaches it only where process_label is empty (a row written before the column, until the boot backfill labels it). The approvals get never reaches it.
  • display_title stays declared. Its expression drops the · record_id half: process_label, else process_name. That is the text the title ladder draws on the data door for every row the engine writes. Its help text and the three translated locales say so. en was regenerated with the package's documented os i18n extract command (via packages/cli/bin/run-dev.js), and --check reports 7 bundles in sync.

The subject record stays named by the Details tab's reference card (record_id / object_name). That card handles the readable, deleted and unresolvable states, which a title cannot.

Acceptance

  1. The approvals get serves the declared nameField value, and a pin holds it. New sys-approval-request-title.integration.test.ts uses the real ApprovalService on real ObjectQL over SqlDriver. The declaration's nameField is process_label, a text field with a real DDL column, and getRequest and listRequests serve the same non-empty value under it.
  2. The H1 names no raw record id and no flow: machine name, in each state, one pin per state. Readable (Rd4bl3Rec24665QQ, opened by the service), deleted (subject deleted, request cancelled / record_deleted) and unresolvable (object_name names an object the registry does not hold). In each, the served title equals the flow label, is non-blank, does not contain the record id and does not match flow:. A fourth case: a row with no stored label and no snapshot label is served the humanized machine name (Invoice Approval), so the template is never reached on this door.
  3. The search palette follows the same title. #24087 (PR fix(metadata-protocol): the search palette titles a hit by nameField before the deprecated titleFormat #24202) made searchAll's renderTitle read the nameField value first (packages/metadata-protocol/src/protocol.ts, titlePointer then titleFormatSource), pinned by protocol.search-title-namefield.test.ts ("THE PIN — an object declaring both keys is titled by its nameField value"). The palette's engine.find row carries the stored process_label, so a request is listed under its flow label. On an unlabelled pre-backfill row it draws {process_name}, the same fallback as the data door. No new palette pin: packages/metadata-protocol is outside this claim's file surface, and the order is already pinned there.

sys-approval-display-title.test.ts flips. The request now resolves to process_label. A labelled row's H1, display_title and resolveRecordTitle are all Discount Sign-off, with no · opp_45. An unlabelled row's pointer value is empty, and the template and display_title both draw flow:deal_desk (never null, never the id). resolveRecordTitle answers undefined there: the accessor reads the pointer and never invents a title.

The open choice inside A1, read on the four axes

display_title: retire or keep? The dispatch said to retire it if nothing but the title ladder reads it. Something else does read it. The activity mirror (plugin-audit audit-writers.ts) stores the title field's name as the provenance of each activity row's summary and record_label (text_sources, from titleFieldOf). The read side (activity-field-redaction.ts sourcesServed) serves those texts only while every named field is in the reader's served set. So retiring the field would withhold the summary and label of every sys_approval_request activity row written while it was the title. The 18.0 upgrade fixture's database.sql carries that text_sources shape on other objects' display_title. This is a code reading. Activity redaction was not run for this object.

  • Real business need: the stored provenance is a measured reader. A zero-consumer field would be retired; this one is not zero-consumer.
  • Long-term soundness: one stored column is the title on every door. The formula is kept only as a computed mirror of that same text, with no second source.
  • Keeping AI from writing it wrong: the kept formula now equals the title, so a field labelled "Title" no longer serves a different text (with a raw id) than the record's heading.
  • No scope creep at startup: retiring it would be a narrowing (BREAKING minor, an ADR-0087 marker, a contract review) whose cost falls on stored history. Keeping it adds nothing and declares nothing new.

Recommendation taken: keep it, with the reason in the field's comment. This PR therefore narrows nothing and carries Clause-②: no with a patch changeset (.changeset/24665-approval-request-title.md). That follows the newest plugin-approvals convention on main (.changeset/24375-approval-object-chip.md, 24501-list-object-column.md: a patch for a shipped object's display change, with no key added or removed).

titleFormat: drop or keep as {process_name}? Kept, at {process_name}. Dropping it would make the unlabelled-row fallback differ by consumer. objectui's derivation would land on a _title / _name affix field, which is the machine name again. searchAll's candidate list holds none of this object's fields, so it would fall to String(row.id), the request's own id. Keeping it gives every door the same machine-name fallback, which display_title also draws. The key is deprecated and lint-advisory platform-wide: 66 *.object.ts files in packages/ declare it. Retiring it is not this card's subject.

Verification

All on head 1b47ec0748, worktree off origin/main, merged with origin/main at 9e2722a92.

  • Closure build: pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-approvals^...' build, which reported VERDICT command-exit 0.
  • pnpm --filter @objectstack/plugin-approvals test: Test Files 85 passed (85), Tests 1146 passed (1146), which reported VERDICT command-exit 0.
  • pnpm --filter @objectstack/plugin-approvals typecheck: check:test-typecheck: OK, which reported VERDICT command-exit 0. The first run caught 3 TS2345 errors in the new test's helper typing, fixed in test(plugin-approvals): type the served-title helper over the row contract.
  • Ablations: one per changed behaviour, each through scripts/ablation-replace.mjs (anchor hit 1 to 0, blob changed) with a trap restore. Each restore was proven: the blob equals the HEAD blob and git diff HEAD is empty. The subject is imported relatively from src/, so no dist leg applies.
    • A, pointer back to display_title: 7 failed of 11. The served title is undefined (expected 'undefined' to be 'string'), which is the defect's own mechanism.
    • B, formula back to · record_id: 2 failed of 11.
    • C, template back to {process_name} · {record_id}: 3 failed of 11.
  • Gates: 67 families, derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 1b47ec0748. They ran locally, with each exit code captured before any pipe: 65 exited 0 and 2 exited 3.
    • The two exit-3 families are NOT MEASURED locally (PREREQUISITE NOT MET). pnpm check:dual-build-cjs-loads needs every package's dist/. pnpm check:i18n needs the built CLI; the package's documented extract command, run through run-dev.js with --check, reports 7 bundles in sync. CI runs both.
    • node scripts/check-plugin-teardown-shape.mjs --self-test first exited 3, because its pinned positive control was unreachable on the shallow clone. After fetching that commit it exited 0.
    • --ran reconciliation: 67 derived, 65 run, 2 NOT-MEASURED (derived from the recorded exit 3), 0 UNRUN.
    • Not run locally: the vitest repo project (test:repo). This diff touches none of its tests, and CI's Test Core runs it.

Acceptance notes

  • objectui's console fixtures (apps/console/src/services/approvalRequestsDataSource.servedPage*.test.tsx) copy the request definition with nameField: 'display_title'. They are copies, not readers, and they change with the console pin bump that approvals B1 closing card (ruling C on objectui#12438): the inbox opens a request on the served sys_approval_request page where the side sheet is, deletes the sheet, and lists each retired pin against its user report objectui#12484 waits on. Carrier: that card's dev.
  • sys_approval_approver's title is {approver} · {request_id} (role:finance · areq_…), which also prints a raw id. Observation only: the reach was not measured, and that index object is not on the request page. Carrier: none.
  • resolveRecordTitle(SysApprovalRequest, row) on a row with no stored label now answers undefined, where it answered flow:NAME · RECORD_ID before. Server-side consumers (the activity record_label, lookup titles) see no title for such a row until the boot backfill labels it, which it does for every request.
  • packages/metadata-protocol/src/protocol.search-title-namefield.test.ts:185 describes sys_approval_request's old declaration as the defect it pinned. That is historical and accurate, with its own inline fixture. Left as is (outside the file surface).

Generated by Claude Code

…bel, never the raw record id

WIP: title pointer moves to process_label (served by the approvals get),
titleFormat drops {record_id}, display_title drops its record-id half.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-approvals, touching 7 documentable anchor(s).

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/approvals.mdx (via sys_approval_request (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/automation/flows.mdx (via sys_approval_request (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/automation/hook-bodies.mdx (via nameField (symbol, a field of const object SysApprovalRequest))
  • content/docs/automation/workflows.mdx (via sys_approval_request (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/data-modeling/formulas.mdx (via nameField (symbol, a field of const object SysApprovalRequest), titleFormat (symbol, a field of const object SysApprovalRequest), display_title (literal, a string literal in displayNameField; a string literal in nameField))
  • content/docs/data-modeling/objects.mdx (via displayNameField (symbol, a field of const object SysApprovalRequest), nameField (symbol, a field of const object SysApprovalRequest), titleFormat (symbol, a field of const object SysApprovalRequest))
  • content/docs/data-modeling/schema-design.mdx (via displayNameField (symbol, a field of const object SysApprovalRequest), nameField (symbol, a field of const object SysApprovalRequest), titleFormat (symbol, a field of const object SysApprovalRequest))
  • content/docs/plugins/packages.mdx (via sys_approval_request (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/ui/forms.mdx (via nameField (symbol, a field of const object SysApprovalRequest))
  • content/docs/ui/translations.mdx (via sys_approval_request (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via nameField (symbol, a field of const object SysApprovalRequest), sys_approval_request (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v17/17-0.mdx (via nameField (symbol, a field of const object SysApprovalRequest), titleFormat (symbol, a field of const object SysApprovalRequest))
  • content/docs/releases/v17/17-2.mdx (via nameField (symbol, a field of const object SysApprovalRequest))
  • content/docs/releases/v17/17-6.mdx (via sys_approval_request (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

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 8e14ce057415992cb045fe712860f21cf29c3036 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0bd2399c1bdaa16bfe0ba4b1604618031c4e127a — the merge of head 1b47ec0748a12fbf8a8ad312e80f3178aef3f35c into base 8e14ce057415992cb045fe712860f21cf29c3036, 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 0bd2399c1bdaa16bfe0ba4b1604618031c4e127a && git checkout 0bd2399c1bdaa16bfe0ba4b1604618031c4e127a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8e14ce057415992cb045fe712860f21cf29c3036 1b47ec0748a12fbf8a8ad312e80f3178aef3f35c && git checkout -B drift-repro 8e14ce057415992cb045fe712860f21cf29c3036 && git merge --no-ff 1b47ec0748a12fbf8a8ad312e80f3178aef3f35c

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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 8e14ce057415992cb045fe712860f21cf29c3036 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:services seat 1 · 2026-10-20T01:01Z

os-justin · session_011TdZ8GjixNZFLz1xJD9zzK (seat post #6021). Reviewed against GitHub and origin/main 8e14ce0574, not against the report. Head under review:

1b47ec0748a12fbf8a8ad312e80f3178aef3f35c
  1. Shape. Draft, base main, first line Fixes #24665, line-start Clause-②: no in the body and the changeset. One closing keyword only. #24087 and approvals B1 closing card (ruling C on objectui#12438): the inbox opens a request on the served sys_approval_request page where the side sheet is, deletes the sheet, and lists each retired pin against its user report objectui#12484 stand with no keyword beside them. The changeset is @objectstack/plugin-approvals patch, and each sentence was read against the diff. It says the pointer moves to process_label, titleFormat becomes {process_name}, and display_title drops its record-id half. All three hold, and nothing is added or removed. check-governed-merges --pr 24708: NOT governed, 380 changed lines, at most 3000.
  2. Premises measured by the seat.
    • process_label is a stored Field.text (sys-approval-request.object.ts:233).
    • The boot fill labels every null row: display-label-backfill.ts:62 wires it on kernel:ready, :163 selects process_label null, :194 writes the chain's label. It is wired at approvals-plugin.ts:534.
    • The kept display_title has a real reader. The activity writer records the title field through titleFieldOf (audit-writers.ts:1449, :2088), and the read side serves that text only through sourcesServed (activity-field-redaction.ts:335, :572). Control in the same file: sys_activity, 24 hits.
    • So the keep branch of the order is the right branch, and no narrowing is declared.
  3. Gates. At ACCEPT, these check-runs on this head are success: Check Changeset, Build Core, Lint · repo & tooling gates, Lint · docs & code guards, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Lint · scoped · query-options erasure, Type Check · debt ledger, Dogfood Regression Gate, Check PR Size, Auto Label, and "No other open PR may claim the same issue". Test Core 1–6/6, Type Check · workspace / consumer gates / source gates, Lint · ESLint, Lint · CI, release & engine gates and Lint · scoped · slot lookup & corpus are in_progress. Expected skips are on the roster. Locally, 65 of 67 derived families exited 0. check:i18n and check:dual-build-cjs-loads are NOT MEASURED locally (prerequisite), so they are CI's to answer.
  4. Accepted deviations.
    • The claim surface is amended: the four translations/*.objects.generated.ts bundles are in. display_title is kept, but its help text follows its expression. en was regenerated with the package's documented extract command, and the three locales were re-translated in the bundle, as PR feat(plugin-approvals): engine-written display labels for the approval lists (step, flow, waiting on, record pointer, acted-as) #23984 did for its keys.
    • New sibling pin sys-approval-request-title.integration.test.ts: a real ApprovalService over SqlDriver, one case per subject state plus an unlabelled row.
    • Three ablations, each red and each restore proven by blob hash.
    • Model-free commit trailers per AGENTS.md.
  5. Out-of-scope dispositions.
  6. Landing clause. No contract review is owed: it declares no widening and no narrowing, touches no published schema and no governed text. Ready + auto-merge through the relay once every check on the current head is green or an expected skip. A push resets this reading.

Generated by Claude Code

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/m tests tooling

Projects

None yet

2 participants