Skip to content

Commit 305e7fc

Browse files
os-billclaude
andauthored
docs(platform-objects): name the slotted page that declares the org record tab strip (#17750)
Part of #16270 - **Clause-②: no** — this PR puts no new key on any published payload. ## The card's premise is false, and that is the deliverable The card asked for a measurement first: three documents assert that the organization record page opens on a **Members** tab with Invitations and Teams beside it, while `packages/platform-objects/src/identity/` declares no `Field.relatedList` prominence key. It framed a two-way disjunction — reading 1, the metadata is short three `relatedList: 'primary'` declarations; reading 2, the three documents are stale — and the triage admitted the card on the grounds that **both branches are defects**. **Neither branch holds.** The tab strip is declared metadata. It is simply not declared by the key the measurement looked for, and not in the directory it looked in. `packages/platform-objects/src/pages/sys-organization.page.ts` exports `SysOrganizationDetailPage`: a `type: 'record'`, `kind: 'slotted'`, `isDefault: true` page for `sys_organization` whose `slots.tabs` override carries exactly three `record:related_list` tabs — **Members, Invitations, Teams, in that order**. `packages/plugins/plugin-auth/src/auth-plugin.ts` hands it to the runtime as `pages: [SysOrganizationDetailPage, SysUserDetailPage]`, and its own comment there already said so: *"sys_organization gets a Members / Invitations / Teams tab strip"* — a fourth document making the claim, and the only one that named the mechanism. The renderer half, read from objectui at the sha this repo pins (`.objectui-sha` = `53ded82bf7`): - `packages/plugin-detail/src/synth/buildDefaultPageSchema.ts` — when the assigned page supplies a `tabs` slot, the synthesizer pushes that authored node and **never calls `buildDefaultTabs`**. The authored strip replaces the synthesized one outright, so Members is at index 0. - The same file's `buildDefaultTabs` always seeds `items[0]` with **Details**. So even under reading 1 — three `relatedList: 'primary'` declarations added — tab 0 would have been *Details*, never *Members*. Reading 1 could not have produced the documented surface. - `packages/app-shell/src/utils/deriveRelatedLists.ts` sets `isPrimary` from `fieldDef.relatedList === 'primary'`, exactly as the card quoted. That key is a different mechanism, and while the `tabs` slot is present it would not move this page at all. `sys-organization.page.ts` was already in the tree at `77781151d`, the commit the card measured against (verified through the contents API at that ref: HTTP 200, 4606 bytes). The zero was real; the inference from it was not. ## What this PR changes — prose only No metadata. No behaviour. The three documents that assert the tab order now name the page that declares it, so the next reader does not repeat the measurement: | file | change | |---|---| | `packages/platform-objects/src/identity/sys-member.object.ts` | the `invite_user` mirror's rationale gains a note naming `../pages/sys-organization.page.ts` as the declaring page, and stating that `relatedList` is a different mechanism absent from this directory | | `packages/platform-objects/src/identity/invite-entry-toolbar.test.ts` | the file header — the stated premise of the whole pin — gains the same pointer | | `docs/qa/platform-checklist/areas/identity-auth.json` | `identity-auth.org-membership-team-management` gains one `source` entry, plus the `revision` 2 to 3 and `history` bump its ledger requires | **What a grader will now do differently** (the checklist is what QA grades, so this is stated plainly): nothing about the grade changes. The item's `steps`, `acceptance` clauses, their oracles and `verify` text, and the `negative` list are byte-unchanged — the same screenshot, the same three tabs, the same FAIL condition for the Organization nav landing on the raw list. What is added is a `source` entry, which is a pointer a grader reads to locate the surface, and it tells them one new thing: if the page renders **Details plus a stacked `Related` tab** instead of the three, that means `SysOrganizationDetailPage` failed to load — it does not mean the clause was wrong. That reading was not available before, and it is the reading this card was filed for lack of. ## Verification | check | result | |---|---| | `pnpm lint` (repo-wide, `eslint . --no-inline-config`) | exit 0, 2m05s — full run, no narrowing claimed | | `pnpm --filter @objectstack/platform-objects test` | exit 0 — 39 files, 561 tests passed | | `pnpm --filter @objectstack/platform-objects typecheck` | exit 0 | | `pnpm --filter "@objectstack/platform-objects..." build` | exit 0 (dependency closure) | | `pnpm check:platform-checklist` | `OK — 15 areas, 264 items; symbol anchors: 633/633 resolved` | | `pnpm check:nul-bytes` · `check:doc-authoring` · `check:page-declaration-shape` · `check:published-files` · `check:test-source-alias` · `check:cross-package-test-inputs` · `check:type-check-coverage` · `check:comment-mask-adoption` · `check:comment-mask-corpus` · `check:keyed-text-bounds` · `check:closing-keyword-parity` | each exit 0 | | `check-empty-changeset` · `check-changeset-no-major` · `check-adr-0087-registration`, each `--base origin/main` | each exit 0 | | `check-partof-closing-keyword` with this body and this branch's commit list | exit 0 | Every exit code above was captured before any pipe (`cmd > file 2>&1; EXIT=$?`), and the two lock-held runs are quoted from the wrapper's own `VERDICT command-exit` line. **Ablation — the checklist gate's new anchor really is checked.** With the new `source` entry's symbol renamed on disk to `SysOrganizationDetailPageXX` (injected text present 1, original present 0, both counted with `grep -oF`), `check-platform-checklist` went to exit 1 with `ABSENT SYMBOL ... is not in packages/platform-objects/src/pages/sys-organization.page.ts outside its comments`. Restored from a pristine copy and proved byte-identical by `git hash-object` (`168dca14` before and after). The green above is therefore a reading, not a gate that judged nothing. **Why a changeset and not the publishes-nothing exemption.** `@objectstack/platform-objects` ships `files: ["dist", ...]`, and its `src` comments reach `dist`: after the build, the new comment text appears **4** times under `packages/platform-objects/dist`, with an exported symbol (`SysMember`, 16 occurrences) as the positive control and the edited test file's header absent at **0**. Published bytes move, so a `patch` changeset is owed — `.changeset/16270-org-record-tab-strip-provenance.md`, in the tree's issue-scoped spelling. **Declared narrowing.** `scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 62 runnable commands for this change set; the table above is a subset chosen by what this diff can move, and the rest is CI's farm. The derivation also warned that this branch's base is 3 commits behind `origin/main` and that one file it derives from (`scripts/check-regen-pending.mjs`) changed across that range; re-deriving after `git fetch origin main` produced a byte-identical command list. ## Why `Part of` and not a closing keyword This PR carries the measurement and repairs the discoverability gap, but it does not discharge the card. `docs/adr/0133-org-management-open-basics.md`, under *"What this record does not decide"*, now states something false — that the tab claim is *"declared by none of its metadata"* and that the ordering is *"either an emergent property of the renderer or a claim that has gone stale"*. `docs/adr/**` is a governed surface, outside this dispatch's declared file face, and its correction is the maintainer's call. Recorded as a finding in the round report rather than ridden in here. ## 验收备注 - **objectui is read-only and was read, not assumed.** All renderer readings are from the sha this repo pins (`53ded82bf7`), taken with `git show 53ded82bf7:path` in the sibling checkout. That checkout's own HEAD is `3fbdd4a2d` on an unrelated branch; nothing there was edited. - **A second, genuinely different surface exists and is easy to confuse with this one.** objectui's console carries a hand-written organization management area at `/organizations/:slug` (`packages/app-shell/src/console/organizations/manage/OrganizationLayout.tsx`), whose tabs are **Members / Invitations / Settings** with an index redirect to `members`. It is reached from a "Manage" button on the organizations list, not from Setup's `nav_organization`, and it has no Teams tab. Noted so the next reader does not measure that surface and grade this one. Not filed: no defect, just a collision of names. - **The checklist item has never been run.** `docs/qa/platform-checklist/runs/` contains only `README.md`, so `identity-auth.org-membership-team-management` has no run record and its tab assertion had never been executed against a live console. Noted, not filed. - **The running-app measurement the triage asked for was not performed.** It is no longer decisive — the static evidence now names the declaring page, the registration site and the renderer branch that honours it — but it remains the only way to observe the rendered strip. Stated here rather than implied. --- _Generated by [Claude Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent aaacf1d commit 305e7fc

4 files changed

Lines changed: 73 additions & 2 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
Name where the organization record page's Members / Invitations / Teams tab strip is declared, at the three places that assert it (#16270)
6+
7+
#16270 measured that no object under `packages/platform-objects/src/identity/` declares
8+
the `Field.relatedList` prominence key, and inferred from that a two-way disjunction:
9+
either the metadata is short three `relatedList: 'primary'` declarations, or the three
10+
documents that describe the page as opening on tab-0 **Members** have gone stale.
11+
12+
**Neither. The premise is false.** The tab strip is declared metadata —
13+
`SysOrganizationDetailPage` in `packages/platform-objects/src/pages/sys-organization.page.ts`,
14+
a `kind: 'slotted'` record page for `sys_organization`, `isDefault: true`, handed to the
15+
runtime by plugin-auth's `pages: [SysOrganizationDetailPage, SysUserDetailPage]`. Its
16+
`slots.tabs` override carries exactly three `record:related_list` tabs — Members,
17+
Invitations, Teams, in that order — and objectui's synthesizer pushes that authored node
18+
and never calls `buildDefaultTabs`, so the strip replaces the synthesized
19+
Details + stacked `Related` one outright and Members really is at index 0. That file was
20+
already in the tree at the commit the card measured.
21+
22+
`relatedList: 'primary'` is a different mechanism (prominence on a child's lookup field,
23+
promoting one derived list to its own tab). The card looked for that key, correctly found
24+
none, and read the zero as "declared by no metadata". While the `tabs` slot is present,
25+
adding the key would not move this page at all.
26+
27+
**What changes here is prose only — no metadata, no behaviour.** The two source comments
28+
that assert the tab order and the QA checklist item that grades it now name the page that
29+
declares it, so the next reader does not repeat the measurement:
30+
31+
- `packages/platform-objects/src/identity/sys-member.object.ts` — the `invite_user`
32+
mirror's rationale
33+
- `packages/platform-objects/src/identity/invite-entry-toolbar.test.ts` — the file header
34+
that states the whole pin's premise
35+
- `docs/qa/platform-checklist/areas/identity-auth.json` —
36+
`identity-auth.org-membership-team-management`, a new `source` entry plus the revision
37+
and history bump its ledger requires. Steps, acceptance clauses, oracles and negatives
38+
are unchanged: a grader grades exactly what it graded before, and now knows that a
39+
Details + stacked `Related` strip means this page failed to load rather than that the
40+
clause was wrong.
41+
42+
This package ships its `src` comments in `dist` (measured: the new comment text appears
43+
4 times under `packages/platform-objects/dist`, with an exported symbol as the positive
44+
control and the test-file header absent at 0), which is why a comment-only diff here
45+
takes a changeset rather than the publishes-nothing exemption.

‎docs/qa/platform-checklist/areas/identity-auth.json‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -747,7 +747,7 @@
747747
"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",
748748
"since": "v17",
749749
"status": "active",
750-
"revision": 2,
750+
"revision": 3,
751751
"priority": "P1",
752752
"surface": "mixed",
753753
"personas": ["tenant admin / org owner", "a target org member", "a non-admin org member (forger)"],
@@ -822,6 +822,7 @@
822822
"traps": ["wrong-persona", "dispatcher-vs-hono-route", "hydration-race"],
823823
"source": [
824824
"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)",
825+
"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)",
825826
"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)",
826827
"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')",
827828
"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)",
@@ -831,7 +832,8 @@
831832
],
832833
"history": [
833834
{ "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" },
834-
{ "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" }
835+
{ "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" },
836+
{ "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" }
835837
]
836838
},
837839
{

‎packages/platform-objects/src/identity/invite-entry-toolbar.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@
77
// tab-1 Invitations. The maintainer, looking to "invite a teammate by email",
88
// landed on Members and concluded the product had no invite entry at all.
99
//
10+
// ⚠️ Where that tab order is DECLARED (#16270 measured that it is not declared
11+
// here): `SysOrganizationDetailPage` in `../pages/sys-organization.page.ts`,
12+
// handed to the runtime by plugin-auth's `pages: [...]`. It is a
13+
// `kind: 'slotted'` record page whose `slots.tabs` override REPLACES the
14+
// synthesized tab strip outright — objectui's `buildDefaultPageSchema` pushes
15+
// the authored node and never calls `buildDefaultTabs` — giving exactly
16+
// Members / Invitations / Teams with Members at index 0. The `relatedList:
17+
// 'primary'` prominence key is a different mechanism and no object under
18+
// `identity/` declares it; with the `tabs` slot present it would not move this
19+
// page if one did. So this pin's premise is declared metadata, not folklore —
20+
// but the declaration is one directory over, not in the objects it constrains.
21+
//
1022
// Two halves are pinned here, because the fix has two failure modes and they
1123
// fail in opposite directions:
1224
//

‎packages/platform-objects/src/identity/sys-member.object.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ export const SysMember = ObjectSchema.create({
4848
// used to live only on tab-1 Invitations: an admin looking to "invite a
4949
// teammate by email" landed on Members, saw only "Add Member" (attach an
5050
// existing user by id), and concluded the product had no invite entry.
51+
//
52+
// ⚠️ That tab order is DECLARED, and not by anything in this directory:
53+
// `SysOrganizationDetailPage` (`../pages/sys-organization.page.ts`,
54+
// handed to the runtime by plugin-auth's `pages: [...]`) is a
55+
// `kind: 'slotted'` record page whose `slots.tabs` override REPLACES the
56+
// synthesized tab strip outright — objectui's `buildDefaultPageSchema`
57+
// pushes the authored node and never calls `buildDefaultTabs` — so the
58+
// strip is exactly Members / Invitations / Teams, Members at index 0.
59+
// It is NOT the `Field.relatedList: 'primary'` prominence key: no object
60+
// under `identity/` declares that key, and with the `tabs` slot present
61+
// one would not move this page if it did. Looking for the tab order in
62+
// this directory finds nothing; it lives one directory over (#16270).
5163
// Declaration order is render order — the related-list toolbar bridge
5264
// maps the child object's `list_toolbar` actions in array order
5365
// (objectui `RelatedRecordActionsBridge.deriveActions` →

0 commit comments

Comments
 (0)