Repository navigation
feat(platform-objects): sys_organization declares a record_header invite action; leave_organization folds to the overflow and names what is lost - #24216
Conversation
…ite action; leave_organization folds to the overflow Claude-Session: https://claude.ai/code/session_01Eey1448sFJ5Z95gbXZh2tG Co-authored-by: Claude <noreply@anthropic.com>
…er placement, add changeset Claude-Session: https://claude.ai/code/session_01Eey1448sFJ5Z95gbXZh2tG Co-authored-by: Claude <noreply@anthropic.com>
…ization feature gate Claude-Session: https://claude.ai/code/session_01Eey1448sFJ5Z95gbXZh2tG Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Eey1448sFJ5Z95gbXZh2tG Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 143 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 51d2f77e4abc6a4c2d737a63c148533a63cb31e0 && git checkout 51d2f77e4abc6a4c2d737a63c148533a63cb31e0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8259ea03e2959129c688639f69d0782c3a820f89 9aa48a7e5a1bcd99e1d200d1ee13ef004dee6908 && git checkout -B drift-repro 8259ea03e2959129c688639f69d0782c3a820f89 && git merge --no-ff 9aa48a7e5a1bcd99e1d200d1ee13ef004dee6908
node scripts/docs-audit/affected-docs.mjs --json 8259ea03e2959129c688639f69d0782c3a820f89
|
Contract reviewServed-tier: Inputs: card #24016 (body and all six comments, including claims 6238369945 and 6240102228 and os-dev-report 6239623631), ruling 6228318638 on #24034 (letter B′), PR #24216 body, file list and the net diff against main at the head (merge-base bb7ccde), and the 40 check-runs on the head. Not read: the dispatch order or the dispatching seat's conclusions. Check-runs on the head at 2026-10-17T20:14Z: 33 success, 6 skipped (paths-filtered lanes: Dogfood Verify CLI, Dogfood Regression Gate shards, Console Pin Gate, Build Docs, Temporal Conformance, Packed-tarball smoke), 1 still in progress (Test Core 1/6), 0 failure. Green at read time: TypeScript Type Check (the lane that runs check:generated, check:api-surface and the spec artifact gates), Check Changeset, Governed Surface Queue Guard, Spec property liveness, Lint and Repo Gates (all nine Lint lanes, check:i18n among them), Build Core, Test Core 2 through 6, and the three card-claim guards. The in-progress shard is recorded as such; this record does not wait on it, and the landing pre-check that every check is green stays the adopting seat's to confirm once it completes. ① Derived judgments
② Semver levelChangeset Clause-②: no ③ Boundary flagsDev deviations (os-dev-report 6239623631):
open_questions:
Reviewer notes for the adopting seat (not blockers):
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
CI red · Failing check: Signature (new; no open card matches): shard 1/6 times out at the 50-minute wall in its test step on a Initial judgment: not shown to be this PR's.
Act: one re-run of the failed jobs on this head, which also yields a readable shard-1 log. If shard 1/6 times out again on this head, the signature is real. It is then diagnosed from that log on this PR, and filed if it is not this diff's. Generated by Claude Code |
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The body opens Fixes #24016, its only closing keyword. #24021's copy half rides here, as ruling 6228318638 item 2 says. |
| Scope | 10 files (+276 / −34). That is the re-declared claim's surface (6240102228): sys-organization.object.ts (the invite action, leave_organization's order: 1 and its confirmText), the four translation bundles, three pins, the changeset, and the one cross-lane spec row declared on #6017. Not governed (check-governed-merges.mjs --pr 24216: ordinary queue, 310 lines). |
| Ruling | Ruling 6228318638 (B′ on #24034), item 1 read key by key at the head: type: 'api', the invite-member target, recordIdParam: 'organizationId', requiresFeature: 'organization', requiresMembershipReach: 'invite_member', variant: 'primary', order: -1, locations: ['record_header']. Every key is declared on ActionSchema. Item 2: the confirm copy names the environments and members in en, zh-CN, ja-JP and es-ES. No new spec key. |
| The spec row | Entailed by the ruled requiresFeature: 'organization': the reverse leg of feature-gate-guard.test.ts reds any features.* gate not booked in gatedInputs. The seat re-read the guard. No non-test code reads gatedInputs (git grep at the head finds only the two typecheck-debt ledgers). The same one-row booking shipped as a patch in both packages in 17.3.0 ("No schema, export or authorable key changes", both CHANGELOGs). The ruling's workload line "no spec change" was an estimate; its binding line "no new spec key" holds. |
| H3 (the order) | Confirmed by the dev, and the seat re-read it. objectui origin/main's action-bar.tsx sorts by order ascending (unset = 0), then primary, then registration order, and its default maxVisible is 3 (page-header-actions.test.tsx, "overflows past the default maxVisible of 3"). The head's four record_header actions are set_active_organization, invite_user, leave_organization and change_slug. So an admin in multi-org sees invite, set_active and change_slug inline, and leave folds. |
| Review face | Published schema (packages/spec/src/kernel/public-auth-features.ts). The CONTRACT_REVIEW_TIER record 6240346929 on this head is PASS. The seat verified its central claims: ADR-0133's "the gate is the organization capability, never multi-org"; the 17.3.0 precedent; no non-test reader of the row; the header set above. The remainder (the bundles and invite-user-action-copy.test.ts) is this ACCEPT at TIER_DEFAULT. |
| Prose face | Changeset, sentence by sentence. The endpoint, organizationId, both params, the grade-capped role picker, the owner / admin / delegated_admin grades, order: -1, leave's danger and order: 1, the four-locale copy and the spec booking each match the diff. "folds into the ⋯ overflow menu instead of taking the third inline slot" holds where the invite leads a full header; the PR's Acceptance notes state the condition (a plain member keeps leave inline, never as the primary). |
| Bundles | en is generator output; the translated-locale values are hand-written, which AGENTS.md's Documentation Guardrails row allows ("Translated-locale values … are hand-written: editing one of those strings is fine"). check:i18n is green on the head. The zh-CN strings read correctly. |
| Pins and ablation | The new sys-organization-invite-action.test.ts pins placement, the single primary, the sort, and leave past the inline budget. The three mirrors' pins now run over four. One ablation (delete leave's order: 1) turns "sorts last, past the inline budget once the invite leads" red, 1 of 5. The restore is clean (blob back to 919503b9). |
Clause-② |
no, at a line start in the PR body and the changeset; patch for @objectstack/platform-objects and @objectstack/spec. Check Changeset is green. |
| Gates | Dev union: 86 derived, 86 run, 0 NOT MEASURED. CI on this head at this read: 41 check-runs, 35 success, 6 skipped (each on the EXPECTED_SKIPS roster in scripts/pm/check-expected-skips.mjs), 0 failed. Test Core (1/6) hit the 50-minute job wall on attempt 1 with no assertion and an unreadable log (signature and judgment: 6240560569); the one re-run passed it in 22m54s, and the aggregate Test Core is green. |
Disposition:
open_questions[0](fold leave unconditionally withcomponent: 'action:menu'): A, keeporder: 1.orderis the ruling's own vocabulary; item 5 has cloud#2910 override order and variant for its page; a second placement lever is not ruled. The record answers the same.open_questions[1](gate the invite onorganizationwhile its siblings usemultiOrgEnabled): A. The ruling namesorganizationverbatim, and ADR-0133 and ADR-0093 D8 record the split as deliberate.deviations: accepted. The spec row is re-declared on the card and the spec seat post; leave'sorder: 1uses an existing key and is declared;objectOverride: 'sys_invitation'is the only way both params resolve on an object without those fields; the hand-written locale values are what AGENTS.md allows (where it and the dispatch prompt differ, AGENTS.md wins); the branch housekeeping and the trailer choice have no contract bearing.- Acceptance note, not filed: ADR-0133's descriptive prose says
invite_user"is declared on three objects"; this PR makes four. Its two binding statements hold for the fourth mirror.docs/adr/**is a governed surface, so the count is the maintainer's Tier H carry, reported in this seat's round report. - The attempt-1 timeout is a suspected flake with no named file, below the filing gate; its signature stays on 6240560569, and a second occurrence of it is filed with the stalled file named.
- Serial: leave_organization: no predicate root carries "the caller is this organisation's only owner", so the platform action cannot be disabled for the last owner before the click (better-auth refuses it only after the confirm) #24020 (same file) is dispatched after this lands. plugin-auth: an OAuth consent binds the consenting user's current organization, the access token carries it as organization_id, and verifyMcpAccessToken returns it (slice 2 of #24137, ruled A at 6232069920) #24166 (
domain:services, declaration 6238948557 on [PM seat] domain:engine — 🟢 os-project-manager #6367) regenerates the sameenbundle for threesys_oauth_*descriptions; whichever lands later mergesmainand regenerates.
Landing: ordinary queue (pr_ready + automerge_enable through the relay) once every check on this head is green.
Generated by Claude Code
Fixes #24016
#24021's copy half rides here (ruling 6228318638 item 2); that card's record-scope key is not added.
Clause-②: no
What
sys_organizationdeclares the invite action ruling 6228318638 (letter B′ on #24034, item 1) names, so an organization record page'spage:headercan list it. Apage:headerresolves action ids against its own object only.invite_useratrecord_header:type: 'api',target: '/api/v1/auth/organization/invite-member',recordIdParam: 'organizationId',requiresFeature: 'organization',requiresMembershipReach: 'invite_member',variant: 'primary',order: -1. Its params areemailandrole, both required, with the role picker onINVITATION_ROLE_OPTIONS, assys_invitation.invite_userdeclares them. Its description and success message match the other three mirrors (sys_user,sys_invitation,sys_member).leave_organizationkeepsvariant: 'danger'and gainsorder: 1. ItsconfirmTextnow names what is lost: "Leave this organization? You will lose access to its environments and members." The zh-CN, ja-JP and es-ES bundles carry translated text.Measurements behind the choices
emailandroleare not fields ofsys_organization. A field-backed param resolves against the action's own object unlessobjectOverridenames another. So both params carryobjectOverride: 'sys_invitation', the devicesys_member's mirror already uses foremail. Apart from that the shape issys_invitation's (required, same options). The existing "every field-backed param resolves to a real field" pin now covers all four mirrors.ActionSchema(requiresFeature,requiresMembershipReachinpackages/spec/src/ui/action.zod.ts) and are already used by all three mirrors. The loweredvisiblematches theirs byte for byte; that is pinned.9753811'spage:headerrenderer (packages/components/src/renderers/layout/containers.tsx). It stable-sorts byorder(unset = 0) and promotes the first non-destructivevariant: 'primary'action. Then the firstmaxVisibleauthored actions (default 3) go inline and the rest fold into the⋯, with destructive ones in its last band. With only the invite added, the row was invite, set_active, leave, andchange_slugfolded. Soleave_organizationstayed inline as the third button. For it to "fall to the overflow" it needs a declared position:order: 1sorts it last, so the inline row is invite, set_active, change_slug and leave folds. This reuses the spec's existingorder, so no new key.en.objects.generated.tsis rewritten from source on every extract. The translated locales keep their values in merge mode, so the new leaves were seeded in English bypnpm i18n:extract. I set them to the siblingsys_invitation.invite_usertranslations and translated the newleave_organization.confirmText, then re-ran the extract. It is idempotent and drops the seeded source-hash entries.pnpm check:i18n: all 10 packages in sync.Outside the claim's file surface
packages/spec/src/kernel/public-auth-features.tsgains one row:sys_organization.actions.invite_userinPUBLIC_AUTH_FEATURES.organization.gatedInputs. This is a ledger entry, not a key.platform-objects/src/feature-gate-guard.test.tsrequires everyfeatures.*gate in the objects to be booked there, and it went red without the row. The ruledrequiresFeature: 'organization'cannot land without it.pnpm --filter @objectstack/spec check:generatedreports all 14 artifacts up to date, and the changeset adds a@objectstack/specpatch.Tests
sys-organization-invite-action.test.tspins:recordIdParam;leave_organizationstaysdangerand sorts past the inline budget.invite-entry-toolbar.test.tsandinvite-user-action-copy.test.tsnow includesys_organizationas a fourth mirror for endpoint, gates, inputs, param resolvability, copy, translated bundles and served metadata. The list-toolbar location pin stays on the three toolbar mirrors.leave_organization'sorder: 1throughscripts/ablation-replace.mjslanded (anchor 1 to 0, blob919503b9to70b55e52). The test "sorts last, past the inline budget once the invite leads" went red (1 failed, 4 passed). The file was restored to the HEAD blob andgit diff HEADwas empty.pnpm --filter @objectstack/platform-objects test: 74 files, 1162 tests passed.typecheck: exit 0.src/kernel/public-auth-features.test.ts: 24 passed. All on9aa48a7e5, after mergingorigin/main, which brought feat(cloud-connection): "Installed Apps" opens Setup Packages filtered to Source = marketplace #24200 and feat(plugin-audit): Setup has one audit entry, "Audit Log", and it opens the audit browser #24204; the bundles regenerated byte-identical.9aa48a7e5:node scripts/pm/dispatch-gates.mjs --commandsderives 86 commands. All 86 exited 0, and--ranreports "86 run, 0 NOT-MEASURED (a DERIVED zero)".Acceptance notes
leave_organizationfolds only when the header has more than 3 visible authored actions. That is the case for an admin in multi-org: invite, set_active, change_slug, leave. A plain member does not see the invite, so leave stays inline as a red outline button. It is never the primary (objectui#12764). Moving it to the overflow unconditionally would takecomponent: 'action:menu'. I did not do that: the ruling speaks in terms of order, and cloud#2910 overrides order and variant per its own posture (ruling item 5).organizationcapability, as the ruling says, while its record-header siblings usemultiOrgEnabled. In single-org mode the invite is the only header action.Generated by Claude Code