…ltime): nine titleFormat-only system objects declare a title pointer instead of taking the raw id (objectstack-ai#20087)
Fixes objectstack-ai#20044
Clause-②: no
## What this changes
ADR-0079 resolves a record's title as `nameField`, then
`displayNameField`, then a derivation. An explicit `nameField` takes
precedence over the render-only `titleFormat`. The `titleFormat`
describe (`packages/spec/src/data/object.zod.ts`) states the migration:
「an explicit nameField now takes precedence … Migrate a single-field
title to nameField, a composite to a formula field designated as
nameField」.
Nine services-lane objects declared a `titleFormat` and no pointer. At
registration the registry's designate-only pass (`provisionPrimary(…, {
synthesize: false })`) derives the first title-eligible field, which on
all nine is `id`, and stamps it as `nameField`. A `/meta` read serves
that stamp as if the author had written it. objectstack-ai/objectui#9436
(landed as objectstack-ai/objectui#10358) makes the record page honour
the declared pointer, so each record page's H1 becomes the raw id once
the console pin moves past it. This is the same remedy PR objectstack-ai#20042 applied
to five objects (objectstack-ai#20015), repeated for these nine.
### Measured per object
Each declaration was registered in a real `ObjectQL` registry on
in-memory SQLite. One representative row was seeded and read back
through `findOne`, and its H1 was read under ADR-0079's order next to
the `titleFormat` rendering. The "main" columns were measured at
`b76aad5f6f`, with the declarations as they stand on `main`. The "this
branch" columns come from the committed pins.
| Object | `titleFormat` | main: registered `nameField` /
`displayNameField` | main: H1 | `titleFormat` rendering | this branch:
pointer | this branch: H1 (= `resolveRecordTitle`) |
|:---|:---|:---|:---|:---|:---|:---|
| `sys_approval_delegation` | `{delegator_id} → {delegate_id}` | `id` /
none | `RcqJgHufg-44Qdbj` (raw id) | `usr_alice → usr_bob` |
`display_title` | `usr_alice → usr_bob` |
| `sys_position_permission_set` | `{position_id} → {permission_set_id}`
| `id` / none | `2lvPKcxu8uDzPCzk` (raw id) | `pos_sales → ps_crm_edit`
| `display_title` | `pos_sales → ps_crm_edit` |
| `sys_user_permission_set` | `{user_id} → {permission_set_id}` | `id` /
none | `F06WPbxwwcDPtxYt` (raw id) | `usr_alice → ps_crm_edit` |
`display_title` | `usr_alice → ps_crm_edit` |
| `sys_user_position` | `{user_id} → {position}` | `id` / none |
`WeYJnXQkg-O3NKIh` (raw id) | `usr_alice → sales_manager` |
`display_title` | `usr_alice → sales_manager` |
| `sys_notification_delivery` | `{channel} → {recipient_id}` | `id` /
none | `MjrV1FGgJdUhhoj7` (raw id) | `email → usr_alice` |
`display_title` | `email → usr_alice` |
| `sys_notification_preference` | `{user_id} · {topic} · {channel}` |
`id` / none | `YuzjUl0Lsxsk4L9v` (raw id) | `usr_alice · billing.invoice
· email` | `display_title` | `usr_alice · billing.invoice · email` |
| `sys_notification_receipt` | `{state}` | `id` / none |
`3IWhzyEYVsd9JGeI` (raw id) | `read` | `state` | `read` |
| `sys_notification_subscription` | `{principal} · {topic}` | `id` /
none | `aNY2Diw2O2_qvHT8` (raw id) | `role:sales_manager ·
billing.invoice` | `display_title` | `role:sales_manager ·
billing.invoice` |
| `sys_presence` | `{user_id} ({status})` | `id` / none |
`zRKJBDnWzfMtp9Ps` (raw id) | `usr_alice (away)` | `display_title` |
`usr_alice (away)` (scratch run, see Deviations) |
### The fix
The eight composites each declare `display_title`, a formula field with
`returnType: 'text'` over the columns their `titleFormat` names.
`nameField` and the `displayNameField` mirror point at it, as in objectstack-ai#20042.
| Object | `display_title` expression |
|:---|:---|
| `sys_approval_delegation` | `record.delegator_id + ' → ' +
record.delegate_id` |
| `sys_position_permission_set` | `record.position_id + ' → ' +
record.permission_set_id` |
| `sys_user_permission_set` | `record.user_id + ' → ' +
record.permission_set_id` |
| `sys_user_position` | `record.user_id + ' → ' + record.position` |
| `sys_notification_delivery` | `record.channel + ' → ' +
record.recipient_id` |
| `sys_notification_preference` | `record.user_id + ' · ' + record.topic
+ ' · ' + record.channel` |
| `sys_notification_subscription` | `record.principal + ' · ' +
record.topic` |
| `sys_presence` | `record.user_id + ' (' + record.status + ')'` |
`sys_notification_receipt`'s title is the single column `{state}`, so
`nameField` and `displayNameField` name `state` directly. That is the
describe's migration for a single-field title. An explicit pointer is
honoured whatever the field's type (ADR-0079 D4, `resolveDisplayField`).
`select` is kept out of derivation only, which is why the pass skipped
`state` and stamped `id`.
- **No NULL part reaches a formula.** Every column the titles read is
`required: true`, so the formulas carry no null guard, like objectstack-ai#20042's
required-column formulas. objectstack-ai#20042's NULL-part legs covered nullable
columns, and none of these nine titles has one. The write path refuses
an omitted title column with `VALIDATION_FAILED`, naming the field with
code `required`. Where the column declares a default, the write fills it
instead: preference `topic` / `channel` become `'*'`, receipt `state`
becomes `'delivered'`, and presence `status` becomes `'online'`. The
engine pins cover both behaviours. A row written around the engine with
a NULL title column (raw SQL) makes the formula evaluate to `null`. This
was measured in a scratch run, where the `titleFormat` rendering of that
row would be `usr_alice → ` instead.
- **No stored column.** A formula is computed on read, and the synced
tables carry no `display_title` column. No search-companion column
appears either: a formula is never a companion source, and `select` is
not title text. The pin runs `provisionSearchCompanion` over the
registered body, the step a pinyin-enabled registry runs, and
`resolveSearchCompanionSources` answers `[]`. No migration runs.
- **`titleFormat` is unchanged** on all nine objects, for renderers that
still read it first.
- **`$search` scans the same fields.** A formula is never a search
target, and neither was `id`. On `sys_notification_receipt`, `state` (a
`select`) was already in the auto-default set and now leads it. The lead
changes the order only, never the members, and none of the nine declares
`searchableFields`.
## Security: the three permission-assignment tables
`sys_position_permission_set`, `sys_user_permission_set` and
`sys_user_position` bind permissions, so the new field was checked
against their existing read access:
- Each formula reads only its own row's columns: the foreign keys
(`position_id`, `permission_set_id`, `user_id`) and the `position` name.
It never reads a field of the record a key points at, so it never
traverses a lookup the reader may not see.
- None of those columns is `hidden`, guarded by `requiredPermissions` or
masked (`maskingRule`). The pin asserts all three per column, and each
was ablated on its own. They were already served to every reader of the
row, and they appear in `highlightFields` and `titleFormat`.
- No row scope, permission set, `apiMethods`, `managedBy` or
`userActions` entry changes. `display_title` is read-only. The shipped
permission sets grant these objects object-level access only
(`default-permission-sets.ts`) and carry no field entries.
## Tests
One new file per package. The three engine files boot the real
`ObjectQL` engine on in-memory SQLite with the real declarations:
- `plugin-approvals/src/sys-approval-delegation-display-title.test.ts`
(6 tests)
-
`plugin-security/src/objects/sys-security-assignment-display-title.test.ts`
(18 tests, 6 per object)
- `service-messaging/src/objects/notification-display-title.test.ts` (24
tests, 6 per object)
- `service-realtime/src/objects/sys-presence-display-title.test.ts` (2
tests; no engine, see Deviations)
Per object, the engine files assert:
- the body the registry holds after registration names the new pointer,
for both `nameField` and `displayNameField`;
- a seeded row's H1 is the literal `titleFormat` text, is not the row's
id, equals the `titleFormat` rendering of that row, and
`resolveRecordTitle` agrees;
- a row missing a title column is refused (`VALIDATION_FAILED`, field
named, code `required`), or is filled from the declared default and
titled by it;
- the formula reads exactly the `titleFormat` columns, one level deep,
each `required`, none `hidden` / permission-guarded / masked;
- no `display_title` column exists in the synced table, and no search
companion is provisioned.
Runs at `17db356e01`, the head of this PR:
| Suite | Result |
|:---|:---|
| `@objectstack/plugin-approvals`, full (`vitest run --maxWorkers=2`, at
`d040485b5b`) | 51 files, 790 tests passed |
| `@objectstack/plugin-security`, full (at `d040485b5b`) | 134 files,
2663 tests passed |
| `@objectstack/service-messaging`, full (at `d040485b5b`) | 46 files,
503 tests passed |
| `@objectstack/service-realtime`, full (at `d040485b5b`) | 5 files, 33
tests passed |
| the three rewritten engine files, at `17db356e01` | 6 + 18 + 24 passed
|
| `typecheck`, all four packages, at `17db356e01` | exit 0;
`check:test-typecheck` OK for plugin-approvals (ledger unchanged) and
plugin-security (0 errors) |
Only the three engine test files changed between `d040485b5b` and
`17db356e01`. `tsc --listFiles` finds every new test file inside a
typecheck program: plugin-approvals and plugin-security
`tsconfig.test.json`, service-messaging and service-realtime
`tsconfig.json`.
### Ablations (committed state `17db356e01`,
`scripts/ablation-replace.mjs`)
The object files are imported relatively from source, so no `dist/` sits
on the resolution path and no rebuild is involved. Every leg printed `ok
mutation landed` before its run and `ok restored: blob == HEAD` after
it. The tree equalled `HEAD` after each of the 33 legs.
| Leg | Mutation | Result |
|:---|:---|:---|
| ptr-id, 8 formula objects | both pointers → `'id'` (objectstack-ai#20015's shape) |
red on the pointer and H1 tests of that object, e.g. `expected
'22o5f2s01PYV_LAL' to be 'usr_alice → usr_bob'`, `expected
'Hc3c8fIEvdtL9EZZ' to be 'pos_sales → ps_crm_edit'`, `expected
'1-dAWCH9Y12B-1z4' to be 'email → usr_alice'`; presence: `expected 'id'
to be 'display_title'` from the designation pass |
| ptr-removed, receipt | both pointers deleted (`main`'s state) | red 3:
`expected 'id' to be 'state'`, `expected 'fKk2vvHoRePGTkDD' to be
'read'`, `expected 'nwVM_6ig0mkb28sJ' to be 'delivered'` |
| ptr-removed, delegation and presence | both pointers deleted | red 1
each, on the `displayNameField` mirror only (`expected undefined to be
'display_title'`); the H1 stays right, see Acceptance notes |
| req, 8 objects | one title column `required: true` → `false` | red on
the refusal test (`promise resolved … instead of rejecting`) and the
inputs test (`recipient_id: expected false to be true`); presence: the
inputs test |
| withheld, 6 legs | `hidden: true` on an input (delegation, delivery,
presence, `sys_position_permission_set`); `requiredPermissions` on
`sys_user_permission_set.user_id`; `maskingRule: 'name'` on
`sys_user_position.position` | red on the inputs test each time, e.g.
`user_id: expected [ 'view_assignment_subjects' ] to deeply equal []`,
`position: expected 'name' to be undefined` |
| col, 7 objects | `Field.formula(` → `Field.text(` | red 4 or 5,
including `to not include 'display_title'` and the companion pin |
| companion-receipt | `state: Field.select([…], {` → `state:
Field.text({` | red 1: `expected [ 'state' ] to deeply equal []` |
The first version of the companion pin read the synced table's columns
for `__search`. The engine the tests boot builds its registry with the
companion off (it follows `OS_SEARCH_PINYIN_ENABLED`), so that pin could
not fail, and the companion-receipt leg stayed green. It was replaced by
the `provisionSearchCompanion` form above, which goes red on that leg.
## Gates
`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` from the real diff derived 62 families. All 62 were run at
`17db356e01`, and `--ran` (with an exit code recorded per family)
reconciles: "62 derived famil(ies) accounted for — 62 run, 0
NOT-MEASURED (a DERIVED zero — all 62 recorded an exit code and none of
them is 3)".
- `check:registry-log-declared` first went red on an intermediate commit
whose tests constructed a `SchemaRegistry`. That requires an
`OS_REGISTRY_LOG` declaration in three `vitest.config.ts` files outside
this change. The pin was rewritten not to construct one, and the gate is
green at `17db356e01`.
- `check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET (six
unrelated packages had no `dist/`). After building them it passed: "104
published require entry point(s) across 67 package(s) load".
- `GITHUB_TOKEN="$GH_TOKEN" node scripts/check-issue-citations.mjs`:
exit 0.
- `check:i18n-coverage` (at `3fc935d944`, the bundle commit; nothing
after it touches a bundle or a declaration): OK, "13 config(s), 621
baselined untranslated string(s), none new".
- Narrowed eslint over the 41 changed `.ts` files (`--no-inline-config
--format json`): 41 files, 0 errors, 0 warnings, 0 ignored. The config
never enables type-aware linting (`eslint.config.mjs` around line 328:
no `parserOptions.project`), so a file's verdict depends on that file
alone. The repo-wide `pnpm lint` is CI's.
The derivation notes the tree is 7 commits behind `origin/main`. Two
gate inputs changed across that range:
`scripts/check-spec-docblock-symbol-anchors.mjs` and
`scripts/doc-authoring-prose-id.baseline.json`. None of those commits
touches a file in this diff.
## i18n
`node scripts/check-i18n-bundles.mjs --write --filter=…` regenerated the
four packages' bundles, and its output is committed unedited. The
English bundles gain `display_title`'s label and help. The zh-CN, ja-JP
and es-ES bundles carry the generator's English fill, and the
source-hash companions record those fills. A second `--write` is a
byte-for-byte fixed point. `check:i18n` and `check:i18n-stale-fill` are
green. objectstack-ai#20042 translated its new leaves by hand, but this dispatch said
the bundles are regenerated by tooling and never edited by hand. The
translated values can be hand-written in a later change, which AGENTS.md
allows.
## Deviations
- **`sys_presence`'s rendered-title pin is not committed.**
`@objectstack/service-realtime` declares neither `@objectstack/objectql`
nor `@objectstack/driver-sql`. An engine test there needs both as
devDependencies, which edits `package.json` and `pnpm-lock.yaml`,
outside the claimed file surface. The committed file pins the
designation pass (`provisionPrimary`, the step the registry runs) and
the formula's inputs. The rendered title was measured through the real
engine in a scratch run, not committed: `usr_alice (away)`, equal to the
`titleFormat` rendering.
- **The inherited commit (`3a1ea534a4`) was kept and amended by a
follow-up commit, not rewritten.** Its formulas were re-derived against
each `titleFormat` and all nine match. `sys_notification_receipt`'s
`titleFormat` is exactly `{state}`, so `state` is its one field. The
follow-up rewraps overlong comment lines and corrects "all required" for
two-column titles. It narrows the plugin-security note to what the
declared read path shows, and replaces a comment that promised the
presence test holds the rendered text.
- **`origin/main` was not merged.** It moved 7 commits, none touching a
file in this diff, so there is nothing to conflict.
## Acceptance notes
- Removing both pointers from a formula object does not bring the defect
back: `display_title` then wins derivation tier 2 (the `_title` affix).
Measured on `sys_approval_delegation` and `sys_presence`, where only the
`displayNameField` mirror went missing. The explicit pointer is kept
because the describe prescribes it and objectstack-ai#20042 declared it.
- A formula is a field of its own under field-level security. The masker
(`field-masker.ts` `maskResults`) deletes fields by name and knows no
formula inputs. A deployment that hides an input column through a
permission-set field entry therefore does not hide `display_title` with
it. That holds for every formula field, objectstack-ai#20042's five included. No
shipped declaration or permission set restricts these inputs. Noted, not
measured.
- Where an input is a lookup, the title carries the stored foreign key,
as the `titleFormat` substitution did (objectstack-ai#20042's `request_id` likewise).
- The `title-format-retired` lint warning stays on all nine objects,
because `titleFormat` stays (as in objectstack-ai#20042).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Fixes #9436
Clause-②: yes
This PR executes ruling C1 on this card: the ADR-0079 declared name pointer outranks
titleFormatin every record-title reader. The authority is class-1 ruling comment 5657441402, ratified in comment 5814246926. The scope is the re-scoped claim, comment 5820165439: one ruled order, three readers, one PR. LookupField is left out (see Acceptance notes).The new public export (why
Clause-②: yes)@object-ui/corenow exportsdeclaredNameField(objectDef). The function already existed privately inrecord-title.ts, and its behaviour is unchanged. It returns the object's declared record-title pointer exactly asgetRecordDisplayNamereads it at steps 1+2:nameField, then the deprecateddisplayNameFieldandNAME_FIELD_KEYaliases. It returnsundefinedwhen none is declared, and it never derives. Never deriving is the difference fromresolveNameField, and it is what lets a caller put the type-aware derivation on a lower rung than its owntitleFormatrendering. All three readers value it the same way,recordDisplayValueAt(record, declaredNameField(objectDef)). None of them re-types the??chain. The claim marks this exportClause-②: yes, so a review-tier contract review is owed before enqueue.Authority, as read
@objectstack/spec17.4.0 (installed),titleFormatdescribe: "[DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence."@object-ui/coregetRecordDisplayNamedocblock, step 3: "objectDef.titleFormat(rendered template) — LEGACY, render-only, kept for back-compat only; an explicitly-declared field (1/2) now wins over it."PageHeaderRenderertitle ladder, before and afterThe new rung is inserted directly above the template. Nothing else moves, and the template keeps the header's own interpolation (i18n option labels, separator cleanup).
main)schema.titleschema.titletitleFormat, via the header's interpolationnameField, thendisplayNameField, when it holds a value on the record. This is a new rung, filtered by the same floor test as rung 4.nameField,displayNameField, coreformatTitleTemplate, type-aware derivationtitleFormat, via the header's interpolation (unchanged)${objectLabel} ${id}placeholder${objectLabel} ${id}placeholder (unchanged)On an object without a
titleFormat, rung 2 answers exactly what rung 3 answered before. A pointer that is blank on the record falls through to the template, just asgetRecordDisplayNamewalks from a blank steps 1+2 to step 3.The other two readers of the same order
DetailView.resolveDisplayTitle:primaryField, then declared pointer (new step 1b), thentitleFormat(still step 2), then the unified resolver, thenschema.title, then the record-key probe, then the floor. The rung is numbered 1b so that existing citations of "step 2" as the template step stay true.record:detailsH1 dedupe: when the declared pointer holds a value, that field IS the H1. Its row is hidden and the template is not consulted. Otherwise the ruling needed(components/core): PageHeaderRenderer rankstitleFormatABOVE the declared name pointer — the opposite ofgetRecordDisplayName's documented ADR-0079 precedence #8351 template scan and the value walk run unchanged.Why all three move together was measured on the stop report (comment 5820114435). With only the header moved, the synthesized page (
page:header+record:details) showed a duplicate row under an identical H1 for a composite template. For a single-field template it also hid a row the H1 no longer showed.DetailViewrenderedHT-2026-001 - Acme CorporationovernameField: 'contract_no'.Upgrade effect: whose H1 moves
The H1 moves on any object that declares BOTH a
nameField(ordisplayNameField) and atitleFormatwhose rendering differs from that field's value.examples/**andapps/**: zero objects declaretitleFormatat all. In-repo declarations are test fixtures only.61609edfand not re-derived by any gate here. The instrument wasgit grep -lfor atitleFormatkey over non-test, non-md sources, then a per-file co-occurrence scan fornameField/displayNameField.sys_import_job,sys_job_queue,sys_job_run,sys_session,sys_setting,sys_migration_journal,sys_activity,sys_audit_log,sys_comment,sys_sharing_ruleandsys_webhook.nameField: 'id', so their H1 becomes the raw record id:sys_approval_action,sys_approval_approver,sys_approval_request,sys_automation_runandsys_http_delivery.This is a known consequence of the ruled order, and nothing in the renderer works around it. The fix belongs to those objects' metadata in ObjectStack: designate a formula field as
nameField, as thetitleFormatdescribe itself advises for a composite title. The seat is carding that in objectstack. The changesets for@object-ui/componentsand@object-ui/plugin-detailstate the same effect. The@object-ui/corechangeset names the export.Pins
packages/components/src/__tests__/page-header-title.test.tsx: the pin "titleFormat still outranks nameField (legacy header behaviour)" is rewritten, not deleted, as "the declared nameField outranks titleFormat (ADR-0079 protocol order, ruling needed(components/core): which rung wins the record title — PageHeaderRenderer rankstitleFormatabove the ADR-0079 declared pointer,getRecordDisplayNameranks it below (option C of objectui#8351) #9436)". The H1 must equal the pointer's value, and the template must render nowhere.displayNameFieldalias.In Progress, notin_progress, which also guards against consulting the whole resolver above the template); an explicitschema.titlestill wins.record-details.titleFormatNoDedupe-8351.test.tsx: the two old-order cases are rewritten, not deleted. "HALF 1" now asserts the pointer's row DROPS. "hides the row the DECLARED POINTER names, not the one the template names" is the other.titleFormatABOVE the declared name pointer — the opposite ofgetRecordDisplayName's documented ADR-0079 precedence #8351 template outcomes (composite hides nothing, empty walks on, collapse hides that row) now run on an object with no declared pointer, where the template really is the H1.record-details.headerDedupeAgreement-9436.test.tsx: the stop report's probe, committed. It renders the realpage:headerH1 beside the realrecord:detailsbody, for a composite and a single-field template, plus a no-pointer control.DetailView.declaredPointerOutranksTitleFormat-9436.test.tsx: two pins (nameField,displayNameField) and three controls: no pointer, a blank pointer, and the view-levelprimaryFieldstill winning.record-title.declaredNameField-9436.test.ts: the export's contract. It covers the alias order, never deriving (with a control showing thatresolveNameFieldwould derive), and agreement withgetRecordDisplayNameat steps 1+2.Ablations (one-shot, run at
96e1b800, the commit holding implementation and pins)Each ablation used
node ../objectstack/scripts/ablation-replace.mjs. That tool requires the anchor to hit exactly once, verifies the mutation on disk (anchor count 1 to 0, blob hash changed), then restores and proves the restore (blob equals HEAD,git diff HEADempty). Each run covered the same five files, 38 tests. Vitest aliases these packages tosrc, so nodistwas involved.declaredTitle ||becomes'' ||if (false)DetailViewback to template-firstif (false)DetailViewpinsLater commits added the changesets, removed one unused eslint directive from the agreement test (a comment line), and merged
main. None of them touches the mutated source lines.Verification at
d02362e7(final head)turbo run build --filter='@object-ui/plugin-detail^...': 11/11 successful. Thentype-checkof@object-ui/core,@object-ui/componentsand@object-ui/plugin-detail: exit 0. All five test files were confirmed inside thetsconfig.test.jsonprograms by--listFiles.pnpm exec vitest run packages/core/ packages/plugin-detail/gave 367 files passed and 1 skipped (a pre-existing timezoneskipIf), 5460 tests passed.pnpm exec vitest run packages/components/gave 289 passed and 1 skipped, 2809 tests passed.pnpm check:control-bytesOK.pnpm check:new-line-citations: 0 new citations.node scripts/check-changeset-presence.mjs: 9 source files of 3 released packages, 3 changesets.check-changeset-no-major,-fixed,-overwrite: OK.pnpm check:pending-changeset-literalsOK.pnpm check:test-path-rootsOK.pnpm check:changeset-claims: report-only. It flagged 10 pending changesets naming a touched file. Each paragraph was read, and none is falsified: they concern tab walkers, action ids, name-spaceresolveNameField,NAME_FIELD_KEYbeing read only insiderecord-title.ts(still true: the export lives there), andinterpolate()renderingtitleFormat(still true)..ts/.tsxfiles: 0 errors (warnings only, from rule classes already present in these files).ESLint.isPathIgnoredis false for 9 of 9.--format json: 9.eslint.config.jssets noparserOptions/projectService(no type-aware linting), and no rule undereslint-rules/reads other files. So this diff cannot move a verdict on an untouched file.pnpm check:readme-exports. Its prerequisite is every package'sdist: its only findings were missingdistentries and the vacuity floors those trip. This diff edits no README, and thecore/componentsREADMEs had zero findings. CI runs it on a full build.Hunk fences
record-details.tsx: this PR's hunks are the import block and the dedupe region, far from the@@ -139/@@ -197hunks of fix(plugin-detail): record:details stops reading requiredPermissions #10279.DetailView.tsx: this PR's hunks are the import line andresolveDisplayTitle, far from the@@ -934hunk of chore(deps): lucide-react 1.31.0 -> 1.43.0, with the one retired spelling repaired #8941.packages/fields/is untouched.Acceptance notes
recordToOptionhas the same defect class: the referenced object'stitleFormatoutranks its declared pointer in lookup option labels. Per the re-scope it is ⛔ not folded here.packages/fields/is held by [fields] Lookup 下拉候选列含 lookup 字段时,每条候选单独发一次请求(N+1) #10223, and the seat files it as its own card.titleFormatABOVE the declared name pointer — the opposite ofgetRecordDisplayName's documented ADR-0079 precedence #8351 dedupe scan, not moved by this PR. The scan only matches values against its candidate list: the name pointer, the derived field and six literal names. Take an object with NO declared pointer whose template collapses onto a field outside that list, for example{contract_no} - {name}withnameblank. Its H1 reads thecontract_novalue while thecontract_norow still prints under it. This PR reproduced it while retargeting "LIT CONTROL B"; the template-scan code that decides it is unchanged frommain. That control now collapses ontoname, a candidate. The gap is reported for the seat to card.The session for this change is
https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC.Generated by Claude Code