fix(platform-objects): title ten identity objects by a declared pointer instead of the stamped id - #20095
Conversation
…ts whose titleFormat a pointer reproduces exactly
sys_account, sys_invitation and sys_verification gain display_title, a text
formula reproducing their all-text titleFormat, with nameField and the
displayNameField mirror pointing at it. sys_scim_subject has a single-field
titleFormat ('{user_id}'), so its pointer names user_id directly, the same
shape as sys_session.
Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx
Co-authored-by: Claude <noreply@anthropic.com>
… a text formula instead of the stamped id sys_business_unit_member, sys_member, sys_scim_group_member, sys_scim_projection_grant, sys_team_member and sys_two_factor gain display_title, a text formula over the columns their titleFormat names, and point nameField and the displayNameField mirror at it. Their templates name a lookup, which a formula reads as the stored id. Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx Co-authored-by: Claude <noreply@anthropic.com>
…title of the ten identity objects Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx Co-authored-by: Claude <noreply@anthropic.com>
…title pointers Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx Co-authored-by: Claude <noreply@anthropic.com>
…ay_title and translate its nine leaves Regenerated with `node scripts/check-i18n-bundles.mjs --write --filter=platform-objects`; the zh-CN, ja-JP and es-ES leaves are translated by hand, and a second `--write` returned the source-hash companions to their prior bytes. Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 064ef94448f995dc751094e1bb3d403c48e495f2 && git checkout 064ef94448f995dc751094e1bb3d403c48e495f2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d4c897e0e700d96e29ff8a39e91c85f2a8a30909 141fcb8c3ad21e7a618e110effc44088d8adcd0d && git checkout -B drift-repro d4c897e0e700d96e29ff8a39e91c85f2a8a30909 && git merge --no-ff 141fcb8c3ad21e7a618e110effc44088d8adcd0d
node scripts/docs-audit/affected-docs.mjs --json d4c897e0e700d96e29ff8a39e91c85f2a8a30909
|
Contract reviewServed-tier: Scope: 16 files (+553/−0) on merge base
The ten object files at Read: card #20059, the PR body, the diff, all 34 check-runs, AGENTS.md, and the code at head and base. Measured through a real ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
Fixes #20059
Clause-②: no
What this changes
ADR-0079 resolves a record's title as
nameField, thendisplayNameField, then a derivation. An explicitnameFieldtakes precedence over the render-onlytitleFormat. ThetitleFormatdescribe (packages/spec/src/data/object.zod.ts) says so and prescribes the migration:Ten identity objects in
packages/platform-objectsdeclared atitleFormatand no pointer. The registry's designate-only pass (provisionPrimary(…, { synthesize: false })inmaterializeBaseLayer) derivedid, the first title-eligible field, and stampednameField: 'id'. A/metaread serves that stamp. So a renderer honouring the declared pointer (objectui#9436, landed there as objectui#10358) draws the raw record id as the H1.This PR follows the shape of PR #20042 (the five services-lane objects, landed
7e6ca1787a):titleFormatnameField/displayNameFielddisplay_title,returnType: 'text')sys_account{provider_id} - {account_id}display_titlerecord.provider_id + ' - ' + record.account_idsys_business_unit_member{user_id} in {business_unit_id}display_titlerecord.user_id + ' in ' + record.business_unit_idsys_invitationInvitation for {email}display_title'Invitation for ' + record.emailsys_member{user_id} ({role})display_titlerecord.role != null ? record.user_id + ' (' + record.role + ')' : record.user_idsys_scim_group_member{scim_user_id} in {group_id}display_titlerecord.scim_user_id + ' in ' + record.group_idsys_scim_projection_grant{role} → {user_id}display_titlerecord.role + ' → ' + record.user_idsys_scim_subject{user_id}user_idsys_team_member{user_id} in {team_id}display_titlerecord.user_id + ' in ' + record.team_idsys_two_factorTwo-factor for {user_id}display_title'Two-factor for ' + record.user_idsys_verificationVerification for {identifier}display_title'Verification for ' + record.identifier{field}is single-field. It takes the pointer directly, as insys_session(nameField: 'user_id') and the other(single-field titleFormat)objects. A template with literal text is a composite, so it becomes a formula.required: trueexceptsys_member.role. Asys_memberrow without a role is titled by its user alone, so the formula does not fail to evaluate.titleFormatis kept on all ten objects for renderers that still read it first. PR fix(plugin-approvals, service-automation, service-messaging): title five system objects with a text formula instead of the raw id #20042 kept it too.display_titlecolumn on any of the nine objects. No migration runs.$searchis unchanged. The display field only orders the auto-default set and never admits a field (autoDefaultFields,packages/spec/src/data/search-fields.ts).formulais inSEARCH_VIRTUAL_TYPES, andlookupandidare auto-excluded. The__searchcompanion refuses a formula and a lookup source (isCompanionSourceType), and it refused the primary key before, so no companion column appears either.import { F } from '@objectstack/spec/shared', the subpath this package already imports from. PR fix(plugin-approvals, service-automation, service-messaging): title five system objects with a text formula instead of the raw id #20042 used the root entry.The commits are split so the six lookup-bearing objects can be separated if the seat wants that:
7def64154bcarries the four whose title a pointer reproduces on every surface (sys_account,sys_invitation,sys_verification,sys_scim_subject), and3eb8abe463carries the six whosetitleFormatnames a lookup (see "Lookups" below).Measurement (real
ObjectQLregistry +SqlDriveron better-sqlite3:memory:, real declarations)One-off script, not committed. Each object was registered, synced, written with a representative row, and read back with
findOne. Base = the ten files at66960564d9. Head = this branch.nameFieldbase → headsys_accountid→display_titlegithub - 5812039sys_business_unit_memberid→display_titleusr_Ab12 in bu_emeasys_invitationid→display_titleInvitation for ada@example.comsys_memberid→display_titleusr_Ab12 (admin)sys_scim_group_memberid→display_titlescu_Qx90 in scg_opssys_scim_projection_grantid→display_titlebilling_admin → usr_Ab12sys_scim_subjectid→user_idusr_Ab12(a renderer reduces the expanded user to its name)sys_team_memberid→display_titleusr_Ab12 in team_coresys_two_factorid→display_titleTwo-factor for usr_Ab12sys_verificationid→display_titleVerification for ada@example.comAt base,
displayNameFieldwas absent and onlynameField: 'id'was stamped.resolveRecordTitlereturned the raw id at base and returns the head H1 above now. At head, the H1 equals thetitleFormatrendering of the same stored row for all ten.Lookups: where the formula and the
titleFormatdifferThe reference renderer is objectui's
formatTitleTemplate(packages/core/src/utils/record-title.tsat objectuiff14e29), ported into the script. A formula is evaluated on the stored row before$expand(applyFormulaPlanruns beforeexpandRelatedRecordsinfind/findOne). It cannot reach a related record:validate.tsin@objectstack/formulastates that nothing hydrates relationship traversal at a formula value. The record page$expands every lookup (buildExpandFieldsin objectui'sRecordDetailView). So on that page the two differ for the six objects whose template names a lookup:titleFormaton the stored row)titleFormaton an expanded rowsys_business_unit_memberusr_Ab12 in bu_emeaAda Lovelace in EMEA Salessys_memberusr_Ab12 (admin)Ada Lovelace (admin)sys_scim_group_memberscu_Qx90 in scg_opsada in Opssys_scim_projection_grantbilling_admin → usr_Ab12billing_admin → Ada Lovelacesys_team_memberusr_Ab12 in team_coreAda Lovelace in Coresys_two_factorTwo-factor for usr_Ab12Two-factor for Ada LovelaceFor these six, the formula title carries the related record's stored id where the
titleFormatrenderer showed its name. That is better than the stamped raw id, and it is what PR #20042'ssys_approval_approver({approver} · {request_id}) does. Forsys_scim_subject, the lookup pointer reproduces thetitleFormaton both kinds of row. The three all-text objects match on both kinds of row.Reach
git grepfor alookup('…')/master_detail('…')/reference: '…'naming any of the ten objects underpackages/andexamples/returns 0 hits. Controls: 86lookup('sys_user')hits and 141reference: 'sys_user'hits in the same trees. So no lookup chip, audit lookup-title resolution (resolveLookupTitlesin plugin-audit, which skipped these objects while the title field wasid) or approval display enrichment reads their title today.resolveRecordTitle,titleFieldOforresolveDisplayField. They all read the declared pointer, and a formula pointer is the caseresolveRecordTitleevaluates.leadWithNameFieldleads a synthesized default column list with the declared pointer. Before, that was the stampedid. Now it isdisplay_title(oruser_id).managedBy: 'better-auth'. The full@objectstack/plugin-authsuite (adapter, schema parity, ADR-0105 D7 extension-field collision guard) is green with the new field.Tests
New:
packages/platform-objects/src/identity/identity-display-title.test.ts, 29 cases. Per formula object it asserts:provisionPrimary(…, { synthesize: false }), the same callmaterializeBaseLayermakes) isdisplay_titlewith thedisplayNameFieldmirror, notid;display_titleis aformulawithreturnType: 'text', so it is title-eligible and has no stored column;ExpressionEngine.evaluate(expression, { now, record }),nullwhen not ok), renders thetitleFormattext for a stored row and never contains the row's id.It also pins the
sys_membernull-role leg and thesys_scim_subjectpointer. platform-objects has no@objectstack/objectqldependency, so the engine-level readings above come from the one-off script, not from a new dependency.@objectstack/platform-objects, full suite, at141fcb8c3a@objectstack/platform-objectstypecheck(3 programs incl.check:test-typecheck), at141fcb8c3atsc --listFilesputs the new test in thetsconfig.test.jsonprogram@objectstack/plugin-auth, full suite@objectstack/plugin-security, full suite@objectstack/client: the five files that import the identity objects@objectstack/runtimeaction-execution-destructive.test.ts,@objectstack/service-messagingrecipient-locale-shape-parity.test.tsThe consumer suites ran on
c48a6ee11d, before the merge ofmain338feda6dd. That merge brought in 17 commits (66960564d9..338feda6dd) that touchedpackages/clientandpackages/runtime. CI at the merged head ran those suites green. (Corrected by the seat from contract review 5826600669.)Red at base, and ablations (committed state, each restore proven blob == HEAD, post-batch tree clean)
The test imports the object files relatively from source, so no
dist/sits on the resolution path.66960564d9blobsexpected { nameField: 'id', …(1) } to deeply equal { nameField: 'display_title', …(1) }displayNameField+nameFieldlinessys_scim_subject's pointer linesexpected { nameField: 'id', …(1) } to deeply equal { nameField: 'user_id', …(1) }sys_memberField.formula(→Field.text(expected 'text' to be 'formula'sys_member's null guardexpected null to be 'usr_Cd34'The ptr legs go red on the mirror alone. With the pointer deleted, derivation still picks
display_title, because*_titleis a tier-2 name-ish affix inresolveDisplayField. The explicit pointer is what the describe prescribes and what keeps the designation independent of the field's name.Gates (at
141fcb8c3a)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 60 families from the real diff. All 60 were run, and--ranreconciles: "60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN".check:dual-build-cjs-loadsfirst answered PREREQUISITE NOT MET: six unrelated packages had nodist/. It was re-run green after building them.check:platform-object-tenancy-census: "84 platform-namespace objects, 58 in the machinery's reach, 26 outside it";check:i18n: all 9 packages in sync;check:i18n-stale-fill: 0 stale fills;check:nul-bytes: OK;check:test-source-alias: OK;check:published-files: OK.check:i18n-coverageOK ("621 baselined untranslated string(s), none new"), andGITHUB_TOKEN=… node scripts/check-issue-citations.mjs --base 338feda6ddexit 0.pnpm lintis CI's). The population is the 15 changed.tsfiles, none ignored (noignorednotice in the json).--format json: 15 files, 0 errors, 0 warnings.eslint.config.mjsnever enables type-aware linting (its own statement near line 327), so this diff cannot move a verdict on an untouched file.i18n
Regenerated with
node scripts/check-i18n-bundles.mjs --write --filter=platform-objects. The ninedisplay_titlelabel and help leaves are translated by hand in zh-CN, ja-JP and es-ES. A second--writereturned the threesource-hashes.generated.tscompanions to their prior bytes, because none of the new leaves is a fill.Acceptance notes
titleFormatand NO pointer getnameField: idstamped by the registry designation pass, so a renderer honouring the declared pointer (objectui#9436) shows the raw record id as the title #20044. The triage note on this card says reading 1 "stays correct under either answer" to reading 2 (the designation pass not stamping a derived pointer on atitleFormatobject). Measured, that holds for four of the ten, not for six. If reading 2 is accepted, an object with no pointer falls through totitleFormat, and on the expanded record page that rendersAda Lovelace in Core. These six now declare a pointer, which wins, so they would showusr_Ab12 in team_coreinstead. That is still better than the raw id they show without reading 2. The six are in their own commit (3eb8abe463) if the seat prefers to hold them for the maintainer's answer. The same limit applies to 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. A formula has no relationship traversal, so no in-contract formula can title by a related record's name.sys_member: for a null role, objectui's renderer drawsusr_… (), because(is not in its separator class. The formula draws the user alone, matching PR fix(plugin-approvals, service-automation, service-messaging): title five system objects with a text formula instead of the raw id #20042's null legs. An empty-string role rendersusr_… ()in both. In the engine measurement an inserted null role took the field's default (member).sys_verification: better-auth writesidentifierasreset-password:plus the live reset token for a password reset (better-auth/dist/api/routes/password.mjs). ThetitleFormatrendered that already, andidentifieris already served on every read of this private object.display_titlerepeats that column and widens nothing. The field's description ("Email address or phone number") is incomplete. That is a pre-existing note, not changed here.Clause-②: nois carried from the claim. The change adds a read-only virtual field to nine objects' read shapes and a pointer to all ten. It widens no accepted input: a formula is not writable.titleFormatand NO pointer getnameField: idstamped by the registry designation pass, so a renderer honouring the declared pointer (objectui#9436) shows the raw record id as the title #20044 (the nine services-lane objects) closedcompletedat 03:30Z through 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, before this PR opened. It is not touched here, and reading 2 is not addressed here. (Corrected by the seat from contract review 5826600669.)Generated by Claude Code