Skip to content

fix(platform-objects): name a team membership by the person and the team, and give a team's Members list its own columns - #30452

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-d6xn5e
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-d6xn5e

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb

维护者速读

  • 改了什么:团队成员记录的标题从「用户 id in 团队 id」改为「用户名 in 团队名」(用户名为空时用邮箱),写法与业务单元成员一致;团队页的成员列表声明自己的列(用户、加入时间),不再以标题列打头;用户的团队列表同样声明(团队、加入时间)。
  • 为什么改:Setup 打开一个团队,成员列表首列是两串原始 id,宽屏右侧成员卡片的每一行、成员记录自己的页头也一样,管理员看不出是谁。
  • 风险与回滚:只改平台自带对象 sys_team_member 的声明,不存列、不迁移数据;标题按读者权限读一跳,读不到用户或团队时标题为空而不是显示 id。回滚即撤本 PR。
  • 先看:packages/platform-objects/src/identity/sys-team-member.object.ts 的 display_title 公式与两个 lookup 上的 relatedListColumns;identity-display-title.test.ts 新增的 sys_team_member 描述块。
  • 两分钟验证:pnpm --filter @objectstack/platform-objects exec vitest run src/identity/identity-display-title.test.ts src/identity/sys-team-derived-related-lists.test.ts,预期 40 条全绿;在 main 的字节上其中 6 条红。
  • 需要你做的:无,席位落地。

Fixes #30421

Clause-②: no — a platform object's shipped declaration: no exported member, accepted value or authorable key changes; what a read serves for the computed display_title changes from two ids to two names
Metadata-protocol: none — shipped platform metadata, not what a metadata author writes
Skills-docs: none — git grep -e sys_team_member -e relatedListColumns -- 'skills/**' hits skills/objectstack-data/rules/relationships.md:329,350 and skills/objectstack-ui/rules/record-page.md:22, which describe relatedListColumns in general and stay true

What changes

The card's Expected: a team page's members list shows the user plus a useful second column such as when they joined, and no column of raw ids; the same for a user page's teams list.

The seat's ruling, corrected mid-card. The order ruled the producer declaration (relatedListColumns) and ruled out a formula title that reads related titles, as a change to how formula titles compute. Measured before the change: that capability already ships (ADR-0079 addendum 2026-10-18, a formula title reads one hop through a reference), and sys_business_unit_member and sys_user_permission_set already use it; and relatedListColumns reaches only the table, while the record page's related column draws each member row titled by the record title (objectui RelatedList.tsx, compact card: getRecordDisplayName). The seat then ruled, verbatim:

席位 5 裁定 #30421:测量成立,我更正原裁定的理由。本 PR 一并修标题公式。display_title 改为经 lookup 读一跳的名字,写法与 sys_business_unit_member 一致(ADR-0079 2026-10-18 附录),这样紧凑卡片、成员记录 H1 和引用处都不再显示原始 id。lookup 读不到名字时(用户或团队已删除、无读权限),回落的写法也照 sys_business_unit_member 的现有做法,不要自造。relatedListColumns 两处保留(表格列)。

1. The record title (sys-team-member.object.ts, display_title). It was record.user_id + ' in ' + record.team_id, the two stored ids. It is now the expression sys_business_unit_member uses, with the team in place of the unit: the user's name, or their email where the name is blank, then ' in ', then the team's name, each read one hop through its lookup. Both hops are read as the reader (the engine's caller authority): a user or team the reader may not read, or one that no longer exists, leaves the title null, never an id. Neither lookup is read bare beside its hop; sys_team.name is required stored text, so there is no second hop. The field's label and description are unchanged, so no translation bundle moves. This is what the members panel in the record page's related column, a membership's own H1, and every reference to the row now show.

2. The team page's Members list (team_id lookup). A derived related list with no authored columns leads with the child's record title (ADR-0079, objectui leadWithNameField) and then its highlightFields. The reader of the relationship's columns is objectui's deriveRelatedLists (ui/packages/app-shell/src/utils/deriveRelatedLists.ts, relatedListColumns on the FK field becomes the list's columns), and RelatedList draws authored columns as written, with no name lead, dropping only the key back to the parent. team_id now declares relatedListColumns: ['user_id', 'created_at']: the list shows the user and when the membership began. Kept beside the title fix: with names, the led Title column would still repeat the User column and the team the page is about.

3. A user's Teams list (user_id lookup) declares relatedListColumns: ['team_id', 'created_at'], the ruled pair. It is latent in Setup today: sys_user_detail is an authored kind: 'full' page, which draws only the lists it names, and it names no team memberships. It applies on a user page that derives its lists.

Changeset: @objectstack/platform-objects patch, both declaration lines.

Tests

  • identity-display-title.test.ts (the shared title harness): the sys_team_member row now carries the related rows the read path preloads and is titled Ada Lovelace in Core Platform, still equal to the retired template's text and never containing an id; a new describe pins that the formula reads both lookups only through the hop (team_id.name, user_id.email, user_id.name), that a blank name falls back to the email, and that a lookup carrying no related row leaves the title null (three spellings: both, either one).
  • sys-team-derived-related-lists.test.ts (the shared team related-list harness): a new top-level describe pins both lookups' relatedListColumns, that neither names the record title, that every column is a field of the member row, and that neither names the key the list drops.

Acceptance notes

  • Other platform join objects whose formula title reads a lookup's stored id, measured on packages/platform-objects/src/**: sys_member (USER_ID (role); both parents' pages are authored and name their columns, so no list leads with it), sys_two_factor (Two-factor for USER_ID; its parent's page is the authored sys_user_detail, which does not list it), sys_scim_projection_grant and sys_scim_group_member (no Setup navigation reaches sys_scim_user or sys_scim_group, and no authored page lists them). None is a related list a Setup page draws, so none is changed here (the order's rule); their own record titles still read ids, reported to the seat.
  • The membership's title now depends on the reader: a reader who may read the membership but not the user or the team sees an empty title. That is the rule sys_business_unit_member already follows, by the seat's ruling.

Verification

All readings on head d2e37144 (the fix's five commits plus one merge of main at fdd06415, which touched no file here and none of the package's tests), except the per-step red and ablation legs, each named with its commit.

Red on main's bytes, green, ablation
red (main's sys-team-member.object.ts restored over the tree only, tests at d2e37144):
  vitest run identity-display-title.test.ts sys-team-derived-related-lists.test.ts
  -> Tests 6 failed | 34 passed (40): the four sys_team_member title cases, both relatedListColumns cases
  restored with git checkout HEAD; blob == HEAD (39e32e6eb25e), git status clean
green (d2e37144): same two files -> Test Files 2 passed (2), Tests 40 passed (40)
ablation 1 (on f8594ce9): ablation-replace --delete "relatedListColumns: ['user_id', 'created_at'],"
  anchor x1 -> x0 -> Tests 1 failed | 6 passed (7) (the team_id case); restored, blob == HEAD, git diff HEAD empty
ablation 2 (on d2e37144): ablation-replace, the hop expression -> "record.user_id + ' in ' + record.team_id"
  anchor x1 -> x0, replacement x0 -> x1 -> Tests 4 failed | 29 passed (33); restored, blob == HEAD, git diff HEAD empty

Both legs read src/ directly (relative imports), so no build stands between the mutation and the run.

The title a read serves, through the real engine (one-off measurement, not committed)

ObjectQL + SqlDriver (better-sqlite3 in memory) over the built dist/ of @objectstack/objectql, @objectstack/driver-sql and this package at d2e37144, registering SysUser, SysTeam and SysTeamMember, the setup of plugin-security/src/business-unit-member-title.test.ts:

PASS registered nameField: "display_title"
PASS registered relatedListColumns team_id: ["user_id","created_at"]
PASS registered relatedListColumns user_id: ["team_id","created_at"]
PASS write response title: "Ada Lovelace in Core Platform"
PASS findOne title: "Ada Lovelace in Core Platform"
PASS row keeps the keys: [true,true]
PASS servedRecordTitle: "Ada Lovelace in Core Platform"
PASS projected find (list Title column): [{"display_title":"Ada Lovelace in Core Platform"}]
PASS follows a team rename: "Ada Lovelace in Core Platform Team"
PASS blank name falls back to email: "grace@example.com in Core Platform Team"
PASS reader who may read both: "Ada Lovelace in Core Platform Team"
PASS reader who may not read the user: title null: null
PASS no hidden name leaks into the row: false
PASS no raw id in the title: false
MEASURE: all checks hold

The engine logged its one WARN formula field 'display_title' … could not be evaluated, so it reads null for the unreadable-user read, as designed.

Suites, typecheck, lint
@objectstack/platform-objects (d2e37144): Test Files 110 passed (110), Tests 1519 passed (1519)
@objectstack/platform-objects typecheck: exit 0 (check:test-typecheck OK, debt unchanged: 1 file / 3 errors)
suites outside the package that read sys_team_member, against this package's rebuilt dist:
  plugin-auth better-auth-schema-parity + managed-extension-fields: 58 passed
  plugin-security default-row-scope-declarations + rbac-objects + shipped-sets-identity-default-row-scope + business-unit-member-title: 191 passed
  plugin-sharing recipient-reads-complete: 16 passed
  plugin-approvals team-member-org-screen + team-position-holders-complete.integration + business-unit-holders-complete.integration: 37 passed
  runtime action-execution-destructive: 57 passed
pnpm lint (eslint . --no-inline-config): exit 0; the three changed source files re-linted on the clean tree: exit 0
Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on d2e37144: 58 commands; each run, exit code recorded before any pipe. --ran: 58 derived, 57 run, 1 NOT-MEASURED, 0 UNRUN.

  • 57 exit 0, pnpm check:i18n among them (after building its declared prerequisite closure).
  • NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3 (prerequisite: every workspace package's dist/; this tree has no full build). CI's Lint & Repo Gates carries it.
  • node scripts/check-plugin-teardown-shape.mjs --self-test: exit 0 after fetching its control commit into this shallow clone.
  • The four artifact-roster gates the derivation flags as keeping their roster in a directory this diff touches, run as well: check:authz-resolver, check:changeset-fixed, check:error-code-casing, check:filter-alias-parity, all exit 0.

Dogfood: not owed (no ! in the title, no (narrowing) arm).


Generated by Claude Code

…lists (red)

The team page's Members list leads with the member row's record title, a
formula over the two lookup ids, because the relationship authors no
columns. Pin the columns both lookups should declare.

Claude-Session: https://claude.ai/code/session_01Utp6CaBLFhcLHEkCSSBNMM
Co-authored-by: Claude <noreply@anthropic.com>
…d of leading with two raw ids

sys_team_member's record title is a formula over its two lookup ids, and a
related list with no authored columns leads with the record title, so
Setup's team page opened its Members list on a "USER_ID in TEAM_ID"
column. Declare relatedListColumns on both lookups: the team's list shows
the user and when they joined, a user's list the team and when.

Claude-Session: https://claude.ai/code/session_01Utp6CaBLFhcLHEkCSSBNMM
Co-authored-by: Claude <noreply@anthropic.com>
… the team (red)

The title reads the two stored lookup ids, so a team's Members panel,
a membership's H1 and every reference to the row name it by two raw ids.
Pin the one-hop title sys_business_unit_member already uses: the user's
name (else email) and the team's name, null when a hop is unresolved.

Claude-Session: https://claude.ai/code/session_01Utp6CaBLFhcLHEkCSSBNMM
Co-authored-by: Claude <noreply@anthropic.com>
…eam, not two stored ids

display_title composed the two lookup ids, so a team's Members panel,
a membership's H1 and every reference to the row read USER_ID in TEAM_ID.
Read the user's name (else email) and the team's name one hop through the
lookups, as sys_business_unit_member's title does (ADR-0079 addendum).

Claude-Session: https://claude.ai/code/session_01Utp6CaBLFhcLHEkCSSBNMM
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/platform-objects, touching 3 documentable anchor(s).

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

  • content/docs/api/error-catalog.mdx (via user_id (literal, a string literal in SysTeamMember))
  • content/docs/data-modeling/drivers.mdx (via user_id (literal, a string literal in SysTeamMember))
  • content/docs/deployment/cli.mdx (via user_id (literal, a string literal in SysTeamMember))
  • content/docs/kernel/runtime-services/audit-service.mdx (via user_id (literal, a string literal in SysTeamMember))
  • content/docs/permissions/authentication.mdx (via team_id (literal, a string literal in SysTeamMember), user_id (literal, a string literal in SysTeamMember))
  • content/docs/permissions/delegated-administration.mdx (via user_id (literal, a string literal in SysTeamMember))
  • content/docs/permissions/permission-sets.mdx (via user_id (literal, a string literal in SysTeamMember))
  • content/docs/permissions/positions.mdx (via user_id (literal, a string literal in SysTeamMember))
  • content/docs/permissions/record-view-auditing.mdx (via user_id (literal, a string literal in SysTeamMember))
  • content/docs/permissions/system-context.mdx (via user_id (literal, a string literal in SysTeamMember))
  • content/docs/protocol/kernel/config-resolution.mdx (via user_id (literal, a string literal in SysTeamMember))

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

  • content/docs/releases/v17/17-5.mdx (via user_id (literal, a string literal in SysTeamMember))

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
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 43 pages)
  • 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 — 2 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 df4c06e6b165bb0a30eb86e4ace6258932f69d8a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3baee4d2bb012689bd4ebea526501b0846ea27be — the merge of head d2e37144b76d5e558fc3c20388832b1fe54e4534 into base df4c06e6b165bb0a30eb86e4ace6258932f69d8a, 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 3baee4d2bb012689bd4ebea526501b0846ea27be && git checkout 3baee4d2bb012689bd4ebea526501b0846ea27be
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin df4c06e6b165bb0a30eb86e4ace6258932f69d8a d2e37144b76d5e558fc3c20388832b1fe54e4534 && git checkout -B drift-repro df4c06e6b165bb0a30eb86e4ace6258932f69d8a && git merge --no-ff d2e37144b76d5e558fc3c20388832b1fe54e4534

node scripts/docs-audit/affected-docs.mjs --json df4c06e6b165bb0a30eb86e4ace6258932f69d8a

⚠️ 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 df4c06e6b165bb0a30eb86e4ace6258932f69d8a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb

席位核对 · ACCEPT

Head d2e37144b · 2026-11-05T12:01Z

  • Scope matches the order (dispatch-30421.md with the 11:10Z amendment): team_id declares relatedListColumns: ['user_id', 'created_at'] and user_id declares ['team_id', 'created_at']. display_title reads one hop through each lookup and uses the same expression shape as sys_business_unit_member.object.ts:63. A blank name falls back to email, and an unreadable user or team gives null.
  • Governed: check-governed-merges.mjs --pr 30452 reports NOT governed (+133 / −11).
  • Declarations: Clause-②: no, Metadata-protocol: none and Skills-docs: none are consistent with the patch changeset. Check Changeset, the PR-body guards and the Governed Surface Queue Guard are green.
  • Tests: identity-display-title.test.ts and sys-team-derived-related-lists.test.ts pin the new title and both column sets.
  • Left open: the titles of the sibling join objects (sys_member, sys_two_factor and two SCIM objects) are out of scope and go to a follow-up card. Team page: a hand-made team reads as managed by the identity provider, member rows are titled with raw ids, and the count is stale #30337 (the ui half) stays open.

Marking ready and arming auto-merge. The remaining CI runs in the queue.

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/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Setup team page: the members list's Title column reads 'USER_ID in TEAM_ID' beside the User column (server half of #30337)

1 participant