Repository navigation
ADR-0079 title: 19 objects that declare titleFormat and NO pointer get nameField: id stamped by the registry designation pass, so a renderer honouring the declared pointer (objectui#9436) shows the raw record id as the title #20044
Description
Activity
objectstack-fleet commented
on Sep 25, 2026 ContributorAuthorMore actions分诊首次定级:
priority:p2·bug·domain:services·pm:queue—— 19 个只写了titleFormat、没有名称指针的系统对象,被注册表的指定步骤盖上nameField: id;objectui 按「声明的指针优先」渲染后,记录页标题会显示原始记录 ID。按修复落点拆开:本卡管服务组的 9 个,引擎组的 10 个(platform-objects)另开 #20059Path:
packages/plugins/plugin-approvals/src/sys-approval-delegation.object.ts·packages/plugins/plugin-security/src/objects/(sys-position-permission-set、sys-user-permission-set、sys-user-position)·packages/services/service-messaging/src/objects/(notification-delivery、notification-preference、notification-receipt、notification-subscription)·packages/services/service-realtime/src/objects/sys-presence.object.tsTriage: lands in
plugin-approvals/plugin-security/service-messaging/service-realtime⇒domain:services,bug,priority:p2,pm:queue; rationale: each of these nine objects declares atitleFormatand no pointer, the designate-only pass at the registry seam derivesidas the first title-eligible field and stampsnameField: 'id', and a/metaread serves it as if declared (measured by the #20015 dev) — with objectui#9436 (ruling C1) now landed on objectuimainas PR objectui#10358, the record-page H1 becomes the raw id once the console pin moves past it; the same regression #20015 fixed for five objects (PR #20042, landed), hence the same p2; the tenplatform-objectsobjects land indomain:engineand are carried by #20059, filed by this seat in the same act; the class-level alternative (reading 2) changes what every served body presents and goes to the maintainer as a decision — neither card waits on it.分诊席 #6015,2026-09-25T00:26Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并在 objectstack
origin/main2274894cc4上核对。本席核对
- 19 个对象逐个确认:定义文件里都有
titleFormat,都没有nameField。服务组 9 个见上面的 Path;引擎组 10 个都在packages/platform-objects/src/identity/。 - 指定步骤:
packages/spec/src/data/display-name.ts的provisionPrimary在指定模式(synthesize: false)下,只要能推导出标题字段就写进nameField。注册表在packages/objectql/src/registry.ts调用它。同文件的注释写明,经/meta读出的对象体「已经被指定过」,作者是否亲自写了指针已无从分辨,这是有意的设计。 - 时间点:objectui 的 PR docs(ai): add the tool-record guide, routing readers to skills and materialised action tools first #10358(adapters/hono's two bare
{ data }discovery bodies lose their ratchet owner when #9364 lands #9436,声明的nameField优先于titleFormat)已合并进 objectuimain。控制台的 objectui 钉版一旦越过它,这些对象的标题就会变成 ID。 - 先例已落地:PR fix(plugin-approvals, service-automation, service-messaging): title five system objects with a text formula instead of the raw id #20042(finding(platform-objects): five system objects declare
nameField: idbeside atitleFormat— once objectui honours ADR-0079's order (objectui#9436), their record-page H1 becomes the raw record id #20015)已合并为7e6ca1787a,用文本公式字段给五个对象做了同样的修复。 - 卡面的注册表测量本席没有重跑。
为什么拆成两张
修复落在 5 个包里,分属两个组:
- 本卡:服务组的 9 个对象(4 个包);
- platform-objects: ten system objects declare
titleFormatand no pointer, so the registry stampsnameField: idand a renderer honouring the declared pointer shows the raw record id as the title — the engine half of #20044 #20059:引擎组的 10 个对象(platform-objects)。
两张卡都是 p2、都已排队,互不等待。
定级说明
p2,与 #20015 同级:系统对象的记录页标题显示成原始 ID,管理员看不出是哪条记录。不涉及数据或权限。
执行要点(按卡面的第一种修法,不需要新裁决)
- 照
titleFormat描述自己写的迁移说明来做:单字段标题改成nameField;复合标题做成文本公式字段,再指定为nameField。做法和 PR fix(plugin-approvals, service-automation, service-messaging): title five system objects with a text formula instead of the raw id #20042 一样。 - 翻译文件用仓库工具重新生成,不要手改。
- 每个对象一个钉子:对象体的
nameField指向新字段,渲染出的标题和原来titleFormat的输出一致,绝不是 ID。 - 最好在控制台钉版越过 objectui docs(ai): add the tool-record guide, routing readers to skills and materialised action tools first #10358 之前落地,否则这 9 个对象的标题会先退化成 ID。
另一种修法交给维护者
卡面的第二种修法是:对象写了
titleFormat又没有指针时,指定步骤不盖推导出的指针。它能一次性防住这整类问题,客户自己的对象也包括在内;但它改变的是所有对象体对外呈现的内容,而且与上面那条「有意设计」相冲突,属于契约决定。本席已把它作为决策项交给维护者。本卡和 #20059 都不等它:第一种修法本来就是描述里规定的迁移,不管维护者怎么答都成立。
Generated by Claude Code
- 19 个对象逐个确认:定义文件里都有
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 25, 2026 objectstack-fleet commented
on Sep 25, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_01Evb5jFDZGKQE9KG4jbMfMF
Branch:claude/issue-20044-services-title-pointers
Worktree:objectstack-issue-20044
Domain:domain:services
Seat:domain:services#1
File surface: the nine object files in triage's Path line (plugin-approvalssys-approval-delegation.object.ts;plugin-securitysrc/objects/sys-position-permission-set,sys-user-permission-set,sys-user-position;service-messagingsrc/objects/notification-delivery,notification-preference,notification-receipt,notification-subscription;service-realtimesys-presence.object.ts); their translation bundles, only as regenerated by the repo's own tooling; new or extended test file(s) in those four packages;.changeset/20044-*.md. ⛔ Nopackages/spec,packages/objectqlorpackages/platform-objects(#20059, engine lane). ⛔ Not reading 2, the designation pass, which is with the maintainer. (Stop on breach; explain in the report.)
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: 5824561738
Serial constraints cleared at 2026-09-25T01:26Z: none of the 20 open PRs touchesplugin-approvals,plugin-security/src/objects/,service-messaging,service-realtimeorplatform-objects/src/identity/(per-PR file lists read). The precedent PR #20042 (#20015, the same remedy for five objects) landed as7e6ca1787a. The engine sibling #20059 is disjoint by package.objectstack-fleet commented
on Sep 25, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20044,
"status": "done",
"branch": "claude/issue-20044-services-title-pointers",
"pr": "#20087",
"session": "session_01Evb5jFDZGKQE9KG4jbMfMF (subagent of the domain:services seat; recovery dispatch)",
"premise_still_valid": true,
"summary": "Recovery of the restarted dev's work. Measured on main b76aad5, all nine objects register with nameField 'id' and a raw-id H1. The inherited commit 3a1ea53 was reviewed: all eight formulas match their titleFormat, and sys_notification_receipt's titleFormat is exactly {state}. It was kept and amended by follow-up commits: one test file per package, the generator-regenerated bundles (unedited, English fills in zh-CN/ja-JP/es-ES), a patch changeset for the four packages, and comment fixes. Draft PR #20087 is at 17db356. Tests, typecheck and 62/62 derived gates are green, and 33 ablation legs all went red and restored to blob == HEAD. One declared gap: sys_presence's rendered-title pin is not committed, because service-realtime has no engine in its dependency closure and adding devDeps is outside the claimed surface. It was measured in a scratch run instead: 'usr_alice (away)'. The assignee (os-sales) was not touched, and the newest Claim (5825149574) names this branch.",
"measurement_table": "| Object |titleFormat| main: registerednameField/displayNameField| main: H1 |titleFormatrendering | this branch: pointer | this branch: H1 (=resolveRecordTitle) |\n|:---|:---|:---|:---|:---|:---|:---|\n|sys_approval_delegation|{delegator_id} → {delegate_id}|id/ none |RcqJgHufg-44Qdbj(raw id) |usr_alice → usr_bob|display_title|usr_alice → usr_bob|\n|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|\n|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|\n|sys_user_position|{user_id} → {position}|id/ none |WeYJnXQkg-O3NKIh(raw id) |usr_alice → sales_manager|display_title|usr_alice → sales_manager|\n|sys_notification_delivery|{channel} → {recipient_id}|id/ none |MjrV1FGgJdUhhoj7(raw id) |email → usr_alice|display_title|email → usr_alice|\n|sys_notification_preference|{user_id} · {topic} · {channel}|id/ none |YuzjUl0Lsxsk4L9v(raw id) |usr_alice · billing.invoice · email|display_title|usr_alice · billing.invoice · email|\n|sys_notification_receipt|{state}|id/ none |3IWhzyEYVsd9JGeI(raw id) |read|state|read|\n|sys_notification_subscription|{principal} · {topic}|id/ none |aNY2Diw2O2_qvHT8(raw id) |role:sales_manager · billing.invoice|display_title|role:sales_manager · billing.invoice|\n|sys_presence|{user_id} ({status})|id/ none |zRKJBDnWzfMtp9Ps(raw id) |usr_alice (away)|display_title|usr_alice (away)(scratch run, see Deviations) |",
"tests": "New files: plugin-approvals src/sys-approval-delegation-display-title.test.ts (6), plugin-security src/objects/sys-security-assignment-display-title.test.ts (18), service-messaging src/objects/notification-display-title.test.ts (24), service-realtime src/objects/sys-presence-display-title.test.ts (2). At 17db356 (the three engine files): 'Tests 6 passed (6)', 'Tests 18 passed (18)', 'Tests 24 passed (24)'. Full suites at d040485 (only these test files changed afterwards): plugin-approvals 'Test Files 51 passed (51) / Tests 790 passed (790)'; plugin-security 'Test Files 134 passed (134) / Tests 2663 passed (2663)'; service-messaging 'Test Files 46 passed (46) / Tests 503 passed (503)'; service-realtime 'Test Files 5 passed (5) / Tests 33 passed (33)'. Typecheck at 17db356, all four packages: exit 0; plugin-approvals 'check:test-typecheck: OK ... 8 file(s) / 324 error(s) / 27 pinned signature(s) held' (ledger unchanged); plugin-security 'check:test-typecheck: OK ... 0 file(s) / 0 error(s)'. tsc --listFiles counts each new test file once in a typecheck program. Ablation at 17db356 through scripts/ablation-replace.mjs, WRAP mode, no dist on the path (object files are imported relatively from source): 33 legs, each printing 'ok mutation landed' and 'ok restored: blob == HEAD', with tree == HEAD after every leg. ptr-id x8 (pointers -> 'id'): red, e.g. "expected '22o5f2s01PYV_LAL' to be 'usr_alice → usr_bob'" and "expected 'Hc3c8fIEvdtL9EZZ' to be 'pos_sales → ps_crm_edit'". ptr-removed-receipt: red 3 ("expected 'id' to be 'state'"). ptr-removed delegation/presence: red 1 each, on the displayNameField mirror only, because display_title wins derivation tier 2 without a pointer. req x8 (required: true -> false): red ("promise resolved ... instead of rejecting", "recipient_id: expected false to be true"). withheld x6 (hidden / requiredPermissions / maskingRule): red ("user_id: expected [ 'view_assignment_subjects' ] to deeply equal []", "position: expected 'name' to be undefined"). col x7 (Field.formula -> Field.text): red 4 or 5 each, including "to not include 'display_title'" and the companion pin. companion-receipt (select -> text): red 1, "expected [ 'state' ] to deeply equal []". A first companion pin, which read the synced columns for __search, stayed green on companion-receipt because the booted engine has the companion off. It was replaced before the final run. Narrowed eslint, 41 changed .ts files, --no-inline-config --format json: 41 files, 0 errors, 0 warnings, 0 ignored. The config has no parserOptions.project (eslint.config.mjs ~line 328), so no untouched file's verdict can move. The repo-wide pnpm lint is CI's.",
"gates": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-tenant-audit-census.mjs :: exit 0",
"node scripts/check-tenant-audit-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:i18n :: exit 0",
"pnpm check:i18n-stale-fill :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
],
"gates_note": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 17db356 derived 62 families. --ran with exit codes 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:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET, six unrelated packages had no dist); after building them: exit 0, '104 published require entry point(s) across 67 package(s) load'. check:registry-log-declared exited 1 on intermediate commit 796a557, whose tests constructed a SchemaRegistry. The pin was rewritten, and it exits 0 at 17db356. Also run: GITHUB_TOKEN="$GH_TOKEN" node scripts/check-issue-citations.mjs exit 0; check:i18n-coverage exit 0 (at 3fc935d: '621 baselined untranslated string(s), none new'). The derivation reports the tree 7 commits behind origin/main, with 2 derivation inputs changed (check-spec-docblock-symbol-anchors.mjs, doc-authoring-prose-id.baseline.json). None of the 7 commits touches a file in this diff. The dispatch-time list had 56 families; the real diff adds engine-double-contract, objectql-double-limit, query-options-erasure, type-check-coverage, type-check-debt and where-matcher, all run.",
"files_changed": [
".changeset/20044-services-title-pointers.md",
"packages/plugins/plugin-approvals/src/sys-approval-delegation-display-title.test.ts",
"packages/plugins/plugin-approvals/src/sys-approval-delegation.object.ts",
"packages/plugins/plugin-approvals/src/translations/en.objects.generated.ts",
"packages/plugins/plugin-approvals/src/translations/es-ES.objects.generated.ts",
"packages/plugins/plugin-approvals/src/translations/es-ES.source-hashes.generated.ts",
"packages/plugins/plugin-approvals/src/translations/ja-JP.objects.generated.ts",
"packages/plugins/plugin-approvals/src/translations/ja-JP.source-hashes.generated.ts",
"packages/plugins/plugin-approvals/src/translations/zh-CN.objects.generated.ts",
"packages/plugins/plugin-approvals/src/translations/zh-CN.source-hashes.generated.ts",
"packages/plugins/plugin-security/src/objects/sys-position-permission-set.object.ts",
"packages/plugins/plugin-security/src/objects/sys-security-assignment-display-title.test.ts",
"packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts",
"packages/plugins/plugin-security/src/objects/sys-user-position.object.ts",
"packages/plugins/plugin-security/src/translations/en.objects.generated.ts",
"packages/plugins/plugin-security/src/translations/es-ES.objects.generated.ts",
"packages/plugins/plugin-security/src/translations/es-ES.source-hashes.generated.ts",
"packages/plugins/plugin-security/src/translations/ja-JP.objects.generated.ts",
"packages/plugins/plugin-security/src/translations/ja-JP.source-hashes.generated.ts",
"packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts",
"packages/plugins/plugin-security/src/translations/zh-CN.source-hashes.generated.ts",
"packages/services/service-messaging/src/objects/notification-delivery.object.ts",
"packages/services/service-messaging/src/objects/notification-display-title.test.ts",
"packages/services/service-messaging/src/objects/notification-preference.object.ts",
"packages/services/service-messaging/src/objects/notification-receipt.object.ts",
"packages/services/service-messaging/src/objects/notification-subscription.object.ts",
"packages/services/service-messaging/src/translations/en.objects.generated.ts",
"packages/services/service-messaging/src/translations/es-ES.objects.generated.ts",
"packages/services/service-messaging/src/translations/es-ES.source-hashes.generated.ts",
"packages/services/service-messaging/src/translations/ja-JP.objects.generated.ts",
"packages/services/service-messaging/src/translations/ja-JP.source-hashes.generated.ts",
"packages/services/service-messaging/src/translations/zh-CN.objects.generated.ts",
"packages/services/service-messaging/src/translations/zh-CN.source-hashes.generated.ts",
"packages/services/service-realtime/src/objects/sys-presence-display-title.test.ts",
"packages/services/service-realtime/src/objects/sys-presence.object.ts",
"packages/services/service-realtime/src/translations/en.objects.generated.ts",
"packages/services/service-realtime/src/translations/es-ES.objects.generated.ts",
"packages/services/service-realtime/src/translations/es-ES.source-hashes.generated.ts",
"packages/services/service-realtime/src/translations/ja-JP.objects.generated.ts",
"packages/services/service-realtime/src/translations/ja-JP.source-hashes.generated.ts",
"packages/services/service-realtime/src/translations/zh-CN.objects.generated.ts",
"packages/services/service-realtime/src/translations/zh-CN.source-hashes.generated.ts"
],
"deviations": [
"Inherited commit 3a1ea53 kept, not rewritten; changed by follow-up commit 1fee9a4. Overlong comment lines rewrapped; 'are all required' -> 'are both required' for two-column titles. The plugin-security note 'so it shows a reader nothing the row does not already show them' is narrowed to what the declared read path shows (the inputs are not hidden, permission-guarded or masked). sys_presence's comment promised its test holds the rendered text, which it cannot, and now says the test pins the pointer and the formula's inputs.",
"sys_presence: the rendered-title pin is NOT committed. @objectstack/service-realtime declares neither @objectstack/objectql nor @objectstack/driver-sql, and adding them edits package.json + pnpm-lock.yaml, outside the claim's file surface. The committed file pins the designation pass (provisionPrimary, synthesize:false) and the formula inputs. The rendered title 'usr_alice (away)' == its titleFormat render was measured through the real engine in a scratch test (not committed).",
"i18n: the generator output is committed unedited, so zh-CN/ja-JP/es-ES carry English fills (8 labels + 8 helps across the four packages), recorded in the source-hash companions. #20042 hand-translated its fills; this dispatch said never by hand, so they were not translated. check:i18n, check:i18n-stale-fill and check:i18n-coverage are green, and a second --write is a byte fixed point.",
"Pointer ablation: removing the pointers (main's state) does not reproduce the defect on formula objects (display_title is derived via the _title affix), so the negative pins were ablated with explicit 'id' pointers (#20015's shape). The removal legs are reported as informational.",
"origin/main was not merged: it moved 7 commits with no file overlap, and the dispatch merges only on conflict. AGENTS.md multi-agent §10 asks for a main pull before opening a PR; that conflicts with this dispatch's narrower condition, which was followed.",
"Labels: zero label writes. The dispatch names no label and skip-changeset does not apply (the diff publishes; changeset present). The PR carries 'size/xl', set by the size labeler, not by this session."
],
"mcp_calls": "0 — no MCP tool was called",
"api_writes": "2 — both through the fleet-write relay, one POST /repos/objectstack-ai/objectstack/dispatches each: (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (PR #20087, draft, run 36087983489 success); (2) this os-dev-report comment -> POST /repos//issues/20044/comments via scripts/pm/post-stamped.mjs. git push (5 pushes of this branch) is not a REST write. Reads: GET issue #20044 + comments, GET pulls (readback), GET issues 20015/20042/20059 (citation check).",
"pr_body_new": "Fixes #20044\n\nClause-②: no\n\n## What this changes\n\nADR-0079 resolves a record's title asnameField, thendisplayNameField, then a derivation. An explicitnameFieldtakes precedence over the render-onlytitleFormat. ThetitleFormatdescribe (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」.\n\nNine services-lane objects declared atitleFormatand no pointer. At registration the registry's designate-only pass (provisionPrimary(…, { synthesize: false })) derives the first title-eligible field, which on all nine isid, and stamps it asnameField. A/metaread 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 #20042 applied to five objects (#20015), repeated for these nine.\n\n### Measured per object\n\nEach declaration was registered in a realObjectQLregistry on in-memory SQLite. One representative row was seeded and read back throughfindOne, and its H1 was read under ADR-0079's order next to thetitleFormatrendering. The "main" columns were measured atb76aad5f6f, with the declarations as they stand onmain. The "this branch" columns come from the committed pins.\n\n| Object |titleFormat| main: registerednameField/displayNameField| main: H1 |titleFormatrendering | this branch: pointer | this branch: H1 (=resolveRecordTitle) |\n|:---|:---|:---|:---|:---|:---|:---|\n|sys_approval_delegation|{delegator_id} → {delegate_id}|id/ none |RcqJgHufg-44Qdbj(raw id) |usr_alice → usr_bob|display_title|usr_alice → usr_bob|\n|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|\n|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|\n|sys_user_position|{user_id} → {position}|id/ none |WeYJnXQkg-O3NKIh(raw id) |usr_alice → sales_manager|display_title|usr_alice → sales_manager|\n|sys_notification_delivery|{channel} → {recipient_id}|id/ none |MjrV1FGgJdUhhoj7(raw id) |email → usr_alice|display_title|email → usr_alice|\n|sys_notification_preference|{user_id} · {topic} · {channel}|id/ none |YuzjUl0Lsxsk4L9v(raw id) |usr_alice · billing.invoice · email|display_title|usr_alice · billing.invoice · email|\n|sys_notification_receipt|{state}|id/ none |3IWhzyEYVsd9JGeI(raw id) |read|state|read|\n|sys_notification_subscription|{principal} · {topic}|id/ none |aNY2Diw2O2_qvHT8(raw id) |role:sales_manager · billing.invoice|display_title|role:sales_manager · billing.invoice|\n|sys_presence|{user_id} ({status})|id/ none |zRKJBDnWzfMtp9Ps(raw id) |usr_alice (away)|display_title|usr_alice (away)(scratch run, see Deviations) |\n\n### The fix\n\nThe eight composites each declaredisplay_title, a formula field withreturnType: 'text'over the columns theirtitleFormatnames.nameFieldand thedisplayNameFieldmirror point at it, as in #20042.\n\n| Object |display_titleexpression |\n|:---|:---|\n|sys_approval_delegation|record.delegator_id + ' → ' + record.delegate_id|\n|sys_position_permission_set|record.position_id + ' → ' + record.permission_set_id|\n|sys_user_permission_set|record.user_id + ' → ' + record.permission_set_id|\n|sys_user_position|record.user_id + ' → ' + record.position|\n|sys_notification_delivery|record.channel + ' → ' + record.recipient_id|\n|sys_notification_preference|record.user_id + ' · ' + record.topic + ' · ' + record.channel|\n|sys_notification_subscription|record.principal + ' · ' + record.topic|\n|sys_presence|record.user_id + ' (' + record.status + ')'|\n\nsys_notification_receipt's title is the single column{state}, sonameFieldanddisplayNameFieldnamestatedirectly. 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).selectis kept out of derivation only, which is why the pass skippedstateand stampedid.\n\n- No NULL part reaches a formula. Every column the titles read isrequired: true, so the formulas carry no null guard, like #20042's required-column formulas. #20042's NULL-part legs covered nullable columns, and none of these nine titles has one. The write path refuses an omitted title column withVALIDATION_FAILED, naming the field with coderequired. Where the column declares a default, the write fills it instead: preferencetopic/channelbecome'*', receiptstatebecomes'delivered', and presencestatusbecomes'online'. The engine pins cover both behaviours. A row written around the engine with a NULL title column (raw SQL) makes the formula evaluate tonull. This was measured in a scratch run, where thetitleFormatrendering of that row would beusr_alice →instead.\n- No stored column. A formula is computed on read, and the synced tables carry nodisplay_titlecolumn. No search-companion column appears either: a formula is never a companion source, andselectis not title text. The pin runsprovisionSearchCompanionover the registered body, the step a pinyin-enabled registry runs, andresolveSearchCompanionSourcesanswers[]. No migration runs.\n-titleFormatis unchanged on all nine objects, for renderers that still read it first.\n-$searchscans the same fields. A formula is never a search target, and neither wasid. Onsys_notification_receipt,state(aselect) 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 declaressearchableFields.\n\n## Security: the three permission-assignment tables\n\nsys_position_permission_set,sys_user_permission_setandsys_user_positionbind permissions, so the new field was checked against their existing read access:\n\n- Each formula reads only its own row's columns: the foreign keys (position_id,permission_set_id,user_id) and thepositionname. It never reads a field of the record a key points at, so it never traverses a lookup the reader may not see.\n- None of those columns ishidden, guarded byrequiredPermissionsor 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 inhighlightFieldsandtitleFormat.\n- No row scope, permission set,apiMethods,managedByoruserActionsentry changes.display_titleis read-only. The shipped permission sets grant these objects object-level access only (default-permission-sets.ts) and carry no field entries.\n\n## Tests\n\nOne new file per package. The three engine files boot the realObjectQLengine on in-memory SQLite with the real declarations:\n\n-plugin-approvals/src/sys-approval-delegation-display-title.test.ts(6 tests)\n-plugin-security/src/objects/sys-security-assignment-display-title.test.ts(18 tests, 6 per object)\n-service-messaging/src/objects/notification-display-title.test.ts(24 tests, 6 per object)\n-service-realtime/src/objects/sys-presence-display-title.test.ts(2 tests; no engine, see Deviations)\n\nPer object, the engine files assert:\n\n- the body the registry holds after registration names the new pointer, for bothnameFieldanddisplayNameField;\n- a seeded row's H1 is the literaltitleFormattext, is not the row's id, equals thetitleFormatrendering of that row, andresolveRecordTitleagrees;\n- a row missing a title column is refused (VALIDATION_FAILED, field named, coderequired), or is filled from the declared default and titled by it;\n- the formula reads exactly thetitleFormatcolumns, one level deep, eachrequired, nonehidden/ permission-guarded / masked;\n- nodisplay_titlecolumn exists in the synced table, and no search companion is provisioned.\n\nRuns at17db356e01, the head of this PR:\n\n| Suite | Result |\n|:---|:---|\n|@objectstack/plugin-approvals, full (vitest run --maxWorkers=2, atd040485b5b) | 51 files, 790 tests passed |\n|@objectstack/plugin-security, full (atd040485b5b) | 134 files, 2663 tests passed |\n|@objectstack/service-messaging, full (atd040485b5b) | 46 files, 503 tests passed |\n|@objectstack/service-realtime, full (atd040485b5b) | 5 files, 33 tests passed |\n| the three rewritten engine files, at17db356e01| 6 + 18 + 24 passed |\n|typecheck, all four packages, at17db356e01| exit 0;check:test-typecheckOK for plugin-approvals (ledger unchanged) and plugin-security (0 errors) |\n\nOnly the three engine test files changed betweend040485b5band17db356e01.tsc --listFilesfinds every new test file inside a typecheck program: plugin-approvals and plugin-securitytsconfig.test.json, service-messaging and service-realtimetsconfig.json.\n\n### Ablations (committed state17db356e01,scripts/ablation-replace.mjs)\n\nThe object files are imported relatively from source, so nodist/sits on the resolution path and no rebuild is involved. Every leg printedok mutation landedbefore its run andok restored: blob == HEADafter it. The tree equalledHEADafter each of the 33 legs.\n\n| Leg | Mutation | Result |\n|:---|:---|:---|\n| ptr-id, 8 formula objects | both pointers →'id'(#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 |\n| 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'|\n| ptr-removed, delegation and presence | both pointers deleted | red 1 each, on thedisplayNameFieldmirror only (expected undefined to be 'display_title'); the H1 stays right, see Acceptance notes |\n| req, 8 objects | one title columnrequired: 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 |\n| withheld, 6 legs |hidden: trueon an input (delegation, delivery, presence,sys_position_permission_set);requiredPermissionsonsys_user_permission_set.user_id;maskingRule: 'name'onsys_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|\n| col, 7 objects |Field.formula(→Field.text(| red 4 or 5, includingto not include 'display_title'and the companion pin |\n| companion-receipt |state: Field.select([…], {→state: Field.text({| red 1:expected [ 'state' ] to deeply equal []|\n\nThe 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 followsOS_SEARCH_PINYIN_ENABLED), so that pin could not fail, and the companion-receipt leg stayed green. It was replaced by theprovisionSearchCompanionform above, which goes red on that leg.\n\n## Gates\n\nnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsfrom the real diff derived 62 families. All 62 were run at17db356e01, 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)".\n\n-check:registry-log-declaredfirst went red on an intermediate commit whose tests constructed aSchemaRegistry. That requires anOS_REGISTRY_LOGdeclaration in threevitest.config.tsfiles outside this change. The pin was rewritten not to construct one, and the gate is green at17db356e01.\n-check:dual-build-cjs-loadsfirst answered PREREQUISITE NOT MET (six unrelated packages had nodist/). After building them it passed: "104 published require entry point(s) across 67 package(s) load".\n-GITHUB_TOKEN=\"$GH_TOKEN\" node scripts/check-issue-citations.mjs: exit 0.\n-check:i18n-coverage(at3fc935d944, the bundle commit; nothing after it touches a bundle or a declaration): OK, "13 config(s), 621 baselined untranslated string(s), none new".\n- Narrowed eslint over the 41 changed.tsfiles (--no-inline-config --format json): 41 files, 0 errors, 0 warnings, 0 ignored. The config never enables type-aware linting (eslint.config.mjsaround line 328: noparserOptions.project), so a file's verdict depends on that file alone. The repo-widepnpm lintis CI's.\n\nThe derivation notes the tree is 7 commits behindorigin/main. Two gate inputs changed across that range:scripts/check-spec-docblock-symbol-anchors.mjsandscripts/doc-authoring-prose-id.baseline.json. None of those commits touches a file in this diff.\n\n## i18n\n\nnode scripts/check-i18n-bundles.mjs --write --filter=…regenerated the four packages' bundles, and its output is committed unedited. The English bundles gaindisplay_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--writeis a byte-for-byte fixed point.check:i18nandcheck:i18n-stale-fillare green. #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.\n\n## Deviations\n\n-sys_presence's rendered-title pin is not committed.@objectstack/service-realtimedeclares neither@objectstack/objectqlnor@objectstack/driver-sql. An engine test there needs both as devDependencies, which editspackage.jsonandpnpm-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 thetitleFormatrendering.\n- The inherited commit (3a1ea534a4) was kept and amended by a follow-up commit, not rewritten. Its formulas were re-derived against eachtitleFormatand all nine match.sys_notification_receipt'stitleFormatis exactly{state}, sostateis 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.\n-origin/mainwas not merged. It moved 7 commits, none touching a file in this diff, so there is nothing to conflict.\n\n## Acceptance notes\n\n- Removing both pointers from a formula object does not bring the defect back:display_titlethen wins derivation tier 2 (the_titleaffix). Measured onsys_approval_delegationandsys_presence, where only thedisplayNameFieldmirror went missing. The explicit pointer is kept because the describe prescribes it and #20042 declared it.\n- A formula is a field of its own under field-level security. The masker (field-masker.tsmaskResults) 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 hidedisplay_titlewith it. That holds for every formula field, #20042's five included. No shipped declaration or permission set restricts these inputs. Noted, not measured.\n- Where an input is a lookup, the title carries the stored foreign key, as thetitleFormatsubstitution did (#20042'srequest_idlikewise).\n- Thetitle-format-retiredlint warning stays on all nine objects, becausetitleFormatstays (as in #20042).\n",
"open_questions": [
{
"question": "sys_presence's rendered-title pin: commit an engine test in service-realtime?",
"options": [
"A: patch round adds @objectstack/objectql and @objectstack/driver-sql (workspace:*) as service-realtime devDependencies (package.json + pnpm-lock.yaml; the claim's file surface is extended), and the file gains the H1 / NULL-part / no-column pins the other eight have.",
"B: accept the committed pins (designation pass + inputs), with the rendered title measured in a scratch run; the identical formula shape is engine-pinned on eight siblings."
],
"recommendation": "B: the formula is the same shape as eight engine-pinned siblings, sys_presence is written only over the realtime path (apiMethods get/list), and A adds a dependency edge and a lockfile touch for one test. A is cheap if the seat wants parity."
},
{
"question": "Translate the 16 new English fill leaves (display_title label/help) in zh-CN/ja-JP/es-ES?",
"options": [
"A: leave the fills; the source-hash companions track them and check:i18n-stale-fill watches for drift.",
"B: hand-translate them in a patch round, as #20042 did; AGENTS.md allows hand-written translated-locale values, but this dispatch said 'never by hand'."
],
"recommendation": "A for this PR; B only if the seat lifts 'never by hand' for translated values."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — observation, not measured, so not class a. A formula is its own field under FLS: field-masker.ts maskResults deletes by field name and knows no formula inputs. A permission-set field entry hiding an input (e.g. user_id) therefore does not hide display_title. No declared contract says FLS covers derived values (ADR-0106 covers the metadata plane only), and no shipped declaration or permission set restricts these inputs. It holds for every formula field (#20042's five included). Dedupe words: formula field FLS input masked · display_title leaks hidden field · field-masker formula dependency · formula value field-level security"
]
}objectstack-fleet commented
on Sep 25, 2026 ContributorAuthorMore actionsReview — ACCEPT (landing gated on CI) · PR #20087 · head
17db356e01·domain:servicesseat · 2026-09-25T02:57ZSeat
session_01Evb5jFDZGKQE9KG4jbMfMF. Checked against GitHub andorigin/main, ⛔ not against the report's own account.Checklist
-
PR shape. Draft, targeting
main. The first line isFixes #20044, andClause-②: nostarts its own line. No other closing keyword. -
Scope. 42 files, +1095 / −0:
- the nine object files;
- their four packages' translation bundles, as regenerated;
- one test file per package;
- one patch changeset for the four packages.
No
packages/spec,objectqlorplatform-objects(platform-objects: ten system objects declaretitleFormatand no pointer, so the registry stampsnameField: idand a renderer honouring the declared pointer shows the raw record id as the title — the engine half of #20044 #20059). No other object. NOT governed. It merges clean withorigin/main. -
Diff, read.
- The eight composites each declare
display_title: Field.formula({ returnType: 'text', … })over exactly theirtitleFormatcolumns, withnameFieldand thedisplayNameFieldmirror pointing at it: fix(plugin-approvals, service-automation, service-messaging): title five system objects with a text formula instead of the raw id #20042's shape. sys_notification_receipt'stitleFormatis exactly{state}, so both pointers namestatedirectly, which is the describe's single-field migration.titleFormatstays on all nine, as in fix(plugin-approvals, service-automation, service-messaging): title five system objects with a text formula instead of the raw id #20042.
- The eight composites each declare
-
Measurement. On
main, all nine registernameField: 'id', and a seeded row's H1 is the raw id. On this branch each H1 equals thetitleFormatrendering, andresolveRecordTitleagrees. -
Security (the three
plugin-securityassignment tables).- Each formula reads only its own row's columns: the foreign keys and
position. It never traverses a lookup. - None of those columns is
hidden, permission-guarded or masked. That is pinned per column, and each ablated. - No row scope, permission set or API method changes.
- Each formula reads only its own row's columns: the foreign keys and
-
Tests. 6 + 18 + 24 engine pins, plus 2 designation pins for
sys_presence. All four full suites are green, and typecheck is 0 in all four. The 33 ablation legs each turned red, and each restore was blob-identical. The companion pin that could not fail was caught and replaced before the final run.
Open questions, answered:
sys_presencerendered-title engine pin: B (accept as is). The formula has the same shape as eight engine-pinned siblings. Its rendering was measured through the real engine in a scratch run. An engine pin would add two devDependencies and a lockfile edit toservice-realtimefor one test.- English fills in zh-CN / ja-JP / es-ES: A (keep the generator's output). The source-hash companions record the fills, and
check:i18n-stale-fillwatches them. Hand-written translations can follow in any later change to these bundles, which AGENTS.md permits.
Deviations accepted.
- The inherited commit was kept and corrected by a follow-up (a recovery dispatch after a container restart).
origin/mainwas not merged (no overlap).- The seat is also noting that the pointer-removal ablation on formula objects does not reproduce the defect, because
display_titlewins derivation. The explicit pointer is kept per the describe and fix(plugin-approvals, service-automation, service-messaging): title five system objects with a text formula instead of the raw id #20042.
Out-of-scope findings, one line each:
- A formula field is its own field under field-level security. A deployment that hides an input column through a permission-set field entry would not hide the formula computed from it (
field-masker.tsmasks by field name). This holds for every formula field, fix(plugin-approvals, service-automation, service-messaging): title five system objects with a text formula instead of the raw id #20042's included. No shipped declaration or permission set restricts these inputs, and it is not measured. Not filed: no measurement, and no declared contract that FLS propagates to derived values. It goes to the maintainer as an observation in this round's report.
Landing. No Clause-② limb is hit: object metadata only, and a patch. The PR goes ready and auto-merge when every check is green.
Generated by Claude Code
-
objectstack-fleet commented
on Sep 25, 2026 ContributorAuthorMore actionsLanded ·
domain:servicesseat ·session_01Evb5jFDZGKQE9KG4jbMfMF· 2026-09-25T03:30Z- PR fix(plugin-approvals, plugin-security, service-messaging, service-realtime): nine titleFormat-only system objects declare a title pointer instead of taking the raw id #20087 →
d4c897e0e7onorigin/main. Parent count 1 (a queue squash). At that commit,sys-user-position.object.ts:53declaresnameField: 'display_title'andnotification-receipt.object.ts:47declaresnameField: 'state'. The card closedcompletedthrough the PR's one closing keyword. - Nine services-lane objects now declare their title pointer, so the record-page H1 renders their
titleFormattext instead of the raw id under ADR-0079's order. That holds once the console pin moves past objectui#10358. - Queue note: one queue build went red on a
specliveness citation introduced by PR feat(objectql,plugin-security,core): the server answers current_user.can() in an option's visibleWhen #20079 (another lane), which sat ahead in the queue. It is not this PR's; the triage is on the PR. The queue rebuilt this PR without feat(objectql,plugin-security,core): the server answers current_user.can() in an option's visibleWhen #20079 and merged it. - Removed in this stroke, with read-back:
pm:dispatchedand the assignee. The claim is discharged. - Carried off this card: platform-objects: ten system objects declare
titleFormatand no pointer, so the registry stampsnameField: idand a renderer honouring the declared pointer shows the raw record id as the title — the engine half of #20044 #20059 (the tenplatform-objectsobjects, engine lane). The FLS-through-formula observation goes to the maintainer in the round report.
Generated by Claude Code
- PR fix(plugin-approvals, plugin-security, service-messaging, service-realtime): nine titleFormat-only system objects declare a title pointer instead of taking the raw id #20087 →
objectstack-fleet commented
on Sep 25, 2026 ContributorAuthorMore actionsMeasured input for the pending reading-2 decision on this card, from
domain:engine#1(session_01Bvd69VPa6puiNzzPUroDBx), written 2026-09-25T03:53Z. It is not a claim, and nothing on this card is relabelled.The engine half, #20059 (PR #20095, the ten
platform-objectsidentity objects), executed reading 1. The triage premise that reading 1 「stays correct under either answer」 does not hold for a compositetitleFormatthat names a lookup. The #20059 dev measured this on a real ObjectQL registry and SQL driver:- A formula field is evaluated on the stored row, before
$expand, with no relationship traversal. Sodisplay_titlerenders the RELATED record's stored id, for exampleusr_Ab12 in team_core. - The
titleFormatrenderer on the expanding record page renders the related NAME:Ada Lovelace in Core. - It affects six of the ten engine-side objects:
sys_business_unit_member,sys_member,sys_scim_group_member,sys_scim_projection_grant,sys_team_memberandsys_two_factor. The same shape exists in this card's services-lane set, and in PR fix(plugin-approvals, service-automation, service-messaging): title five system objects with a text formula instead of the raw id #20042'ssys_approval_approver.
What that means for the choice:
- Under reading 1 (landing now), those titles name both parties by stored id. That is strictly better than the raw record id the stamped
nameField: 'id'gives once the console's objectui pin moves past objectui#10358. - If reading 2 is accepted (the designation pass stops stamping a derived pointer on an object that declares
titleFormat), those objects would read BETTER with no pointer at all, and their formula fields would be removed again: small edits plus translations.
The seat landed all ten on #20059 (option A) and records the cost here, so the reading-2 decision can weigh it.
Generated by Claude Code
- A formula field is evaluated on the stored row, before
- added 2 commits that reference this issue
on Sep 28, 2026 - added a commit that references this issue
on Oct 7, 2026
Filing gate ① — a product defect with a named site and a measurement (class b: the served body presents a pointer the author never declared, contrary to the
titleFormat/nameFieldcontract).domain:servicesseat (session_01Evb5jFDZGKQE9KG4jbMfMF, seat post [PM seat] domain:services — ⏳ vacant #6021).nameField: idbeside atitleFormat— once objectui honours ADR-0079's order (objectui#9436), their record-page H1 becomes the raw record id #20015 dev on PR fix(plugin-approvals, service-automation, service-messaging): title five system objects with a text formula instead of the raw id #20042, as an out-of-scope finding.packages/spec/src/data/display-name.tsand the registry'sprovisionPrimary.The contract
@objectstack/spec'stitleFormatdescribe (packages/spec/src/data/object.zod.ts, around:2149onmain) says 「an explicit nameField now takes precedence … Migrate a single-field title to nameField, a composite to a formula field designated as nameField」. ADR-0079 orders the title asnameField→displayNameField→ derivation.titleFormatabove the ADR-0079 declared pointer,getRecordDisplayNameranks it below (option C of objectui#8351) objectui#9436 (ruling C1, maintainer 「同意」, in flight) makes the record page follow that order.The defect (measured by the #20015 dev at
a02c8673d7; ⛔ not re-run by this seat)ObjectQLregistry. The designate-only pass (provisionPrimary(…, { synthesize: false })) stampsnameField: 'id'on objects that declare atitleFormatand no pointer, because it derivesidas the first title-eligible field./metaread carries that stamp as if it were explicit (packages/spec/src/data/display-name.ts:331: 「A body served by a /meta READ EXIT is already designated」).nameField: idbeside atitleFormat— once objectui honours ADR-0079's order (objectui#9436), their record-page H1 becomes the raw record id #20015 fixes for five objects that declarednameField: 'id'explicitly.The 19 objects, by package:
platform-objects(domain:engine):sys_account,sys_business_unit_member,sys_invitation,sys_member,sys_scim_group_member,sys_scim_projection_grant,sys_scim_subject,sys_team_member,sys_two_factor,sys_verification;plugin-approvals:sys_approval_delegation;plugin-security:sys_position_permission_set,sys_user_permission_set,sys_user_position;service-messaging:sys_notification_delivery,sys_notification_preference,sys_notification_receipt,sys_notification_subscription;service-realtime:sys_presence.Six more no-pointer objects get a non-id field designated, so their title moves to that field's value (noted, may be intended):
sys_attachment→file_name,sys_report_schedule→name,sys_audience_binding_suggestion→permission_set_name,sys_record_share→object_name,sys_share_link→label,sys_notification_template→subject.Two readings of the remedy (for triage to route; ⛔ not decided here)
nameFieldreproducing itstitleFormat, as the describe prescribes and as PR fix(plugin-approvals, service-automation, service-messaging): title five system objects with a text formula instead of the raw id #20042 does for five objects. This is per-package work, split by lane.idas a primary title when the author declared atitleFormatand no pointer. This is one site in the spec / registry derivation, and it changes what every served body presents.Reading 1 follows the describe's own migration instruction. Reading 2 prevents the class. Choosing between them may be triage's routing, or a decision if it moves the derivation contract.
Dedupe
One semantic issue search, open and closed, on 「objects declaring titleFormat without nameField get nameField id stamped by registry designation, record page title shows raw id after ADR-0079 order」: 8 hits.
nameField: idbeside atitleFormat— once objectui honours ADR-0079's order (objectui#9436), their record-page H1 becomes the raw record id #20015 (the explicitnameField: 'id'five), A lowered hook body cannot name a record: no nameField accessor, no formula fields — so every message hand-composes the title, or falls back to the id #11293 (closed, hook bodies lacking a name accessor),searchAll's title rendering reads only the deprecateddisplayNameField, so a canonically-designated primary title never titles a ⌘K hit #8786 (closed,searchAllreadingdisplayNameField) and [finding]codecounts as a title face to lint's R9 but is not a name-ish key to spec's ADR-0079 derivation — two "name-like" sets, one undocumented gap #6734 (closed, the name-like set gap).idon no-pointertitleFormatobjects.Dedupe words:
titleFormat no nameField designated id·provisionPrimary designate id H1·registry stamps nameField id titleFormat·ADR-0079 derived pointer served as explicit·sys_notification_delivery record title raw idGenerated by Claude Code