Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .changeset/16270-org-record-tab-strip-provenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
'@objectstack/platform-objects': patch
---

Name where the organization record page's Members / Invitations / Teams tab strip is declared, at the three places that assert it (#16270)

#16270 measured that no object under `packages/platform-objects/src/identity/` declares
the `Field.relatedList` prominence key, and inferred from that a two-way disjunction:
either the metadata is short three `relatedList: 'primary'` declarations, or the three
documents that describe the page as opening on tab-0 **Members** have gone stale.

**Neither. The premise is false.** The tab strip is declared metadata —
`SysOrganizationDetailPage` in `packages/platform-objects/src/pages/sys-organization.page.ts`,
a `kind: 'slotted'` record page for `sys_organization`, `isDefault: true`, handed to the
runtime by plugin-auth's `pages: [SysOrganizationDetailPage, SysUserDetailPage]`. Its
`slots.tabs` override carries exactly three `record:related_list` tabs — Members,
Invitations, Teams, in that order — and objectui's synthesizer pushes that authored node
and never calls `buildDefaultTabs`, so the strip replaces the synthesized
Details + stacked `Related` one outright and Members really is at index 0. That file was
already in the tree at the commit the card measured.

`relatedList: 'primary'` is a different mechanism (prominence on a child's lookup field,
promoting one derived list to its own tab). The card looked for that key, correctly found
none, and read the zero as "declared by no metadata". While the `tabs` slot is present,
adding the key would not move this page at all.

**What changes here is prose only — no metadata, no behaviour.** The two source comments
that assert the tab order and the QA checklist item that grades it now name the page that
declares it, so the next reader does not repeat the measurement:

- `packages/platform-objects/src/identity/sys-member.object.ts` — the `invite_user`
mirror's rationale
- `packages/platform-objects/src/identity/invite-entry-toolbar.test.ts` — the file header
that states the whole pin's premise
- `docs/qa/platform-checklist/areas/identity-auth.json` —
`identity-auth.org-membership-team-management`, a new `source` entry plus the revision
and history bump its ledger requires. Steps, acceptance clauses, oracles and negatives
are unchanged: a grader grades exactly what it graded before, and now knows that a
Details + stacked `Related` strip means this page failed to load rather than that the
clause was wrong.

This package ships its `src` comments in `dist` (measured: the new comment text appears
4 times under `packages/platform-objects/dist`, with an exported symbol as the positive
control and the test-file header absent at 0), which is why a comment-only diff here
takes a changeset rather than the publishes-nothing exemption.
6 changes: 4 additions & 2 deletions docs/qa/platform-checklist/areas/identity-auth.json
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,7 @@
"title": "Setup Organization page resolves the active org and drives member/invitation/team management through the better-auth org endpoints — non-admins refused server-side",
"since": "v17",
"status": "active",
"revision": 2,
"revision": 3,
"priority": "P1",
"surface": "mixed",
"personas": ["tenant admin / org owner", "a target org member", "a non-admin org member (forger)"],
Expand Down Expand Up @@ -822,6 +822,7 @@
"traps": ["wrong-persona", "dispatcher-vs-hono-route", "hydration-race"],
"source": [
"packages/platform-objects/src/apps/setup-nav.contributions.ts#nav_organization (nav_organization recordId {current_org_id}, cloud ADR-0081 D3; Teams/Invitations always mounted per cloud ADR-0081 D1)",
"packages/platform-objects/src/pages/sys-organization.page.ts#SysOrganizationDetailPage (WHERE THE THREE TABS COME FROM: a kind: 'slotted' record page whose slots.tabs override replaces the synthesized tab strip outright, giving exactly Members / Invitations / Teams with Members at index 0. Handed to the runtime by plugin-auth's pages: [...]. NOT the Field.relatedList: 'primary' prominence key — no identity object declares that key, and it would not move this page while the tabs slot is present. A grader who finds a Details + stacked Related strip instead is looking at the synthesized default, i.e. this page failed to load)",
"packages/plugins/plugin-auth/src/auth-route-ledger.ts (organization family: update-member-role, remove-member, update, create-team, add-team-member, list-members/teams/invitations, get-active-member, get-full-organization)",
"packages/spec/src/identity/membership-role.ts#BUILTIN_MEMBERSHIP_ROLES (BUILTIN_MEMBERSHIP_ROLES / BUILTIN_MEMBERSHIP_ROLE_OPTIONS — THE role vocabulary: owner/admin/delegated_admin/member, ADR-0108; 'nothing widens these at boot any more')",
"docs/adr/0108-membership-grade-is-not-a-capability-channel.md (why the list is closed: a grade decides what you can REACH, never a bundle of what you may do)",
Expand All @@ -831,7 +832,8 @@
],
"history": [
{ "revision": 1, "date": "2026-08-08", "change": "new item: Setup Organization page {current_org_id} resolution (ADR-0081) with Members/Invitations/Teams tabs, update-member-role (4-name vocab)/remove-member/rename, create-team + add-team-member → sys_team_member rows, non-admin refused server-side (PENDING-GAPS §B). Teams membership deep-tested in identity-auth.teams-bu-membership; org-member management stays here", "ref": "claude/platform-test-checklist-ocwugl" },
{ "revision": 2, "date": "2026-08-11", "change": "CORRECTION from run #7663: the role vocabulary named here was wrong. The enforced builtin set is {owner, admin, delegated_admin, member} (ADR-0108 BUILTIN_MEMBERSHIP_ROLE_OPTIONS), NOT {owner, admin, member, guest} — 'guest' is rejected 400 ROLE_NOT_FOUND and delegated_admin is legitimate. Corrected the step, the acceptance clause and the negative; added a closed-vocabulary clause (guest / a stack position / a PermissionSet name each refused, no row left behind); re-pointed source at membership-role.ts + ADR-0108 + the vocabulary dogfood pin instead of the stale organization.zod.ts doc-comment the wrong text came from (that doc-comment is fixed in the same PR)", "ref": "#7740" }
{ "revision": 2, "date": "2026-08-11", "change": "CORRECTION from run #7663: the role vocabulary named here was wrong. The enforced builtin set is {owner, admin, delegated_admin, member} (ADR-0108 BUILTIN_MEMBERSHIP_ROLE_OPTIONS), NOT {owner, admin, member, guest} — 'guest' is rejected 400 ROLE_NOT_FOUND and delegated_admin is legitimate. Corrected the step, the acceptance clause and the negative; added a closed-vocabulary clause (guest / a stack position / a PermissionSet name each refused, no row left behind); re-pointed source at membership-role.ts + ADR-0108 + the vocabulary dogfood pin instead of the stale organization.zod.ts doc-comment the wrong text came from (that doc-comment is fixed in the same PR)", "ref": "#7740" },
{ "revision": 3, "date": "2026-09-12", "change": "PROVENANCE, no clause change: #16270 measured that no object under packages/platform-objects/src/identity/ declares Field.relatedList and read the Members/Invitations/Teams claim here as possibly stale. It is not stale — the strip is declared by SysOrganizationDetailPage (packages/platform-objects/src/pages/sys-organization.page.ts), a slotted page whose slots.tabs override replaces the synthesized strip. Added that file as a source entry so the next reader does not have to re-derive it. Steps, acceptance clauses, oracles and negatives are unchanged", "ref": "#16270" }
]
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@
// tab-1 Invitations. The maintainer, looking to "invite a teammate by email",
// landed on Members and concluded the product had no invite entry at all.
//
// ⚠️ Where that tab order is DECLARED (#16270 measured that it is not declared
// here): `SysOrganizationDetailPage` in `../pages/sys-organization.page.ts`,
// handed to the runtime by plugin-auth's `pages: [...]`. It is a
// `kind: 'slotted'` record page whose `slots.tabs` override REPLACES the
// synthesized tab strip outright — objectui's `buildDefaultPageSchema` pushes
// the authored node and never calls `buildDefaultTabs` — giving exactly
// Members / Invitations / Teams with Members at index 0. The `relatedList:
// 'primary'` prominence key is a different mechanism and no object under
// `identity/` declares it; with the `tabs` slot present it would not move this
// page if one did. So this pin's premise is declared metadata, not folklore —
// but the declaration is one directory over, not in the objects it constrains.
//
// Two halves are pinned here, because the fix has two failure modes and they
// fail in opposite directions:
//
Expand Down
12 changes: 12 additions & 0 deletions packages/platform-objects/src/identity/sys-member.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ export const SysMember = ObjectSchema.create({
// used to live only on tab-1 Invitations: an admin looking to "invite a
// teammate by email" landed on Members, saw only "Add Member" (attach an
// existing user by id), and concluded the product had no invite entry.
//
// ⚠️ That tab order is DECLARED, and not by anything in this directory:
// `SysOrganizationDetailPage` (`../pages/sys-organization.page.ts`,
// handed to the runtime by plugin-auth's `pages: [...]`) is a
// `kind: 'slotted'` record page whose `slots.tabs` override REPLACES the
// synthesized tab strip outright — objectui's `buildDefaultPageSchema`
// pushes the authored node and never calls `buildDefaultTabs` — so the
// strip is exactly Members / Invitations / Teams, Members at index 0.
// It is NOT the `Field.relatedList: 'primary'` prominence key: no object
// under `identity/` declares that key, and with the `tabs` slot present
// one would not move this page if it did. Looking for the tab order in
// this directory finds nothing; it lives one directory over (#16270).
// Declaration order is render order — the related-list toolbar bridge
// maps the child object's `list_toolbar` actions in array order
// (objectui `RelatedRecordActionsBridge.deriveActions` →
Expand Down
Loading