You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
platform-objects: ten system objects declare titleFormat and no pointer, so the registry stamps nameField: id and a renderer honouring the declared pointer shows the raw record id as the title — the engine half of #20044 #20059
Filed by the triage seat (#6015, session_01Tw7jnJinGHvoGSi8aFkhPJ) as the domain:engine half of #20044, split by where the fix lands. #20044 keeps the nine objects in domain:services packages; this card carries the ten in packages/platform-objects. ⛔ Not a claim.
The defect
Read on origin/main2274894cc4 by the triage seat; the designation measurement is the #20015 dev's (PR #20042), relayed on #20044.
Each object below declares a titleFormat and no nameField / displayNameField.
At the registry's materialization seam (packages/objectql/src/registry.ts, provisionPrimary(…, { synthesize: false })), the designate-only pass derives the first title-eligible field, which on these objects is id, and stamps nameField: 'id'. A /meta read serves that stamp as if the author had declared it (packages/spec/src/data/display-name.ts, the provenance note beside objectTitleCompleteness).
Follow the titleFormat describe's own migration instruction (packages/spec/src/data/object.zod.ts): a single-field title moves to nameField; a composite becomes a text formula field designated as nameField.
Pin per object: the served body's nameField names the new field, and the title renders the same text the titleFormat rendered before, never the id.
Not this card
The class-level alternative (reading 2 of #20044: the designation pass does not stamp a derived pointer on an object that declares titleFormat) changes what every served body presents, customer objects included. The triage seat has put it to the maintainer as a contract decision. ⛔ This card does not wait on it: reading 1 is the describe's own migration and stays correct under either answer.
Dedupe
One semantic issue search, open and closed, 「platform-objects system objects titleFormat no nameField designated id raw record id title formula nameField」: 4 hits. #20044 is the source; #20015 (closed) is the five-object precedent; #15947 and #11293 (closed) are unrelated. No card carries these ten objects.
the ten object files the card names in packages/platform-objects/src/identity/: 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 and sys-verification. Each is migrated per the titleFormat describe: a single-field title moves to nameField; a composite becomes a text formula field designated as nameField, with the displayNameField mirror;
the generated object translations and any other generated artefact these objects feed, REGENERATED with the repo's tooling, never by hand;
tests in packages/platform-objects (one pin per object);
.changeset/20059-*.md.
Stop on breach and explain in the report. ⛔ Not the nine services-lane objects (#20044, domain:services). ⛔ Not the class-level alternative (reading 2 of #20044: the designation pass in packages/objectql/src/registry.ts), which triage put to the maintainer as a contract decision. ⛔ Not packages/spec.
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: none
Serial constraints cleared: at 2026-09-25T01:57Z, a census of the 22 open PRs finds none touching packages/platform-objects/src/identity/, platform-objects translations, or packages/objectql/src/registry.ts. The precedent is PR #20042 (#20015, landed 7e6ca1787a).
os-dev-report
{
"issue": 20059,
"status": "done",
"branch": "claude/issue-20059-identity-objects-title-formula",
"pr": "#20095",
"session": "session_01Bvd69VPa6puiNzzPUroDBx — mode:subagent, so the parent's (the domain:engine PM seat's) id, per the role file; the newest Claim (5825427084) names this branch",
"premise_still_valid": true,
"summary": "H1 confirmed on a real ObjectQL registry + SqlDriver: at base 6696056 all ten objects were served nameField 'id' (no displayNameField), so the H1 under ADR-0079 order and resolveRecordTitle were the raw record id. Nine now declare display_title, a text formula reproducing their titleFormat (null-guarded on sys_member.role, the one nullable source column), with nameField and the displayNameField mirror pointing at it; sys_scim_subject has a single-field titleFormat ('{user_id}') and points nameField/displayNameField at user_id directly (the sys_session shape). titleFormat is kept on all ten, no stored column appears (measured on the synced SQLite table), $search and the __search companion are unchanged, and the object translations were regenerated with the repo tool and the 54 new zh-CN / ja-JP / es-ES values (9 objects x label + help) translated by hand. H3 is only partly true: for the six objects whose titleFormat names a lookup, the formula (evaluated on the stored row, before $expand, with no relationship traversal) renders the related record's stored id where the titleFormat renderer on the expanding record page shows its name (e.g. 'usr_Ab12 in team_core' vs 'Ada Lovelace in Core'); so the triage claim that reading 1 'stays correct under either answer' to reading 2 holds for four of the ten, not six. The six are isolated in commit 3eb8abe and raised as the open question; the card assignee read os-sales and was not written; branch merged with main 338feda before opening the PR.",
"tests": "New packages/platform-objects/src/identity/identity-display-title.test.ts (29 cases): served pointer via provisionPrimary(schema, { synthesize: false }) (the call materializeBaseLayer makes) is display_title + mirror / user_id; display_title is formula + returnType text + isTitleEligible; ExpressionEngine.evaluate(expr, { now, record }) (the applyFormulaPlan call) equals the titleFormat rendering of a stored row and never contains the row id; sys_member null-role leg; sys_scim_subject pointer. Runs (os-verify-lock, VERDICT command-exit 0 each): platform-objects full suite at 141fcb8 'Test Files 55 passed (55) / Tests 912 passed (912)'; platform-objects typecheck (tsc x2 + check:test-typecheck) at 141fcb8 exit 0, 'check:test-typecheck: OK', tsc --listFiles -p tsconfig.test.json counts the new file once; consumer suites on c48a6ee (pre-merge; main's 17 commits touch none of these packages): plugin-auth full '114 passed / 2440 passed', plugin-security full '133 passed / 2648 passed', client (5 files importing the identity objects) '46 passed', runtime action-execution-destructive '66 passed', service-messaging recipient-locale-shape-parity '16 passed'. Integration/dogfood tiers declared to CI. Engine measurement (one-off script, real ObjectQL + SqlDriver better-sqlite3 :memory:, base files vs head files): served nameField id → display_title x9 / user_id x1; H1 raw id → titleFormat text x10; display_title column absent x9. Red at base (ten object files set to their 6696056 blobs, pin at HEAD): 'Tests 29 failed (29)', e.g. "expected { nameField: 'id', …(1) } to deeply equal { nameField: 'display_title', …(1) }"; restored, all ten blob == HEAD, git diff HEAD empty. Ablations at c48a6ee via scripts/ablation-replace.mjs WRAP mode inside an outer trap-restore script (relative source imports, no dist on the path, no rebuild): 9 pointer-line deletions red 1/29 each (the served-pointer case: with the pointer gone, derivation still picks display_title as a tier-2 '_title' affix, so only the displayNameField mirror goes red), sys_scim_subject pointer deletion red 1/29 ("expected { nameField: 'id', …(1) } to deeply equal { nameField: 'user_id', …(1) }"), Field.formula → Field.text red 1/29 ("expected 'text' to be 'formula'"), null guard dropped red 1/29 ("expected null to be 'usr_Cd34'"); every leg printed 'ok mutation landed' (anchor 1 → 0, blob changed) and the post-batch check printed 'all ten object files blob == HEAD'. Narrowed eslint: population = the 15 changed .ts files, none ignored (no 'ignored' notice in the json); --format json 15 files / 0 errors / 0 warnings; invariance: eslint.config.mjs never enables type-aware linting (its own statement near line 327), so this diff moves no verdict on an untouched file.",
"mcp_calls": "0 — no MCP GitHub tool was called (reads went through single-card REST GETs and a blobless git clone of objectui for record-title.ts / RecordDetailView.tsx)",
"api_writes": "3 — all through the fleet-write relay as objectstack-fleet[bot] (each one POST /repos/objectstack-ai/objectstack/dispatches from the seat): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft, PR #20095, run 36091830231); (2) label-write --assign os-sales → POST /repos//issues/20095/assignees (run 36091890054, read-back MATCHES); (3) this os-dev-report comment → POST /repos//issues/20059/comments. git push is not counted.",
"open_questions": [
{
"question": "Land the six lookup-bearing objects (sys_business_unit_member, sys_member, sys_scim_group_member, sys_scim_projection_grant, sys_team_member, sys_two_factor; commit 3eb8abe) now, or hold them for the maintainer's answer on reading 2 of #20044? Their formula title carries the related record's stored id ('usr_Ab12 in team_core'); the titleFormat on the expanding record page renders the related name ('Ada Lovelace in Core'). A formula cannot reach a related record (evaluated before $expand; formula/src/validate.ts: nothing hydrates traversal at a formula value). Without a pointer, the stamped id makes the H1 the raw record id; with reading 2 accepted and no pointer, the H1 would be the titleFormat's expanded render; with this pointer it is the FK-id text either way.",
"options": [
"A — land all ten as dispatched. Business need: replaces the raw record id with text naming both parties now, before the console pin moves past objectui#10358. Long-term: if reading 2 is accepted, six formula fields become worse than no pointer and would be removed again (six small edits plus translations). AI-safety: follows the titleFormat describe's own migration literally, declared = enforced. Startup focus: no new capability, no gate.",
"B — split: land the four (commit 7def641 plus tests/translations for them) and hold the six until the maintainer answers reading 2. Business need: those six keep the raw-id H1 once the pin moves, until the answer. Long-term: no churn whichever way reading 2 goes. AI-safety: neutral. Startup focus: least code now; costs a second PR and a split of the translation hunks.",
"C — for a composite that leads with a user lookup, point nameField at the lookup (the sys_session shape), so the renderer reduces the expanded row to the name ('Ada Lovelace'). Business need: best record-page text, but drops the second party; server-side resolveRecordTitle still answers the stored id. Long-term and AI-safety: departs from the describe (a composite goes to a formula) and designates a title-ineligible type for a composite, which is the misreading an author would copy. Startup focus: no new capability."
],
"recommendation": "A, because the dispatch ruled reading 1 without waiting on reading 2, it is the describe's own migration (AI-safety axis), and it is strictly better than the raw id the six show once the pin moves (business-need axis). The long-term cost if reading 2 lands is small and already shared by PR #20042's sys_approval_approver. The split is kept cheap (own commit) in case the seat weighs the reading-2 timing differently; the maintainer's reading-2 decision should know the triage claim 'correct under either answer' does not hold for lookup-bearing composites."
}
],
"out_of_scope_findings": [
"carrier: the #20044 dev and the maintainer's reading-2 decision · boundary, not class c (the describe and lint hint lead an author to metadata the runtime honours, but for a composite naming a lookup the formula title carries the stored id where the titleFormat renderer showed the related name; measured on six objects here, and the same shape exists in #20044's services-lane set and in PR #20042's sys_approval_approver) · noted in the PR's Acceptance notes, not filed",
"carrier: 承接者:无 · sys_verification.identifier describe says 'Email address or phone number', but better-auth writes 'reset-password:' plus the live reset token there (better-auth dist/api/routes/password.mjs); an incomplete description, not a reachable defect, and display_title repeats a column every read already serves · noted in Acceptance notes only",
"carrier: 承接者:无 · observation: a field named display_title is picked by the designate-only derivation even without the explicit pointer (tier-2 '_title' affix in resolveDisplayField) · noted in Acceptance notes only"
],
"gates": "Derived at 141fcb8 with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; change set 16 files vs merge base 338feda, no stale-tree warning after the merge): 60 commands, all run; --ran reconcile: 'Run reconciliation — 60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN' and '60 recorded an exit code and none of them is 3'. check:dual-build-cjs-loads first exit 3 (PREREQUISITE NOT MET: studio, client-react, embedder-openai, knowledge-ragflow, organizations, service-cluster-redis had no dist), re-run exit 0 after building them. The other 59 exited 0 on the first run, including check:platform-object-tenancy-census ('84 platform-namespace objects, 58 in the machinery's reach, 26 outside it'), check:system-context-census OK, check:keyed-text-bounds, check:i18n ('9 package(s) — all bundles in sync'), check:i18n-stale-fill, check:nul-bytes ('no raw ASCII control bytes'), check:test-source-alias OK, check:published-files, check:dts-closure, check:lean-entry-closure, check:type-check-coverage, check:type-check-debt, check:engine-double-contract. Also: GITHUB_TOKEN=... node scripts/check-issue-citations.mjs --base 338feda exit 0 ('no issue citations added against 338feda'); pnpm check:i18n-coverage exit 0 ('621 baselined untranslated string(s), none new', after building its prerequisite closure). CI: in_progress, not awaited.",
"line_budget": "n/a — no skills/** in the diff",
"deviations": [
"Merged main 338feda into the branch (merge commit 141fcb8, clean, delta vs main = the 16 files) before opening the PR, per AGENTS.md §10; rebuilt, re-ran the platform-objects suite + typecheck and all gates on the merged head; the consumer suites ran pre-merge on c48a6ee.",
"PR assignee set to the card's (os-sales) with one label-write --assign, per .claude/agents/os-dev.md as amended on main by 9b8c74c ('PR assignee = 卡的'). The os-dev.md loaded for this run predates that and reads 'never write assignee'; the dispatch allowed one label-write 'if a label is due'. No label was due, so the write carried the assignee only. Flagged as a conflict between the loaded text and main.",
"The permanent pin measures the served pointer with provisionPrimary (the exact call the registry's materializeBaseLayer makes) and the formula with ExpressionEngine.evaluate (the applyFormulaPlan call), not through a real ObjectQL registry: platform-objects has no objectql / driver-sql dependency, and adding them (package.json, lockfile, vitest alias) would breach the claimed file surface. The real-engine readings come from a one-off script and are in the PR body.",
"F is imported from '@objectstack/spec/shared' (the subpath platform-objects already uses), not the root entry PR #20042 used.",
"Commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By; the merge commit carries git's default message with no trailer pair."
],
"files_changed": [
".changeset/20059-identity-objects-title-formula.md",
"packages/platform-objects/src/apps/translations/en.objects.generated.ts",
"packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts",
"packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts",
"packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts",
"packages/platform-objects/src/identity/identity-display-title.test.ts",
"packages/platform-objects/src/identity/sys-account.object.ts",
"packages/platform-objects/src/identity/sys-business-unit-member.object.ts",
"packages/platform-objects/src/identity/sys-invitation.object.ts",
"packages/platform-objects/src/identity/sys-member.object.ts",
"packages/platform-objects/src/identity/sys-scim-group-member.object.ts",
"packages/platform-objects/src/identity/sys-scim-projection-grant.object.ts",
"packages/platform-objects/src/identity/sys-scim-subject.object.ts",
"packages/platform-objects/src/identity/sys-team-member.object.ts",
"packages/platform-objects/src/identity/sys-two-factor.object.ts",
"packages/platform-objects/src/identity/sys-verification.object.ts"
],
"objects": [
{
"object": "sys_account",
"titleFormat": "{provider_id} - {account_id}",
"migration": "formula",
"formula": "record.provider_id + ' - ' + record.account_id",
"lookup_placeholders": [],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
},
{
"object": "sys_business_unit_member",
"titleFormat": "{user_id} in {business_unit_id}",
"migration": "formula",
"formula": "record.user_id + ' in ' + record.business_unit_id",
"lookup_placeholders": [
"user_id",
"business_unit_id"
],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
},
{
"object": "sys_invitation",
"titleFormat": "Invitation for {email}",
"migration": "formula",
"formula": "'Invitation for ' + record.email",
"lookup_placeholders": [],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
},
{
"object": "sys_member",
"titleFormat": "{user_id} ({role})",
"migration": "formula",
"formula": "record.role != null ? record.user_id + ' (' + record.role + ')' : record.user_id",
"lookup_placeholders": [
"user_id"
],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
},
{
"object": "sys_scim_group_member",
"titleFormat": "{scim_user_id} in {group_id}",
"migration": "formula",
"formula": "record.scim_user_id + ' in ' + record.group_id",
"lookup_placeholders": [
"scim_user_id",
"group_id"
],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
},
{
"object": "sys_scim_projection_grant",
"titleFormat": "{role} → {user_id}",
"migration": "formula",
"formula": "record.role + ' → ' + record.user_id",
"lookup_placeholders": [
"user_id"
],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
},
{
"object": "sys_scim_subject",
"titleFormat": "{user_id}",
"migration": "single",
"formula": null,
"lookup_placeholders": [
"user_id"
],
"served_nameField_before": "id",
"served_nameField_after": "user_id"
},
{
"object": "sys_team_member",
"titleFormat": "{user_id} in {team_id}",
"migration": "formula",
"formula": "record.user_id + ' in ' + record.team_id",
"lookup_placeholders": [
"user_id",
"team_id"
],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
},
{
"object": "sys_two_factor",
"titleFormat": "Two-factor for {user_id}",
"migration": "formula",
"formula": "'Two-factor for ' + record.user_id",
"lookup_placeholders": [
"user_id"
],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
},
{
"object": "sys_verification",
"titleFormat": "Verification for {identifier}",
"migration": "formula",
"formula": "'Verification for ' + record.identifier",
"lookup_placeholders": [],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
}
],
"regeneration": {
"command": "node scripts/check-i18n-bundles.mjs --write --filter=platform-objects (run twice: once to extract, once after the hand translations, which returned the source-hash companions to their prior bytes)",
"files": [
"packages/platform-objects/src/apps/translations/en.objects.generated.ts",
"packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts",
"packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts",
"packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts"
],
"note": "zh-CN / ja-JP / es-ES label and help leaves (9 objects x 2 x 3 = 54 values) translated by hand; no other generated artefact reads these objects' fields"
}
}
domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-25T04:17Z. Reviewed on GitHub against references/review-checklist.md, not from the dev's os-dev-report.
Shape: the first line is Fixes #20059, and it is the only closing keyword. The body declares Clause-②: no, and the changeset grades @objectstack/platform-objectspatch.
Scope: 16 files, +553/−0: the ten identity object files, four translation bundles, one new test and the changeset. Not governed (check-governed-merges --pr 20095). It stays within the card's surface.
Reading 1 executed: nine objects declare a display_title formula that reproduces their titleFormat, and point nameField and displayNameField at it. sys_scim_subject points both at user_id directly. titleFormat stays on all ten.
CI at this head: 34 runs, 31 success and 3 skipped, and every required context is green. git merge-tree against current main is clean.
Out of scope, noted for a card: the auto-default $search set of some identity objects includes text columns that hold secrets. This predates the PR and is unchanged by it (the allowed set is identical at base and head).
Landing: ready plus auto-merge through the queue now.
The changeset 20059-identity-objects-title-formula.md is present at that commit and absent at its parent.
The squash message carries the PR body as corrected by the seat before landing.
This card: closed completed via Fixes #20059. pm:dispatched is removed in the same act. The ten identity objects now declare a title pointer: nine point at a display_title formula that reproduces their titleFormat, and sys_scim_subject points at user_id.
Activity
objectstack-fleet commentedon Sep 25, 2026
Claim: PM loop round 22
Session:
session_01Bvd69VPa6puiNzzPUroDBxBranch:
claude/issue-20059-identity-objects-title-formulaWorktree:
objectstack-issue-20059Domain:
domain:engineSeat:
domain:engine#1File surface:
packages/platform-objects/src/identity/: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-factorandsys-verification. Each is migrated per thetitleFormatdescribe: a single-field title moves tonameField; a composite becomes a text formula field designated asnameField, with thedisplayNameFieldmirror;packages/platform-objects(one pin per object);.changeset/20059-*.md.Stop on breach and explain in the report. ⛔ Not the nine services-lane objects (#20044,
domain:services). ⛔ Not the class-level alternative (reading 2 of #20044: the designation pass inpackages/objectql/src/registry.ts), which triage put to the maintainer as a contract decision. ⛔ Notpackages/spec.Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)Clause-②: no
Thread-read: none
Serial constraints cleared: at 2026-09-25T01:57Z, a census of the 22 open PRs finds none touching
packages/platform-objects/src/identity/, platform-objects translations, orpackages/objectql/src/registry.ts. The precedent is PR #20042 (#20015, landed7e6ca1787a).Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
os-dev-report
{
"issue": 20059,
"status": "done",
"branch": "claude/issue-20059-identity-objects-title-formula",
"pr": "#20095",
"session": "session_01Bvd69VPa6puiNzzPUroDBx — mode:subagent, so the parent's (the domain:engine PM seat's) id, per the role file; the newest Claim (5825427084) names this branch",
"premise_still_valid": true,
"summary": "H1 confirmed on a real ObjectQL registry + SqlDriver: at base 6696056 all ten objects were served nameField 'id' (no displayNameField), so the H1 under ADR-0079 order and resolveRecordTitle were the raw record id. Nine now declare display_title, a text formula reproducing their titleFormat (null-guarded on sys_member.role, the one nullable source column), with nameField and the displayNameField mirror pointing at it; sys_scim_subject has a single-field titleFormat ('{user_id}') and points nameField/displayNameField at user_id directly (the sys_session shape). titleFormat is kept on all ten, no stored column appears (measured on the synced SQLite table), $search and the __search companion are unchanged, and the object translations were regenerated with the repo tool and the 54 new zh-CN / ja-JP / es-ES values (9 objects x label + help) translated by hand. H3 is only partly true: for the six objects whose titleFormat names a lookup, the formula (evaluated on the stored row, before $expand, with no relationship traversal) renders the related record's stored id where the titleFormat renderer on the expanding record page shows its name (e.g. 'usr_Ab12 in team_core' vs 'Ada Lovelace in Core'); so the triage claim that reading 1 'stays correct under either answer' to reading 2 holds for four of the ten, not six. The six are isolated in commit 3eb8abe and raised as the open question; the card assignee read os-sales and was not written; branch merged with main 338feda before opening the PR.",
"tests": "New packages/platform-objects/src/identity/identity-display-title.test.ts (29 cases): served pointer via provisionPrimary(schema, { synthesize: false }) (the call materializeBaseLayer makes) is display_title + mirror / user_id; display_title is formula + returnType text + isTitleEligible; ExpressionEngine.evaluate(expr, { now, record }) (the applyFormulaPlan call) equals the titleFormat rendering of a stored row and never contains the row id; sys_member null-role leg; sys_scim_subject pointer. Runs (os-verify-lock, VERDICT command-exit 0 each): platform-objects full suite at 141fcb8 'Test Files 55 passed (55) / Tests 912 passed (912)'; platform-objects typecheck (tsc x2 + check:test-typecheck) at 141fcb8 exit 0, 'check:test-typecheck: OK', tsc --listFiles -p tsconfig.test.json counts the new file once; consumer suites on c48a6ee (pre-merge; main's 17 commits touch none of these packages): plugin-auth full '114 passed / 2440 passed', plugin-security full '133 passed / 2648 passed', client (5 files importing the identity objects) '46 passed', runtime action-execution-destructive '66 passed', service-messaging recipient-locale-shape-parity '16 passed'. Integration/dogfood tiers declared to CI. Engine measurement (one-off script, real ObjectQL + SqlDriver better-sqlite3 :memory:, base files vs head files): served nameField id → display_title x9 / user_id x1; H1 raw id → titleFormat text x10; display_title column absent x9. Red at base (ten object files set to their 6696056 blobs, pin at HEAD): 'Tests 29 failed (29)', e.g. "expected { nameField: 'id', …(1) } to deeply equal { nameField: 'display_title', …(1) }"; restored, all ten blob == HEAD, git diff HEAD empty. Ablations at c48a6ee via scripts/ablation-replace.mjs WRAP mode inside an outer trap-restore script (relative source imports, no dist on the path, no rebuild): 9 pointer-line deletions red 1/29 each (the served-pointer case: with the pointer gone, derivation still picks display_title as a tier-2 '_title' affix, so only the displayNameField mirror goes red), sys_scim_subject pointer deletion red 1/29 ("expected { nameField: 'id', …(1) } to deeply equal { nameField: 'user_id', …(1) }"), Field.formula → Field.text red 1/29 ("expected 'text' to be 'formula'"), null guard dropped red 1/29 ("expected null to be 'usr_Cd34'"); every leg printed 'ok mutation landed' (anchor 1 → 0, blob changed) and the post-batch check printed 'all ten object files blob == HEAD'. Narrowed eslint: population = the 15 changed .ts files, none ignored (no 'ignored' notice in the json); --format json 15 files / 0 errors / 0 warnings; invariance: eslint.config.mjs never enables type-aware linting (its own statement near line 327), so this diff moves no verdict on an untouched file.",
"mcp_calls": "0 — no MCP GitHub tool was called (reads went through single-card REST GETs and a blobless git clone of objectui for record-title.ts / RecordDetailView.tsx)",
"api_writes": "3 — all through the fleet-write relay as objectstack-fleet[bot] (each one POST /repos/objectstack-ai/objectstack/dispatches from the seat): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft, PR #20095, run 36091830231); (2) label-write --assign os-sales → POST /repos//issues/20095/assignees (run 36091890054, read-back MATCHES); (3) this os-dev-report comment → POST /repos//issues/20059/comments. git push is not counted.",
"open_questions": [
{
"question": "Land the six lookup-bearing objects (sys_business_unit_member, sys_member, sys_scim_group_member, sys_scim_projection_grant, sys_team_member, sys_two_factor; commit 3eb8abe) now, or hold them for the maintainer's answer on reading 2 of #20044? Their formula title carries the related record's stored id ('usr_Ab12 in team_core'); the titleFormat on the expanding record page renders the related name ('Ada Lovelace in Core'). A formula cannot reach a related record (evaluated before $expand; formula/src/validate.ts: nothing hydrates traversal at a formula value). Without a pointer, the stamped id makes the H1 the raw record id; with reading 2 accepted and no pointer, the H1 would be the titleFormat's expanded render; with this pointer it is the FK-id text either way.",
"options": [
"A — land all ten as dispatched. Business need: replaces the raw record id with text naming both parties now, before the console pin moves past objectui#10358. Long-term: if reading 2 is accepted, six formula fields become worse than no pointer and would be removed again (six small edits plus translations). AI-safety: follows the titleFormat describe's own migration literally, declared = enforced. Startup focus: no new capability, no gate.",
"B — split: land the four (commit 7def641 plus tests/translations for them) and hold the six until the maintainer answers reading 2. Business need: those six keep the raw-id H1 once the pin moves, until the answer. Long-term: no churn whichever way reading 2 goes. AI-safety: neutral. Startup focus: least code now; costs a second PR and a split of the translation hunks.",
"C — for a composite that leads with a user lookup, point nameField at the lookup (the sys_session shape), so the renderer reduces the expanded row to the name ('Ada Lovelace'). Business need: best record-page text, but drops the second party; server-side resolveRecordTitle still answers the stored id. Long-term and AI-safety: departs from the describe (a composite goes to a formula) and designates a title-ineligible type for a composite, which is the misreading an author would copy. Startup focus: no new capability."
],
"recommendation": "A, because the dispatch ruled reading 1 without waiting on reading 2, it is the describe's own migration (AI-safety axis), and it is strictly better than the raw id the six show once the pin moves (business-need axis). The long-term cost if reading 2 lands is small and already shared by PR #20042's sys_approval_approver. The split is kept cheap (own commit) in case the seat weighs the reading-2 timing differently; the maintainer's reading-2 decision should know the triage claim 'correct under either answer' does not hold for lookup-bearing composites."
}
],
"out_of_scope_findings": [
"carrier: the #20044 dev and the maintainer's reading-2 decision · boundary, not class c (the describe and lint hint lead an author to metadata the runtime honours, but for a composite naming a lookup the formula title carries the stored id where the titleFormat renderer showed the related name; measured on six objects here, and the same shape exists in #20044's services-lane set and in PR #20042's sys_approval_approver) · noted in the PR's Acceptance notes, not filed",
"carrier: 承接者:无 · sys_verification.identifier describe says 'Email address or phone number', but better-auth writes 'reset-password:' plus the live reset token there (better-auth dist/api/routes/password.mjs); an incomplete description, not a reachable defect, and display_title repeats a column every read already serves · noted in Acceptance notes only",
"carrier: 承接者:无 · observation: a field named display_title is picked by the designate-only derivation even without the explicit pointer (tier-2 '_title' affix in resolveDisplayField) · noted in Acceptance notes only"
],
"gates": "Derived at 141fcb8 with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; change set 16 files vs merge base 338feda, no stale-tree warning after the merge): 60 commands, all run; --ran reconcile: 'Run reconciliation — 60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN' and '60 recorded an exit code and none of them is 3'. check:dual-build-cjs-loads first exit 3 (PREREQUISITE NOT MET: studio, client-react, embedder-openai, knowledge-ragflow, organizations, service-cluster-redis had no dist), re-run exit 0 after building them. The other 59 exited 0 on the first run, including check:platform-object-tenancy-census ('84 platform-namespace objects, 58 in the machinery's reach, 26 outside it'), check:system-context-census OK, check:keyed-text-bounds, check:i18n ('9 package(s) — all bundles in sync'), check:i18n-stale-fill, check:nul-bytes ('no raw ASCII control bytes'), check:test-source-alias OK, check:published-files, check:dts-closure, check:lean-entry-closure, check:type-check-coverage, check:type-check-debt, check:engine-double-contract. Also: GITHUB_TOKEN=... node scripts/check-issue-citations.mjs --base 338feda exit 0 ('no issue citations added against 338feda'); pnpm check:i18n-coverage exit 0 ('621 baselined untranslated string(s), none new', after building its prerequisite closure). CI: in_progress, not awaited.",
"line_budget": "n/a — no skills/** in the diff",
"deviations": [
"Merged main 338feda into the branch (merge commit 141fcb8, clean, delta vs main = the 16 files) before opening the PR, per AGENTS.md §10; rebuilt, re-ran the platform-objects suite + typecheck and all gates on the merged head; the consumer suites ran pre-merge on c48a6ee.",
"PR assignee set to the card's (os-sales) with one label-write --assign, per .claude/agents/os-dev.md as amended on main by 9b8c74c ('PR assignee = 卡的'). The os-dev.md loaded for this run predates that and reads 'never write assignee'; the dispatch allowed one label-write 'if a label is due'. No label was due, so the write carried the assignee only. Flagged as a conflict between the loaded text and main.",
"The permanent pin measures the served pointer with provisionPrimary (the exact call the registry's materializeBaseLayer makes) and the formula with ExpressionEngine.evaluate (the applyFormulaPlan call), not through a real ObjectQL registry: platform-objects has no objectql / driver-sql dependency, and adding them (package.json, lockfile, vitest alias) would breach the claimed file surface. The real-engine readings come from a one-off script and are in the PR body.",
"F is imported from '@objectstack/spec/shared' (the subpath platform-objects already uses), not the root entry PR #20042 used.",
"Commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By; the merge commit carries git's default message with no trailer pair."
],
"files_changed": [
".changeset/20059-identity-objects-title-formula.md",
"packages/platform-objects/src/apps/translations/en.objects.generated.ts",
"packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts",
"packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts",
"packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts",
"packages/platform-objects/src/identity/identity-display-title.test.ts",
"packages/platform-objects/src/identity/sys-account.object.ts",
"packages/platform-objects/src/identity/sys-business-unit-member.object.ts",
"packages/platform-objects/src/identity/sys-invitation.object.ts",
"packages/platform-objects/src/identity/sys-member.object.ts",
"packages/platform-objects/src/identity/sys-scim-group-member.object.ts",
"packages/platform-objects/src/identity/sys-scim-projection-grant.object.ts",
"packages/platform-objects/src/identity/sys-scim-subject.object.ts",
"packages/platform-objects/src/identity/sys-team-member.object.ts",
"packages/platform-objects/src/identity/sys-two-factor.object.ts",
"packages/platform-objects/src/identity/sys-verification.object.ts"
],
"objects": [
{
"object": "sys_account",
"titleFormat": "{provider_id} - {account_id}",
"migration": "formula",
"formula": "record.provider_id + ' - ' + record.account_id",
"lookup_placeholders": [],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
},
{
"object": "sys_business_unit_member",
"titleFormat": "{user_id} in {business_unit_id}",
"migration": "formula",
"formula": "record.user_id + ' in ' + record.business_unit_id",
"lookup_placeholders": [
"user_id",
"business_unit_id"
],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
},
{
"object": "sys_invitation",
"titleFormat": "Invitation for {email}",
"migration": "formula",
"formula": "'Invitation for ' + record.email",
"lookup_placeholders": [],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
},
{
"object": "sys_member",
"titleFormat": "{user_id} ({role})",
"migration": "formula",
"formula": "record.role != null ? record.user_id + ' (' + record.role + ')' : record.user_id",
"lookup_placeholders": [
"user_id"
],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
},
{
"object": "sys_scim_group_member",
"titleFormat": "{scim_user_id} in {group_id}",
"migration": "formula",
"formula": "record.scim_user_id + ' in ' + record.group_id",
"lookup_placeholders": [
"scim_user_id",
"group_id"
],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
},
{
"object": "sys_scim_projection_grant",
"titleFormat": "{role} → {user_id}",
"migration": "formula",
"formula": "record.role + ' → ' + record.user_id",
"lookup_placeholders": [
"user_id"
],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
},
{
"object": "sys_scim_subject",
"titleFormat": "{user_id}",
"migration": "single",
"formula": null,
"lookup_placeholders": [
"user_id"
],
"served_nameField_before": "id",
"served_nameField_after": "user_id"
},
{
"object": "sys_team_member",
"titleFormat": "{user_id} in {team_id}",
"migration": "formula",
"formula": "record.user_id + ' in ' + record.team_id",
"lookup_placeholders": [
"user_id",
"team_id"
],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
},
{
"object": "sys_two_factor",
"titleFormat": "Two-factor for {user_id}",
"migration": "formula",
"formula": "'Two-factor for ' + record.user_id",
"lookup_placeholders": [
"user_id"
],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
},
{
"object": "sys_verification",
"titleFormat": "Verification for {identifier}",
"migration": "formula",
"formula": "'Verification for ' + record.identifier",
"lookup_placeholders": [],
"served_nameField_before": "id",
"served_nameField_after": "display_title"
}
],
"regeneration": {
"command": "node scripts/check-i18n-bundles.mjs --write --filter=platform-objects (run twice: once to extract, once after the hand translations, which returned the source-hash companions to their prior bytes)",
"files": [
"packages/platform-objects/src/apps/translations/en.objects.generated.ts",
"packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts",
"packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts",
"packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts"
],
"note": "zh-CN / ja-JP / es-ES label and help leaves (9 objects x 2 x 3 = 54 values) translated by hand; no other generated artefact reads these objects' fields"
}
}
objectstack-fleet commentedon Sep 25, 2026
ACCEPT: PR #20095 at
141fcb8cdomain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-25T04:17Z. Reviewed on GitHub againstreferences/review-checklist.md, not from the dev'sos-dev-report.Fixes #20059, and it is the only closing keyword. The body declaresClause-②: no, and the changeset grades@objectstack/platform-objectspatch.check-governed-merges --pr 20095). It stays within the card's surface.display_titleformula that reproduces theirtitleFormat, and pointnameFieldanddisplayNameFieldat it.sys_scim_subjectpoints both atuser_iddirectly.titleFormatstays on all ten.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 (comment 5826403791).141fcb8c(5826600669).idto the title on 10/10, and the title equals thetitleFormatrendering on stored rows.$searchallowed set is identical at base and head on all ten.patch/nois right.orderByandwhereondisplay_titleare refused on the nine formula objects, whereidwas accepted. No in-repo consumer sorts or filters by the pointer. fix(plugin-approvals, service-automation, service-messaging): title five system objects with a text formula instead of the raw id #20042 and 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 carry the same property. objectui's default sort is flagged for the console pin move past objectui#10358.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 remains open"; "That merge touched none of these packages"). Both are rewritten in the body. Neither changes the shipped bytes.successand 3skipped, and every required context is green.git merge-treeagainst currentmainis clean.$searchset of some identity objects includes text columns that hold secrets. This predates the PR and is unchanged by it (the allowed set is identical at base and head).Landing:
readyplus auto-merge through the queue now.objectstack-fleet commentedon Sep 25, 2026
Landed: PR #20095, verified on
maindomain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-25T04:39Z.d624002eb9, which has one parent and is an ancestor oforigin/mainafter a fresh fetch. The diff is 16 files, +553/−0, exactly the reviewed PR at141fcb8c(PASS 5826600669).20059-identity-objects-title-formula.mdis present at that commit and absent at its parent.completedviaFixes #20059.pm:dispatchedis removed in the same act. The ten identity objects now declare a title pointer: nine point at adisplay_titleformula that reproduces theirtitleFormat, andsys_scim_subjectpoints atuser_id.orderByandwhereondisplay_titleare refused on the nine formula objects, the same property as fix(plugin-approvals, service-automation, service-messaging): title five system objects with a text formula instead of the raw id #20042 and 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. objectui's default sort is flagged for the console pin move past objectui#10358.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 is ever accepted, they would read better with no pointer; the cost is recorded at 5826403791.packages/platform-objects/src/identity/(the ten titled objects) has no queued neighbour.fix(platform-objects): title ten identity objects by a declared point…