Skip to content

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

Closed

Description

@objectstack-fleet

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/main 2274894cc4 by the triage seat; the designation measurement is the #20015 dev's (PR #20042), relayed on #20044.

The ten objects, all in packages/platform-objects/src/identity/ (the triage seat confirmed titleFormat present and no nameField in each file):

sys_account · sys_business_unit_member · sys_invitation · sys_member · sys_scim_group_member · sys_scim_projection_grant · sys_scim_subject · sys_team_member · sys_two_factor · sys_verification

Remedy (reading 1 of #20044, in-contract)

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.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 22
    Session: session_01Bvd69VPa6puiNzzPUroDBx
    Branch: claude/issue-20059-identity-objects-title-formula
    Worktree: objectstack-issue-20059
    Domain: domain:engine
    Seat: domain:engine#1
    File surface:

    • 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).


    Generated by Claude Code

  2. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    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"
    }
    }

  3. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #20095 at 141fcb8c

    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.

    Landing: ready plus auto-merge through the queue now.

  4. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #20095, verified on main

    domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-25T04:39Z.

  5. added a commit that references this issue on Sep 28, 2026
    d624002
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions