Skip to content

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

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20044-services-title-pointers
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20044-services-title-pointers

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #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 #20042 applied to five objects (#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 #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 fix(plugin-approvals, service-automation, service-messaging): title five system objects with a text formula instead of the raw id #20042's required-column formulas. fix(plugin-approvals, service-automation, service-messaging): title five system objects with a text formula instead of the raw id #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' (#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. #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


Generated by Claude Code

…ltime): declare a title pointer on nine objects that relied on titleFormat

Eight composite titles become a display_title text formula designated as
nameField; sys_notification_receipt's single-field title points nameField
at state.

Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF
Co-authored-by: Claude <noreply@anthropic.com>
…altime): pin the nine title pointers through the registry

Per object: the registered body names the new pointer, a seeded row's H1
is the titleFormat text and never the id, a row missing a title column
never reaches the formula with a NULL part, the formula reads exactly the
titleFormat columns on its own row (required, none withheld), and nothing
adds a stored column. sys_presence has no engine in its package's
dependency closure, so its file pins the designation pass and the
formula's inputs only.

Also rewraps the inherited object comments and narrows the plugin-security
note to what the declared read path shows.

Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF
Co-authored-by: Claude <noreply@anthropic.com>
Output of `node scripts/check-i18n-bundles.mjs --write --filter=<pkg>` for
plugin-approvals, plugin-security, service-messaging and service-realtime,
unedited: the English leaves come from the new formula fields' label and
description, the translated locales carry the generator's default fill,
and the source-hash companions record those fills.

Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF
Co-authored-by: Claude <noreply@anthropic.com>
…yin-enabled registry about the search companion

The engine the tests boot builds its registry with the companion switched
off (it follows OS_SEARCH_PINYIN_ENABLED), so "no __search column" could
never fail there. The pin now registers each object in a registry built
with `searchCompanion: true` and asserts no companion field is provisioned.

Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF
Co-authored-by: Claude <noreply@anthropic.com>
…ompanion step over the registered body instead of a second registry

Constructing a SchemaRegistry in a test makes check:registry-log-declared
require an OS_REGISTRY_LOG declaration in each package's vitest config,
outside this change's surface. The registry provisions the companion by
running `provisionSearchCompanion` over the body it has designated, so the
pin now runs that pure step over the engine's registered body, and asks
`resolveSearchCompanionSources` for the sources: none.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/plugin-approvals, @objectstack/plugin-security, @objectstack/service-messaging, @objectstack/service-realtime, touching 23 documentable anchor(s).

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

  • content/docs/automation/approvals.mdx (via sys_approval_delegation (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_position (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/automation/hook-bodies.mdx (via nameField (symbol, a field of const object NotificationDelivery; a field of const object NotificationPreference; a field of const object NotificationReceipt; a field of const object NotificationSubscription; a field of const object SysApprovalDelegation; a field of const object SysPositionPermissionSet; a field of const object SysPresence; a field of const object SysUserPermissionSet; a field of const object SysUserPosition))
  • content/docs/data-modeling/formulas.mdx (via nameField (symbol, a field of const object NotificationDelivery; a field of const object NotificationPreference; a field of const object NotificationReceipt; a field of const object NotificationSubscription; a field of const object SysApprovalDelegation; a field of const object SysPositionPermissionSet; a field of const object SysPresence; a field of const object SysUserPermissionSet; a field of const object SysUserPosition), display_title (literal, a string literal in displayNameField; a string literal in nameField))
  • content/docs/data-modeling/objects.mdx (via SysUserPermissionSet (symbol, a top-level const object), displayNameField (symbol, a field of const object NotificationDelivery; a field of const object NotificationPreference; a field of const object NotificationReceipt; a field of const object NotificationSubscription; a field of const object SysApprovalDelegation; a field of const object SysPositionPermissionSet; a field of const object SysPresence; a field of const object SysUserPermissionSet; a field of const object SysUserPosition), nameField (symbol, a field of const object NotificationDelivery; a field of const object NotificationPreference; a field of const object NotificationReceipt; a field of const object NotificationSubscription; a field of const object SysApprovalDelegation; a field of const object SysPositionPermissionSet; a field of const object SysPresence; a field of const object SysUserPermissionSet; a field of const object SysUserPosition), sys_approval_delegation (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_position_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_position (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/data-modeling/schema-design.mdx (via displayNameField (symbol, a field of const object NotificationDelivery; a field of const object NotificationPreference; a field of const object NotificationReceipt; a field of const object NotificationSubscription; a field of const object SysApprovalDelegation; a field of const object SysPositionPermissionSet; a field of const object SysPresence; a field of const object SysUserPermissionSet; a field of const object SysUserPosition), nameField (symbol, a field of const object NotificationDelivery; a field of const object NotificationPreference; a field of const object NotificationReceipt; a field of const object NotificationSubscription; a field of const object SysApprovalDelegation; a field of const object SysPositionPermissionSet; a field of const object SysPresence; a field of const object SysUserPermissionSet; a field of const object SysUserPosition))
  • content/docs/deployment/environment-variables.mdx (via sys_position_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_position (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/permissions/administrator-guide.mdx (via sys_user_position (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/permissions/authentication.mdx (via sys_user_position (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/permissions/authorization.mdx (via sys_position_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_position (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/permissions/delegated-administration.mdx (via sys_position_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_position (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/permissions/permission-sets.mdx (via sys_position_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_position (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/permissions/positions.mdx (via sys_position_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_position (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/permissions/profiles.mdx (via sys_position_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/plugins/packages.mdx (via sys_presence (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/ui/forms.mdx (via nameField (symbol, a field of const object NotificationDelivery; a field of const object NotificationPreference; a field of const object NotificationReceipt; a field of const object NotificationSubscription; a field of const object SysApprovalDelegation; a field of const object SysPositionPermissionSet; a field of const object SysPresence; a field of const object SysUserPermissionSet; a field of const object SysUserPosition))

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

  • content/docs/releases/implementation-status.mdx (via sys_position_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v13.mdx (via SysPositionPermissionSet (symbol, a top-level const object), SysUserPosition (symbol, a top-level const object), sys_position_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_position (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v14.mdx (via sys_user_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_position (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v15.mdx (via sys_user_position (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v16.mdx (via nameField (symbol, a field of const object NotificationDelivery; a field of const object NotificationPreference; a field of const object NotificationReceipt; a field of const object NotificationSubscription; a field of const object SysApprovalDelegation; a field of const object SysPositionPermissionSet; a field of const object SysPresence; a field of const object SysUserPermissionSet; a field of const object SysUserPosition), sys_approval_delegation (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_presence (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects), sys_user_position (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v17/17-0.mdx (via nameField (symbol, a field of const object NotificationDelivery; a field of const object NotificationPreference; a field of const object NotificationReceipt; a field of const object NotificationSubscription; a field of const object SysApprovalDelegation; a field of const object SysPositionPermissionSet; a field of const object SysPresence; a field of const object SysUserPermissionSet; a field of const object SysUserPosition), sys_user_position (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v17/17-1.mdx (via sys_user_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v17/17-2.mdx (via nameField (symbol, a field of const object NotificationDelivery; a field of const object NotificationPreference; a field of const object NotificationReceipt; a field of const object NotificationSubscription; a field of const object SysApprovalDelegation; a field of const object SysPositionPermissionSet; a field of const object SysPresence; a field of const object SysUserPermissionSet; a field of const object SysUserPosition), sys_position_permission_set (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v17/17-4.mdx (via sys_user_position (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))

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

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

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

Which tree this was computed on

This run read content/docs from a9fbf69a090b2ed35cdbd30e56440c9fbfb11573 — the merge of head 17db356e015ace978e64e642025164acdd6607b6 into base 7ddf396109b9d423cf52c2e5b69b0e34f235e7f5, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a9fbf69a090b2ed35cdbd30e56440c9fbfb11573 && git checkout a9fbf69a090b2ed35cdbd30e56440c9fbfb11573
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7ddf396109b9d423cf52c2e5b69b0e34f235e7f5 17db356e015ace978e64e642025164acdd6607b6 && git checkout -B drift-repro 7ddf396109b9d423cf52c2e5b69b0e34f235e7f5 && git merge --no-ff 17db356e015ace978e64e642025164acdd6607b6

node scripts/docs-audit/affected-docs.mjs --json 7ddf396109b9d423cf52c2e5b69b0e34f235e7f5

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

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

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 36089293345 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — evidence pointers (#5623) > is green against a verbatim copy of the shipped ledgers
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — evidence pointers (#5623) > stays green when the missing path is attributed to ANOTHER repo
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — evidence pointers (#5623) > never bounds a citation attributed to ANOTHER repo
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — evidence pointers (#5623) > prints the citation count and how many are in range, in the documented two
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — symbol anchors (#12516) > stays GREEN on the drifted BEFORE-state — the honest residual this grammar e
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — symbol anchors (#12516) > prints the anchor count and how many resolve, equal on a green run
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the evidence-scan population (#13041) > stays GREEN when a `dead` entry carries the SAME rotted pointe
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the evidence-scan population (#13041) > declares every status either scanned or explicitly unscanned, 
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the live-elsewhere criteria (#13483) > is green on the shipped ledgers and publishes the population be
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the README state table (#7257) > is green against a verbatim copy, and says how many rows it checked
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the generated count artifact (#7377) > is green against a verbatim copy, and says the artifact is curr
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the evidence summary line (#5623) > prints declared and resolved as separate numbers, equal on a green
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 2 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Queue-build triage · run 36089293345 red: not this PR's diff · domain:services seat · 2026-09-25T03:26Z


Generated by Claude Code

Merged via the queue into main with commit d4c897e Sep 25, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20044-services-title-pointers branch September 25, 2026 03:30
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…er instead of the stamped id (objectstack-ai#20095)

Fixes objectstack-ai#20059

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`) says so and prescribes
the migration:

> [DEPRECATED → nameField (ADR-0079)] Render-only title template; the
server cannot return or query it, and an explicit nameField now takes
precedence. Migrate a single-field title to nameField, a composite to a
formula field designated as nameField.

Ten identity objects in `packages/platform-objects` declared a
`titleFormat` and no pointer. The registry's designate-only pass
(`provisionPrimary(…, { synthesize: false })` in `materializeBaseLayer`)
derived `id`, the first title-eligible field, and stamped `nameField:
'id'`. A `/meta` read 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 objectstack-ai#20042 (the five services-lane objects,
landed `7e6ca1787a`):

| Object | `titleFormat` | Migration | `nameField` / `displayNameField`
| Formula (`display_title`, `returnType: 'text'`) |
|:---|:---|:---|:---|:---|
| `sys_account` | `{provider_id} - {account_id}` | formula |
`display_title` | `record.provider_id + ' - ' + record.account_id` |
| `sys_business_unit_member` | `{user_id} in {business_unit_id}` |
formula | `display_title` | `record.user_id + ' in ' +
record.business_unit_id` |
| `sys_invitation` | `Invitation for {email}` | formula |
`display_title` | `'Invitation for ' + record.email` |
| `sys_member` | `{user_id} ({role})` | formula | `display_title` |
`record.role != null ? record.user_id + ' (' + record.role + ')' :
record.user_id` |
| `sys_scim_group_member` | `{scim_user_id} in {group_id}` | formula |
`display_title` | `record.scim_user_id + ' in ' + record.group_id` |
| `sys_scim_projection_grant` | `{role} → {user_id}` | formula |
`display_title` | `record.role + ' → ' + record.user_id` |
| `sys_scim_subject` | `{user_id}` | single field | `user_id` | none |
| `sys_team_member` | `{user_id} in {team_id}` | formula |
`display_title` | `record.user_id + ' in ' + record.team_id` |
| `sys_two_factor` | `Two-factor for {user_id}` | formula |
`display_title` | `'Two-factor for ' + record.user_id` |
| `sys_verification` | `Verification for {identifier}` | formula |
`display_title` | `'Verification for ' + record.identifier` |

- **Single field vs composite.** A template that is exactly one
`{field}` is single-field. It takes the pointer directly, as in
`sys_session` (`nameField: 'user_id'`) and the other `(single-field
titleFormat)` objects. A template with literal text is a composite, so
it becomes a formula.
- **Null guard.** Every source column is `required: true` except
`sys_member.role`. A `sys_member` row without a role is titled by its
user alone, so the formula does not fail to evaluate.
- **`titleFormat` is kept** on all ten objects for renderers that still
read it first. PR objectstack-ai#20042 kept it too.
- **No stored column.** A formula is virtual. The measured synced SQLite
table has no `display_title` column on any of the nine objects. No
migration runs.
- **`$search` is unchanged.** The display field only orders the
auto-default set and never admits a field (`autoDefaultFields`,
`packages/spec/src/data/search-fields.ts`). `formula` is in
`SEARCH_VIRTUAL_TYPES`, and `lookup` and `id` are auto-excluded. The
`__search` companion refuses a formula and a lookup source
(`isCompanionSourceType`), and it refused the primary key before, so no
companion column appears either.
- **Expression tag import.** The objects use `import { F } from
'@objectstack/spec/shared'`, the subpath this package already imports
from. PR objectstack-ai#20042 used the root entry.

The commits are split so the six lookup-bearing objects can be separated
if the seat wants that: `7def64154b` carries the four whose title a
pointer reproduces on every surface (`sys_account`, `sys_invitation`,
`sys_verification`, `sys_scim_subject`), and `3eb8abe463` carries the
six whose `titleFormat` names a lookup (see "Lookups" below).

## Measurement (real `ObjectQL` registry + `SqlDriver` on 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 at `66960564d9`. Head = this branch.

| Object | served `nameField` base → head | H1 under ADR-0079 order,
base | H1, head |
|:---|:---|:---|:---|
| `sys_account` | `id` → `display_title` | raw id | `github - 5812039` |
| `sys_business_unit_member` | `id` → `display_title` | raw id |
`usr_Ab12 in bu_emea` |
| `sys_invitation` | `id` → `display_title` | raw id | `Invitation for
ada@example.com` |
| `sys_member` | `id` → `display_title` | raw id | `usr_Ab12 (admin)` |
| `sys_scim_group_member` | `id` → `display_title` | raw id | `scu_Qx90
in scg_ops` |
| `sys_scim_projection_grant` | `id` → `display_title` | raw id |
`billing_admin → usr_Ab12` |
| `sys_scim_subject` | `id` → `user_id` | raw id | `usr_Ab12` (a
renderer reduces the expanded user to its name) |
| `sys_team_member` | `id` → `display_title` | raw id | `usr_Ab12 in
team_core` |
| `sys_two_factor` | `id` → `display_title` | raw id | `Two-factor for
usr_Ab12` |
| `sys_verification` | `id` → `display_title` | raw id | `Verification
for ada@example.com` |

At base, `displayNameField` was absent and only `nameField: 'id'` was
stamped. `resolveRecordTitle` returned the raw id at base and returns
the head H1 above now. At head, the H1 equals the `titleFormat`
rendering of the same stored row for all ten.

### Lookups: where the formula and the `titleFormat` differ

The reference renderer is objectui's `formatTitleTemplate`
(`packages/core/src/utils/record-title.ts` at objectui `ff14e29`),
ported into the script. A formula is evaluated on the stored row before
`$expand` (`applyFormulaPlan` runs before `expandRelatedRecords` in
`find`/`findOne`). It cannot reach a related record: `validate.ts` in
`@objectstack/formula` states that nothing hydrates relationship
traversal at a formula value. The record page `$expand`s every lookup
(`buildExpandFields` in objectui's `RecordDetailView`). So on that page
the two differ for the six objects whose template names a lookup:

| Object | formula (= `titleFormat` on the stored row) | `titleFormat`
on an expanded row |
|:---|:---|:---|
| `sys_business_unit_member` | `usr_Ab12 in bu_emea` | `Ada Lovelace in
EMEA Sales` |
| `sys_member` | `usr_Ab12 (admin)` | `Ada Lovelace (admin)` |
| `sys_scim_group_member` | `scu_Qx90 in scg_ops` | `ada in Ops` |
| `sys_scim_projection_grant` | `billing_admin → usr_Ab12` |
`billing_admin → Ada Lovelace` |
| `sys_team_member` | `usr_Ab12 in team_core` | `Ada Lovelace in Core` |
| `sys_two_factor` | `Two-factor for usr_Ab12` | `Two-factor for Ada
Lovelace` |

For these six, the formula title carries the related record's stored id
where the `titleFormat` renderer showed its name. That is better than
the stamped raw id, and it is what PR objectstack-ai#20042's `sys_approval_approver`
(`{approver} · {request_id}`) does. For `sys_scim_subject`, the lookup
pointer reproduces the `titleFormat` on both kinds of row. The three
all-text objects match on both kinds of row.

## Reach

- **No lookup targets the ten.** `git grep` for a `lookup('…')` /
`master_detail('…')` / `reference: '…'` naming any of the ten objects
under `packages/` and `examples/` returns 0 hits. Controls: 86
`lookup('sys_user')` hits and 141 `reference: 'sys_user'` hits in the
same trees. So no lookup chip, audit lookup-title resolution
(`resolveLookupTitles` in plugin-audit, which skipped these objects
while the title field was `id`) or approval display enrichment reads
their title today.
- **Server title consumers.** Nine non-test files call
`resolveRecordTitle`, `titleFieldOf` or `resolveDisplayField`. They all
read the declared pointer, and a formula pointer is the case
`resolveRecordTitle` evaluates.
- **objectui list defaults.** `leadWithNameField` leads a synthesized
default column list with the declared pointer. Before, that was the
stamped `id`. Now it is `display_title` (or `user_id`).
- **better-auth.** Nine of the ten are `managedBy: 'better-auth'`. The
full `@objectstack/plugin-auth` suite (adapter, schema parity, ADR-0105
D7 extension-field collision guard) is green with the new field.
- **Translations.** The only generated artefact these objects feed is
the object translation bundles. No generated docs page lists their
fields.

## Tests

New:
`packages/platform-objects/src/identity/identity-display-title.test.ts`,
29 cases. Per formula object it asserts:

- the pointer the registry's designate-only pass leaves on the served
body (`provisionPrimary(…, { synthesize: false })`, the same call
`materializeBaseLayer` makes) is `display_title` with the
`displayNameField` mirror, not `id`;
- `display_title` is a `formula` with `returnType: 'text'`, so it is
title-eligible and has no stored column;
- the formula, evaluated as the read path evaluates it
(`ExpressionEngine.evaluate(expression, { now, record })`, `null` when
not ok), renders the `titleFormat` text for a stored row and never
contains the row's id.

It also pins the `sys_member` null-role leg and the `sys_scim_subject`
pointer. platform-objects has no `@objectstack/objectql` dependency, so
the engine-level readings above come from the one-off script, not from a
new dependency.

| Run | Result |
|:---|:---|
| `@objectstack/platform-objects`, full suite, at `141fcb8c3a` | 55
files, 912 tests passed |
| `@objectstack/platform-objects` `typecheck` (3 programs incl.
`check:test-typecheck`), at `141fcb8c3a` | exit 0; `tsc --listFiles`
puts the new test in the `tsconfig.test.json` program |
| `@objectstack/plugin-auth`, full suite | 114 files, 2440 tests passed
|
| `@objectstack/plugin-security`, full suite | 133 files, 2648 tests
passed |
| `@objectstack/client`: the five files that import the identity objects
| 46 tests passed |
| `@objectstack/runtime` `action-execution-destructive.test.ts`,
`@objectstack/service-messaging` `recipient-locale-shape-parity.test.ts`
| 66 and 16 passed |

The consumer suites ran on `c48a6ee11d`, before the merge of `main`
`338feda6dd`. That merge brought in 17 commits
(`66960564d9..338feda`) that touched `packages/client` and
`packages/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.

| Leg | Mutation | Result |
|:---|:---|:---|
| base | all ten object files set to their `66960564d9` blobs | red
29/29, e.g. `expected { nameField: 'id', …(1) } to deeply equal {
nameField: 'display_title', …(1) }` |
| ptr (×9) | delete the `displayNameField` + `nameField` lines | red
1/29 each (the served-pointer case) |
| ptr-subject | delete `sys_scim_subject`'s pointer lines | red 1/29:
`expected { nameField: 'id', …(1) } to deeply equal { nameField:
'user_id', …(1) }` |
| type | `sys_member` `Field.formula(` → `Field.text(` | red 1/29:
`expected 'text' to be 'formula'` |
| guard | drop `sys_member`'s null guard | red 1/29: `expected null to
be 'usr_Cd34'` |

The ptr legs go red on the mirror alone. With the pointer deleted,
derivation still picks `display_title`, because `*_title` is a tier-2
name-ish affix in `resolveDisplayField`. 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/objectstack` derived 60 families from the real diff. All
60 were run, and `--ran` reconciles: "60 derived, 60 run, 0
NOT-MEASURED, 0 UNRUN".
- `check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET: six
unrelated packages had no `dist/`. It was re-run green after building
them.
- Verdict lines include:
- `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.
- Also run: `check:i18n-coverage` OK ("621 baselined untranslated
string(s), none new"), and `GITHUB_TOKEN=… node
scripts/check-issue-citations.mjs --base 338feda` exit 0.
- Narrowed eslint (repo-wide `pnpm lint` is CI's). The population is the
15 changed `.ts` files, none ignored (no `ignored` notice in the json).
`--format json`: 15 files, 0 errors, 0 warnings. `eslint.config.mjs`
never 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 nine `display_title` label and help
leaves are translated by hand in zh-CN, ja-JP and es-ES. A second
`--write` returned the three `source-hashes.generated.ts` companions to
their prior bytes, because none of the new leaves is a fill.

## Acceptance notes

- **The six lookup-bearing objects, and reading 2 of objectstack-ai#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 a `titleFormat` object). Measured, that holds for four of the
ten, not for six. If reading 2 is accepted, an object with no pointer
falls through to `titleFormat`, and on the expanded record page that
renders `Ada Lovelace in Core`. These six now declare a pointer, which
wins, so they would show `usr_Ab12 in team_core` instead. 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 objectstack-ai#20042's
`sys_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 draws `usr_… ()`,
because `(` is not in its separator class. The formula draws the user
alone, matching PR objectstack-ai#20042's null legs. An empty-string role renders
`usr_… ()` in both. In the engine measurement an inserted null role took
the field's default (`member`).
- `sys_verification`: better-auth writes `identifier` as
`reset-password:` plus the live reset token for a password reset
(`better-auth/dist/api/routes/password.mjs`). The `titleFormat` rendered
that already, and `identifier` is already served on every read of this
private object. `display_title` repeats 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-②: no` is 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.
- objectstack-ai#20044 (the nine services-lane objects) closed `completed` at 03:30Z
through PR objectstack-ai#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](https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

1 participant