Skip to content

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

Description

@os-zhuang

Symptom

The in-shell Team surface (sys_organization record page, ADR-0081) defaults to tab-0 Members. Its related-list toolbar carries exactly one action, Add Member (add_member — attach an EXISTING user by userId). The email-invite entry (invite_user) lives only on tab-1 Invitations (sys_invitation related list). Both buttons render as variant: primary with the same user-plus icon.

Measured effect: the maintainer, looking to "invite a teammate by email", landed on Members and concluded the product has no invite entry. If the author cannot find it, users will not.

Mechanism / file surface

  • packages/platform-objects/src/pages/sys-organization.page.ts — tab order: Members (sys_member) / Invitations (sys_invitation) / Teams.
  • packages/platform-objects/src/identity/sys-member.object.ts — list_toolbar: add_member only (primary, user-plus).
  • packages/platform-objects/src/identity/sys-invitation.object.ts — list_toolbar: invite_user (primary, user-plus) → POST /api/v1/auth/organization/invite-member. The delivery half works once found: sendInvitationEmail is wired (template auth.invitation, accept URL /_console/accept-invitation/<id>).

Premise verified against origin/main @ 4c9780c (2026-08-24): invite_user appears only on sys_invitation and sys_user; sys_member's toolbar has only add_member.

Requested outcome

Make the email-invite flow discoverable from the default Members tab. Suggested route (PM suggestion, not a ruling — implementer verifies):

  • Mirror invite_user onto sys_member's list_toolbar (same target / params / requiresFeature: 'organization' gate as the sys_invitation copy), and
  • differentiate add_member (icon and/or label) so the two toolbar actions do not render as identical primary user-plus buttons.

Mechanism to verify before writing metadata: the objectui related-list toolbar bridge must render BOTH declared toolbar actions on the record page's related list.

Notes

Activity

  1. self-assigned this
    on Aug 24, 2026
  2. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 1 (maintainer direct-dispatch channel)
    Session: session_71836b57-5db6-459d-ae4d-c6d9d7dd2cc6
    Branch: claude/issue-11544-invite-entry-members-tab
    Worktree: objectstack-issue-11544
    Domain: domain:engine
    File surface: packages/platform-objects/src/identity/sys-member.object.ts, packages/platform-objects/src/pages/sys-organization.page.ts; read-mostly: packages/platform-objects/src/identity/sys-invitation.object.ts (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus — node scripts/pm/dispatch-gates.mjs --tier @ a2ec377: "no path-derived mandate … tier stays the PM's per-card judgment call"; judgment card ⇒ default judgment tier
    Clause-②: no
    Serial constraints cleared: PR #11538/#11537/#11536/#11533 et al. checked — only #11530 (claude/issue-9968, retires set_user_role from sys_user) touches the same package platform-objects/src/identity/; different file, no region overlap, no barrel edits on either side. Dev must merge origin/main before opening the PR if #11530 lands first. No in-flight claim on this card's files found.

    Audit (direct-dispatch): maintainer instruction, 2026-08-24 chat, quoted verbatim: 「使用 ../objectstack 项目经理技能派发这个任务」. Routing and dispatch of this single card are performed under that instruction; triage may re-check domain:*/type on next sweep.

  3. added a commit that references this issue on Aug 24, 2026
    da42adc
  4. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 11544,
      "status": "done",
      "branch": "claude/issue-11544-invite-entry-members-tab",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11562",
      "premise_still_valid": true,
      "summary": "sys_member now declares its own invite_user on list_toolbar, ahead of add_member, so the email-invite flow is reachable from the org record page's default Members tab; add_member is differentiated in chrome only (variant secondary + icon link-2), behaviour and label untouched. PM assumptions 1 and 3 CONFIRMED against objectui at bc21c704b (deriveActions assigns the whole filtered list with no cap; the bridge dispatches runRowAction with an undefined row). Assumption 2 REFUTED in its literal form: sys_member has no `email` field, so the sys_invitation copy cannot be mirrored unchanged -- resolveActionParams answers an unresolvable field-backed param with a type:'text' fallback labelled by the raw field name, an ADR-0078 valid-but-inert degradation with nothing red. The fix stayed inside platform-objects via the existing authorable objectOverride key (same device sys_user already uses for its role half), so NO spec schema change was needed and the card's premise held. DECLARED DEVIATION: one line of registry bookkeeping in packages/spec/src/kernel/public-auth-features.ts. feature-gate-guard.test.ts enforces bidirectionally that every features.* reference is booked in gatedInputs, so declaring requiresFeature:'organization' mechanically obliges the entry -- the twelve siblings beside it are the same obligation discharged; the alternatives were an ungated button that 404s (Prime Directive #10) or an undelivered card. No schema, export, authorable key or accept/reject behaviour moved, and check:generated reports 14/14 artifacts unchanged. Flagged for PM adjudication rather than decided quietly, since the card was claimed Clause-2: no.",
      "tests": "All on merge commit 22ecd75cb (the PR head); every exit code captured before any pipe. pnpm --filter @objectstack/platform-objects test -> EXIT=0, 'Test Files 29 passed (29) / Tests 476 passed (476)'. typecheck (platform-objects + spec) EXIT=0. check:i18n EXIT=0 ('all bundles in sync, no undeclared authoring keys'). spec check:generated EXIT=0 ('All 14 generated artifacts are up to date' -- no regeneration, the mechanical proof the public surface did not move). Also green: 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-major. Pre-merge, the rest of the dispatch-gates 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, spec check:empty-state / check:liveness / check:strictness-ledger / check:variant-docs. NEW TEST: identity/invite-entry-toolbar.test.ts, 21 cases; its load-bearing case is stated over all THREE mirrors, not just the broken one -- every field-backed param of every invite_user copy must name a field that really exists on the object it resolves against, which closes the class. ABLATION (predicted direction: red; observed: red): sys-member.object.ts reverted to origin/main, everything else intact. Mutation PROVEN ON DISK before any result was read -- grep counts for name:'invite_user', objectOverride:'sys_invitation' and icon:'link-2' each went to 0 and the pre-fix icon:'user-plus' on add_member returned. Mutated: 10 failed | 11 passed (21). Restored: 21 passed (21), git status --porcelain empty on the path (byte-exact restore), trap fired. NO REBUILD was needed for either leg and this is why: these suites import the object modules by RELATIVE SOURCE path, so no dist/ sits between the mutation and the assertion -- the exports-resolution precondition for a stale-dist false green does not hold here. The 11 green-under-mutation cases are the sys_user / sys_invitation mirror rows the revert did not touch. DECLARED NARROWINGS (host capability, not diff findings): this seat ran on macOS bash 3.2.57 with no `mapfile` and no `flock`. scripts/pm/os-verify-lock.sh refused with its own line 'VERDICT lock-unusable (exit 99) - never acquired - refused before waiting - nothing was built or tested', so every build and test ran UNLOCKED. check:objectui-changeset EXIT=1 with 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 (0 hits in git status) and the self-test runs on temp fixtures, so it is a host gap, not a diff finding. scripts/pm/os-regen-merge.sh could not run for the same reason; its documented order was followed by hand, no merge=os-regen path is on either side of the merge, and no deferral marker is outstanding. check-dev-prereqs EXIT=1 reporting the workspace unbuilt (12 of 67 packages lack a dist/ entry point -- client-react, studio, embedder-openai and others, none touched here); that is a consequence of building only the affected closure per resource discipline, NOT a verdict about this diff. check:type-check-debt --re-measure was NOT run: it requires the full workspace closure built, which the targeted-build discipline deliberately does not produce; CI runs it.",
      "open_questions": [
        {
          "question": "The card was claimed Clause-2: no with an instruction to stop if packages/spec had to be touched. No spec SCHEMA change was needed, but the feature-gate completeness guard mechanically obliges one line in PUBLIC_AUTH_FEATURES.organization.gatedInputs. Does that ledger append count as clause-2 territory for enqueue purposes?",
          "options": [
            "A: treat it as bookkeeping and let the PR proceed -- no schema, export, authorable key or accept/reject behaviour moved, check:generated reports 14/14 artifacts unchanged, and the twelve sibling entries plus the just-landed #11530 (which edits this same list for the same reason) establish the shape",
            "B: re-dispatch or re-tier the card as clause-2 because the diff touches packages/spec/src/** at all",
            "C: split the ledger line into a separate PR -- rejected on inspection: the guard is bidirectional, so a platform-objects-only PR would be red on its own and a spec-only PR would be red on its own; neither half can land alone"
          ],
          "recommendation": "A. Real business need: the maintainer personally ordered this outcome and the button cannot ship gated without the entry. Long-term soundness: the entry is contract-first -- it is what makes 'declared = enforced' hold for the new gate, and omitting it would mean shipping either an ungated 404-ing button or an unbooked gate the guard exists to catch. Hard to get wrong for AI authors: the ledger is the structural record that stops the next mirror shipping unbooked, which is the opposite of a consumer-side tolerance. Startup scope discipline: it is one line appended to an existing list with no new surface, no new capability and no new authorable key. C is not available on the mechanism, and B costs a re-dispatch for a line whose correct form has zero latitude. The enqueue gate judges the actual diff regardless, so A is also the option that keeps the mechanical check in the loop rather than substituting my judgement for it."
        }
      ],
      "out_of_scope_findings": []
    }
  5. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    ContributorAuthor

    ACCEPT — PR #11562

    Review of record (verified against GitHub, not the report's self-description):

    • PR shape: draft ✓, base main ✓, body first line Fixes #11544 ✓. Changed files = claimed surface + declared deviations only; no governed-face paths.
    • Spot readings: the sys_member invite_user mirror carries the same target/params/requiresFeature: 'organization' as its two siblings; the email param's objectOverride: 'sys_invitation' uses the pre-existing spec key (action.zod.ts:177 on origin/main; same device as sys_user's role half) — zero spec schema change, as claimed. add_member differentiation is chrome-only (secondary + link-2), label untouched with a defensible i18n rationale. Ablation evidence (mutation proven on disk, 10 fail → restore 21 pass, relative-source imports rule out stale-dist false greens) is the strong form.
    • Declared narrowings acknowledged: verify-lock unusable on this host (bash 3.2, no flock) — gates ran unlocked; check:objectui-changeset red is a host mapfile gap, not a diff finding; check:type-check-debt --re-measure deferred to CI. All accepted as environmental.

    Clause-② adjudication (the open question): Option A's outcome, via the gate — not around it. The enqueue gate judges the actual diff, and the diff touches packages/spec/src/**, so the path limb fired regardless of the card's Clause-②: no (which remains a correct content-limb claim: no accept/reject behaviour moved, no surface widened — check:generated 14/14 unchanged). needs:contract-review was hung on both carriers and the review chain ran; it is now cleared by review, below. Option C is confirmed impossible (the guard is bidirectional — verified forward:/reverse: describe blocks in feature-gate-guard.test.ts), and B would re-dispatch for a line with zero latitude.

    Contract review (needs:contract-review → PASS): reviewed the contract increment — one gatedInputs entry 'sys_member.actions.invite_user' appended to PUBLIC_AUTH_FEATURES.organization in exact sibling shape; it is the booking that keeps declared = enforced for the new gate; no schema, export, or authorable key moved. Reviewer tier: this dispatch seat, machine-verified at CONTRACT_REVIEW_TIER (claude-fable-5) — host lacks the claude-code-remote get_session reading; substitute machine reading declared: the session transcript's harness-recorded served-model field (claude-fable-5, last 3 records). Self-review of an own-dispatched card per the 2026-08-21 relaxation (the seat wrote no code on this card).

    Next: gate jobs → green ⇒ ready + auto-merge (queue). Consumer follow-up: cloud-side .objectstack-sha bump card to be filed on acceptance per multi-repo rule 3.

  6. claude commented on Aug 24, 2026

    @claude
    Contributor

    Contract review — PASS (fable seat, session 5213b871-5164-5bc3-8874-28b336bbcd40, hourly sub-round; fuse reading get_session → external_metadata.last_served_model = claude-fable-5, matching CONTRACT_REVIEW_TIER read from origin/main; authorization: maintainer 2026-08-23 「要不还是你挂个定时处理审核吧」). Independence: not this seat's dispatch — independent review.

    Reviewed PR #11562 @ 22ecd75cbd against the actual diff.

    • The affordance lands as pure metadata: sys_member mirrors invite_user via the EXISTING authorable objectOverride key (the same mechanism sys_user's copy already uses) — assumption 2's refutation is the substantive finding: an unchanged mirror would have shipped an ADR-0078 valid-but-inert dialog (unresolvable email param silently degrading to an untyped text box). No schema, no export, no authorable key moved — check:generated reports all 14 artifacts up-to-date, the mechanical confirmation the public surface did not move.
    • The one packages/spec line is bookkeeping the guard mechanically obliges: feature-gate-guard.test.ts enforces bidirectionally that a requiresFeature-gated action is booked in PUBLIC_AUTH_FEATURES — the alternative was an ungated button that 404s where the capability is off. The dev flagged it for this gate rather than deciding quietly — correct posture, and this gate judges it: registry booking, not clause-② accept/reject movement.
    • The class gets closed, not just the instance: the new test's load-bearing case quantifies over ALL THREE invite_user mirrors — every field-backed param must name a real field on its resolution object — so the next inert-dialog copy lands red instead of silent.
    • Chrome differentiation verified as real (variant/icon both render through measured paths); UI-side bridge behavior read-only measured against objectui, nothing edited there; ablation predicted red, observed red.

    Verdict: PASS. Clearing needs:contract-review on both carriers. Enqueue/flip belongs to the dispatching seat's landing window.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions