Skip to content

fix(platform-objects,spec): surface the email-invite entry on the org page default Members tab - #11562

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-11544-invite-entry-members-tab
Aug 24, 2026
Merged

fix(platform-objects,spec): surface the email-invite entry on the org page default Members tab#11562
os-zhuang merged 2 commits into
mainfrom
claude/issue-11544-invite-entry-members-tab

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#11544

The sys_organization record page (ADR-0081) opens on tab-0 Members, whose related-list toolbar carried exactly one action — add_member, which attaches an already-registered user by id. The email-invite entry, invite_user, was declared only on sys_invitation and sys_user, so it appeared only on tab-1 Invitations. An admin looking to "invite a teammate by email" landed on Members, found no invite affordance, and concluded the product had none. The delivery half worked the whole time — only the door was in another room.

What changed

  • sys_member declares its own invite_user on list_toolbar, ahead of add_member. Same endpoint (/api/v1/auth/organization/invite-member), same email + role inputs, same requiresFeature: 'organization' capability gate as the other two mirrors.
  • add_member is differentiated in chrome onlyvariant: 'secondary' and icon: 'link-2'. Its behaviour, target, params and label are untouched.
  • PUBLIC_AUTH_FEATURES.organization.gatedInputs books the new gated action (see the declared deviation below).
  • Translation bundles regenerated; the three translated locales were hand-filled from the values their sibling mirrors already carry, rather than left holding the English gap-fill.

The PM's three mechanism assumptions, as measured

Read against ../objectui at bc21c704b (read-only; nothing in that repo was edited).

  1. The related-list toolbar bridge renders ALL declared list_toolbar actions — CONFIRMED.RelatedRecordActionsBridge.deriveActions filters the child object's actions by locations.includes('list_toolbar') and assigns the whole array; RelatedList maps over (toolbarActions ?? []). No cap, no slice. Two buttons render side by side.
  2. invite_user could NOT be mirrored unchanged — REFUTED, and this is the substantive finding.sys_member has no email field, so the sys_invitation copy's { field: 'email' } is unresolvable there. resolveActionParams.ts answers an unresolvable field-backed param with a type: 'text' fallback labelled by the raw field name — the dialog still renders and still submits, so nothing goes red anywhere (the ADR-0078 valid-but-inert class). The fix stays entirely inside platform-objects via the existing authorable objectOverride key, exactly as sys_user's own copy already reaches for sys_member for its role half. No spec schema change was needed, so the card's premise held. role needs no override: sys_member declares it, from the same BUILTIN_MEMBERSHIP_ROLE_OPTIONS constant sys_invitation reads.
  3. A list_toolbar action needs no row context — CONFIRMED. The bridge dispatches runRowAction(objectName, undefined, action); the undefined row is the same path the sys_invitation copy already executes on.

Also measured, because it decides whether the differentiation is real rather than decorative: RelatedToolbarButton renders variant={action.variant === 'primary' ? 'default' : 'outline'}, so demoting add_member to secondary genuinely changes it from a filled button to an outlined one. The icon is resolved through lucide and rendered. Neither half is inert metadata.

Declared deviation — one line in packages/spec

The card was claimed Clause-②: no, and the dispatch instruction was to stop and escalate if packages/spec had to be touched. That instruction anticipated a schema change to make the mirror expressible; assumption 2 above shows none was needed. What the change does require is one line of registry bookkeeping: feature-gate-guard.test.ts enforces, in both directions, that every features.* reference in any platform object's lowered predicate is booked in PUBLIC_AUTH_FEATURES.<flag>.gatedInputs. Declaring requiresFeature: 'organization' on the new action therefore mechanically obliges the ledger entry — the twelve sibling entries beside it, including sys_invitation.actions.invite_user, are the same obligation discharged. The alternatives were to ship the button ungated (it would render and then 404 wherever the org capability is off — a Prime Directive #10 violation) or to leave the card undelivered.

No schema, no export, no authorable key and no accept/reject behaviour changes; pnpm --filter @objectstack/spec check:generated reports all 14 generated artifacts up to date with no regeneration, which is the mechanical confirmation that the public surface did not move. Flagging it here rather than deciding it quietly: if the PM reads the ledger append as clause-② territory, the enqueue gate judges the actual diff before this can queue.

Tests

New: packages/platform-objects/src/identity/invite-entry-toolbar.test.ts (21 cases). Beyond pinning the new declaration, its load-bearing case is stated over all three mirrors rather than over the one that was wrong — every field-backed param of every invite_user copy must name a field that really exists on the object it resolves against. That closes the class, so the next copy of any action onto an object lacking one of its param fields lands as a red test instead of as a silently untyped dialog.

Extended: the #2874 feature-gate lowering matrix in platform-objects.test.ts gains the SysMember.invite_user row.

Ablation (predicted direction: red). sys-member.object.ts reverted to origin/main with everything else intact; the mutation was proven on disk before any result was read — the three markers name: 'invite_user', objectOverride: 'sys_invitation' and icon: 'link-2' each dropped to 0 grep hits and the pre-fix icon: 'user-plus' on add_member came back. Mutated: 10 failed | 11 passed (21). Restored: 21 passed (21), with git status --porcelain empty on the path, proving the restore was byte-exact. The 11 that stay green under mutation are the sys_user / sys_invitation mirror rows, which the mutation did not touch — the right shape for a targeted revert. No rebuild was required for either leg: these suites import the object modules by relative source path, so no dist/ sits between the mutation and the assertion.

Gates run locally, all on merge commit 22ecd75cb (the head this PR opens with), each exit code captured before any pipe:

pnpm --filter @objectstack/platform-objects test29 files / 476 passed · typecheck (platform-objects + spec) · check:i18n · --filter @objectstack/spec check:generated (14/14 up to date) · check:nul-bytes · check:engine-double-contract · check:where-matcher · check:cross-package-test-inputs · check:type-check-coverage · check-adr-0087-registration · check-empty-changeset · check-changeset-no-majorall exit 0.

Earlier in the round, on the pre-merge commit, the rest of the derived family also ran green: check:changeset-gate-self-tests, check:query-options-erasure, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check:spec-parsed-alias, check:merge-driver, check-ci-filter-parity, check-plugin-teardown-shape, check:doc-formula-expressions, and spec's check:empty-state / check:liveness / check:strictness-ledger / check:variant-docs.

Declared narrowings — host capability, not diff findings

This seat ran on macOS with bash 3.2.57, which has no mapfile builtin, and no flock. Three consequences, all declared rather than worked around:

  • scripts/pm/os-verify-lock.sh refuses on this host — VERDICT lock-unusable (exit 99) · never acquired · refused before waiting · nothing was built or tested. Every build and test above therefore ran unlocked.
  • pnpm check:objectui-changeset reports 7 --self-test failures, all ten diagnostics naming scripts/bump-objectui.sh: line 324: mapfile: command not found. That script is not in this diff, and the self-test operates on temp fixtures rather than on this changeset. Linux CI has the builtin.
  • scripts/pm/os-regen-merge.sh could not run for the same reason, so its documented order was followed by hand. No merge=os-regen path appears on either side of this merge, so its step 2 was a no-op; no deferral marker was recorded and none is outstanding.
  • node scripts/check-dev-prereqs.mjs reports the workspace unbuilt (12 of 67 packages missing a dist/ entry point — client-react, studio, embedder-openai and others). That is a consequence of building only the affected closure per resource discipline, not a verdict about this diff; none of the named packages is touched here.

Merge note

#11530 landed as 033a34c7c while this branch was in flight and is merged in here. Its overlap with this diff was wider than the claim comment anticipated — beyond identity/, it also moved the same four translation bundles, platform-objects.test.ts, feature-gate-guard.test.ts and packages/spec/src/kernel/public-auth-features.ts. All hunks are disjoint and both sides survive: in the spec ledger this branch's entry sits in the organization block while that PR's retirement comment sits in the admin block. The full re-verification above was run after the merge for exactly this reason.


Generated by Claude Code

os-zhuangand others added 2 commits August 24, 2026 11:23
…ault Members tab
The sys_organization record page (ADR-0081) opens on tab-0 Members, whose
related-list toolbar carried only `add_member` (attach an already-registered
user by id). The email-invite entry lived on tab-1 Invitations, so an admin
looking to invite a teammate by email found no invite affordance at all.
- sys_member declares its own `invite_user` on `list_toolbar`, ahead of
`add_member` (declaration order is render order in the toolbar bridge).
- The `email` param names `objectOverride: 'sys_invitation'` — sys_member has
no `email` field, and an unresolvable field-backed param degrades silently
to an untyped text input (ADR-0078). `role` resolves natively.
- `add_member` is differentiated in chrome only: `variant: 'secondary'` +
`icon: 'link-2'`. Behaviour, target and label unchanged.
- PUBLIC_AUTH_FEATURES.organization.gatedInputs books the new gated action,
as the feature-gate completeness guard requires.
Fixes#11544
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec, touching 7 documentable anchor(s).

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

  • content/docs/deployment/tenancy-modes.mdx(via sys_member (symbol))
  • content/docs/permissions/authentication.mdx(via sys_member (symbol), /api/v1/auth/organization/invite-member (route))
  • content/docs/permissions/delegated-administration.mdx(via sys_member (symbol))
  • content/docs/permissions/permission-sets.mdx(via sys_member (symbol))
  • content/docs/permissions/positions.mdx(via sys_member (symbol))
  • content/docs/protocol/objectui/actions.mdx(via list_toolbar (literal))
  • content/docs/ui/actions.mdx(via list_toolbar (literal))

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

  • content/docs/releases/implementation-status.mdx(via sys_member (symbol))
  • content/docs/releases/v16.mdx(via sys_member (symbol))
  • content/docs/releases/v17.mdx(via sys_member (symbol))

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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 222 client-bound route-ledger rows — the other 177 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 126 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 ba8420b58d36077fe79ca8ce0f201bb31f8be2bcpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 748ed671c1db109dea81b59a2fa823124a20621f — the merge of head 22ecd75cbd5db06793808b76b3c640bbdb9d024d into base ba8420b58d36077fe79ca8ce0f201bb31f8be2bc, 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 748ed671c1db109dea81b59a2fa823124a20621f && git checkout 748ed671c1db109dea81b59a2fa823124a20621f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ba8420b58d36077fe79ca8ce0f201bb31f8be2bc 22ecd75cbd5db06793808b76b3c640bbdb9d024d && git checkout -B drift-repro ba8420b58d36077fe79ca8ce0f201bb31f8be2bc && git merge --no-ff 22ecd75cbd5db06793808b76b3c640bbdb9d024d
node scripts/docs-audit/affected-docs.mjs --json ba8420b58d36077fe79ca8ce0f201bb31f8be2bc

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

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console: org record page's default Members tab hides the email-invite entry — invite_user lives only on the Invitations tab

1 participant

@os-zhuang