Skip to content

sys_business_unit_member: the record title shows raw ids (user id in business unit id) instead of the person's and the unit's names #26195

Closed

Description

@objectstack-fleet

Filing gate ②: a maintainer ruling, carried out as child 9 of the nine that #26059's ruling (Q3) names. ⛔ Not a claim.

The Title column in Setup → Business Unit Members, and the record page heading, read " in ": two opaque ids, with no person or department name. This is not a rendering bug. The object's title is declared as a formula over the two stored lookup ids, so the title is the ids by definition. It needs to be built from the two lookups' display names instead.

Part of #26059
Ruling-ref: 6416233738 (#26059 (comment))
Ruling: batch #396 item 1, letter Q1 是 / Q2 不做 / Q3 分拆 / Q4 框架. This card is Q3 child 9 (server, p3): item 8. Brief 6415795834, item-by-item reading 6415971852 (item 8).
Filed by domain:ui seat 2 (domain:ui#2) · os-steve · session session_01SMps7wq7rBTk53e8KTSEup, first claimant of #26059. Domain, grade and route are triage's on first touch.

Business reason. An admin checking who belongs to which department reads a column of ids and has to open each row to find out. Any other surface that names a membership record by its title shows the same ids.

Reader who acts. The domain:server execution seat, dispatch step: one os-dev, one PR in packages/platform-objects/src/identity/. The PR touches the formula runtime or a hook only if the mechanism chosen below needs it.

Spec contract. None is expected to change. If the chosen mechanism needs a formula capability that does not exist, say so on this card before building. Do not widen @objectstack/formula silently.

Duplicate check. A local scan of the 1,962 newest issues (all states, #22168–#26181) for sys_business_unit_member.{0,200}(title|display)|display_title.{0,100}business_unit found 0 hits.

Ruled scope (ruling Q3 item 9)

  • sys_business_unit_member's title is built over the two lookups' display names instead of record.user_id + ' in ' + record.business_unit_id (sys-business-unit-member.object.ts:38, :52–55).
  • The dev names the mechanism first. Either a formula over related display names, or a stamped title kept up to date by a hook on both ids. The reading did not find a lookup-display helper in packages/formula/src/stdlib.ts.

Anchors (re-read on origin/main 83a26c38)

  • packages/platform-objects/src/identity/sys-business-unit-member.object.ts:
    • :38 titleFormat: '{user_id} in {business_unit_id}' holds.
    • :52–58 is the display_title formula field; the expression F`record.user_id + ' in ' + record.business_unit_id` is at :55. The ruling's :52–55 holds.
    • The file's own comment at :33–35 already states the limit: "a formula is evaluated on the stored row, before $expand, and cannot reach a related record's own title". That confirms the mechanism has to be named before any code.
  • packages/platform-objects/src/identity/identity-display-title.test.ts holds titleFormat and display_title to the same text, so both move together.
  • packages/formula/src/stdlib.ts: no lookup-display helper found (re-checked).

Expected / acceptance

  • The mechanism is named on this card before code, with its cost on writes and reads.
  • The list Title column and the record heading show "<user's name> in <business unit's name>".
  • The title follows a change of either user_id or business_unit_id. A renamed user or unit shows the new name; if the mechanism is a stamp, the PR says how far that goes.
  • titleFormat and display_title still agree (identity-display-title.test.ts). A test covers the named title.

Activity

  1. added
    bugSomething isn't working
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Oct 26, 2026
  2. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — as filed · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-26T16:04Z

    ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · card thread
    Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01KCZSeNY7z5jAXn3EyHdZww
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-6rh8kd (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#2
    Ruling-ref: the maintainer's ruling on #26059 (6416233738, Q3 child 9) and triage audit 6420804912, dispatched under 「p1 p2 处理完了,就可以处理 p3」 (2026-10-26T10:29Z) — sys_business_unit_member's title is built over the two lookups' display names; the mechanism is named on the card with its cost before any code; ⛔ no silent widening of @objectstack/formula (a missing capability goes to needs_decision)
    File surface: packages/platform-objects/src/identity/sys-business-unit-member.object.ts, identity-display-title.test.ts and the identity tests, a hook in the identity package only if the named mechanism is a stamp, one changeset. ⛔ No packages/spec/**, ⛔ no packages/formula/** without a ruling (stop on breach; explain in the report)
    Container & model: M, bug, mode:thread, default model and effort (set at thread open)
    Clause-②: no (expected; the dev measures on the real diff)
    Metadata-protocol: none
    Responsibility: graded as filed by triage 6420804912; a child of #26059 under ruling 6416233738
    Thread-read: 6420804912
    Premise: holds (read 2026-10-26T18:53Z at origin/main c49688601d: card open, unassigned, pm:queue, no claim on the thread)
    Resume-from: none — fresh start from origin/main
    Serial constraints cleared: no open PR known to the seat touches sys-business-unit-member.object.ts at this act; the dev re-measures every open PR on packages/platform-objects/src/identity/** before the first edit and before pr_create


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 26, 2026
  5. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Mechanism, named before code (card thread session_01KCZSeNY7z5jAXn3EyHdZww, branch claude/project-thread-6rh8kd)

    Choice: a formula over the related display names, read ONE hop through each lookup. No stamp, no hook, no new formula capability.

    It already exists; nothing in @objectstack/formula is widened. The one-hop formula read is on main since #24197 (ADR-0079 addendum 2026-10-18, items 1 to 4): record.FIELD.RELATED through a lookup is preloaded by the engine read path (find, findOne, the insert and update responses; packages/objectql/src/engine.ts formulaHopScope / resolveOneHopRelated). The comment at sys-business-unit-member.object.ts:33–35 ("a formula ... cannot reach a related record's own title") predates it and is stale. A sibling join object already titles itself this way: sys_user_permission_set.display_title (packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts:77), pinned by sys-user-permission-set-person-title.test.ts. I reuse that shape:

    (isBlank(trim(record.user_id.name)) ? record.user_id.email : record.user_id.name) + ' in ' + record.business_unit_id.name

    name is the declared title of both sys_user and sys_business_unit (nameField: 'name', a stored required text column, so no second hop). The email arm is the platform's name || email rule for a person, the same one the sibling uses and the one the console's own embedded-lookup display applies.

    Cost on writes: none. No stored column, no hook, no extra statement on insert or update of the membership, the user or the unit.

    Cost on reads: two batched reads per page (one sys_user, one sys_business_unit, $in over the page's ids), never one per row, under the reader's own permissions. A findOne is two extra point reads. Reads that do not project display_title pay nothing.

    Rename reach: total and immediate. Nothing is stamped, so a renamed user or unit shows the new name on the next read everywhere the title is served.

    Known limits (already true today, unchanged): the title is not sortable or searchable (ADR-0079 addendum item 9; it is a formula over ids today and was already neither). A user or unit the reader may not read, or one that no longer exists, leaves the title null (addendum item 2), and the renderer falls to titleFormat.

    titleFormat and display_title keep agreeing. titleFormat stays {user_id} in {business_unit_id}: the console renders a lookup placeholder by the embedded record's display name (name, else email), so over an expanded row it renders the same text as the formula. identity-display-title.test.ts will assert that over the row the hop serves.

    Proceeding to red-first measurement and the change.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 26195,
      "status": "done",
      "branch": "claude/project-thread-6rh8kd",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/26300",
      "session": "session_01KCZSeNY7z5jAXn3EyHdZww",
      "premise_still_valid": true,
      "summary": "sys_business_unit_member.display_title now reads the user's name (email where blank) and the unit's name one hop through each lookup: (isBlank(trim(record.user_id.name)) ? record.user_id.email : record.user_id.name) + ' in ' + record.business_unit_id.name. Mechanism named on the card first (comment 6424698289): the existing one-hop formula read (#24197, ADR-0079 addendum 2026-10-18), the same shape sys_user_permission_set already ships; no formula or spec change, no stored column, no hook. Write cost none; read cost one batched sys_user and one batched sys_business_unit read per page; renames reach every read immediately. titleFormat stays {user_id} in {business_unit_id} and agrees with the formula over an expanded row (the console draws a lookup placeholder by name, else email); identity-display-title.test.ts asserts that, and the stale object comment saying a formula cannot reach a related title is rewritten.",
      "tests": "Live REST red/green (pnpm dev --fresh showcase, seeded admin, POST a membership, then GET /api/v1/data/sys_business_unit_member and GET .../ID): base c4968860 serves display_title '7eLi5ISndhNgrM0zdFvHtlv24DnJBmQK in bu_acme' on both; branch 81074800 serves 'Dev Admin in Acme Corporation' on both; base file restored via git checkout HEAD (blob hash matched, git diff HEAD empty) and rebuilt. New plugin-security/src/business-unit-member-title.test.ts (real ObjectQL + SqlDriver + real sys_user/sys_business_unit/membership): base 7 failed | 1 passed (Received 'simLa1JzYHiOZOF5 in VJG4H1tG-WmtXsHS'), branch 8 passed (8). At 81074800: platform-objects test 96 files / 1361 passed; plugin-security 228 files / 4934 passed / 45 skipped; plugin-sharing 55 / 1233 passed; plugin-approvals 98 / 1276 passed (each exit 0); platform-objects + plugin-security typecheck exit 0 (VERDICT command-exit 0); eslint --no-inline-config on the 3 touched files exit 0. No ablation beyond the base red run (the behaviour change is the formula itself, measured red on base through both the engine test and the REST door).",
      "gates": "Local at 81074800: dispatch-gates --commands derived 67 families, all 67 exit 0 (check:i18n and check:dual-build-cjs-loads first exit 3 PREREQUISITE NOT MET, re-run exit 0 after a full build; check-plugin-teardown-shape --self-test first exit 3 on the shallow clone, exit 0 after git fetch of its positive-control 621a4876). --ran reconciliation: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN. PR #26300 check-runs: in_progress at report time; the seat reads CI.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0 — no MCP GitHub calls; reads used gh api REST (issue, comments, open-PR file lists)",
      "api_writes": "3 relay writes + 1 git push — comment #26195 6424698289 (mechanism, post-stamped.mjs), pr_create #26300 draft with assignee leg os-project-manager (fleet-write dispatch.mjs), this os-dev-report comment (post-stamped.mjs); git push -u origin claude/project-thread-6rh8kd",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed — file surface: the claim named the object, identity-display-title.test.ts and 'the identity tests'; the engine-level test lives in packages/plugins/plugin-security/src/business-unit-member-title.test.ts because platform-objects has no objectql/driver-sql dependency (beside the sibling sys-user-permission-set-person-title.test.ts). Declared in the PR Acceptance notes."
      ]
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    2026-10-26T20:18Z · domain:server seat 2 · ACCEPT → PR #26300 comment 6426361869 (landing through the queue).


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed

    2026-10-26T20:37Z · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo. PR #26300 merged at 2026-10-26T20:36Z by the merge queue as 1f40201977 on main (Fixes #26195, Part of #26059; ACCEPT 6426361869). The merge closed this card as completed.

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-26300-*' 0 refs (left the queue)
    Ancestry git merge-base --is-ancestor 1f40201977 origin/main holds
    Card state GET /repos/objectstack-ai/objectstack/issues/26195 closed, completed

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions