Skip to content

fix(app-shell): approver membership-tier picker reads the server-published enum; delegated_admin stops rendering "(invalid)" - #5327

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-5309-membership-tier-server-enum
Aug 19, 2026
Merged

fix(app-shell): approver membership-tier picker reads the server-published enum; delegated_admin stops rendering "(invalid)"#5327
os-support-ai merged 2 commits into
mainfrom
claude/issue-5309-membership-tier-server-enum

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#5309

The approver membership-tier select hardcoded three tiers. delegated_admin could not be picked, and a spec-valid stored value rendered as delegated_admin (invalid) — a legitimately-saved approver labelled invalid to the author's face.

What changed

ReferenceCombobox's org-membership-level branch now resolves its vocabulary through a new exported membershipLevelOptions(source):

  1. The server-published enum wins.resolveRefKind's source.values (the spec's xRef.sources annotation, carried through by json-schema-to-fields) is used verbatim — order included. This is the same precedence rule the file already states for record lookups in recordLookupFor, and it is the only source that cannot drift from what the engine accepts.
  2. The hardcoded list survives only as the pre-annotation fallback — and is no longer hardcoded. It derives from BUILTIN_MEMBERSHIP_ROLE_OPTIONS (@objectstack/spec/identity), which that package documents as "the picker's vocabulary" and ships with labels. Same move, and the same reason, as KIND_TO_RECORD_LOOKUP (objectui#3017).
  3. A tier this pin has never heard of is humanized rather than rendered as a raw token, so the next vocabulary addition reaches authors without an objectui release.
  4. A published-but-empty enum falls back instead of rendering an empty strict select — a select with nothing in it traps the author, the one thing this control must never do.
  5. (invalid) keeps its meaning: a value outside the vocabulary the server actually published is still flagged.

Label echoes updated in flow-node-config.ts. The type-picker label became Organization membership tier rather than being re-spelled with four values: enumerating the vocabulary inside a label is the same staleness class this card is about.

The pin fork window — measured, and it does not bind

Triage flagged that this repo's spec pin may not cover objectstack#9942. Measured: it does not. This repo pins @objectstack/spec@17.0.0 (lockfile-exact), and:

APPROVER_VALUE_SOURCES.org_membership_level -> {"source":"enum","values":["owner","admin","member"]}
ORG_MEMBERSHIP_LEVELS: null (not exported)
BUILTIN_MEMBERSHIP_ROLES: ["owner","admin","delegated_admin","member"]

So the approver projection still publishes three values and nothing here asserts it — that pin would be red for reasons unrelated to this change.

But the same pin already carries BUILTIN_MEMBERSHIP_ROLE_OPTIONS as the complete four-value list with labels, including {label: "Delegated Admin", value: "delegated_admin"}. Deriving the fallback from that — upstream's own source for the #9806 ruling that ORG_MEMBERSHIP_LEVELSisBUILTIN_MEMBERSHIP_ROLES — makes the fix and its tests fully assertable today. After the pin bump the two derivations converge, so nothing here needs revisiting.

No spec pin bump is in this PR, per the standing rule that a bump is never a rider.

Verification

Re-derived on origin/main @ 6e6884aad first. All results below are at 528bfe5f1.

  • New FlowReferenceField.membershipTier.test.tsx11 passed.
  • packages/app-shell/src/views/metadata-admin/inspectors/55 files, 609 passed, 1 skipped.
  • Full packages/app-shell/450 files, 4343 passed, 1 skipped, 0 failed (source tree identical to 528bfe5f1; the only later change was the changeset .md).
  • pnpm --filter @object-ui/app-shell type-check — clean. lint — 0 errors; the 3 warning sites in the touched file are outside every diff hunk.
  • Gate union re-run at 528bfe5f1: check:control-bytes, check:spec-symbols, check:esm-specifiers, check:phantom-deps, check:self-import, check-changeset-no-major — all pass.

Vitest was run from the repository root throughout.

Build artifact between the edit and the thing under test

Stated explicitly because this leg reads a value published by @objectstack/spec:

  • The objectui side has no artifact in the path. The root vitest.config.mts aliases every @object-ui/* specifier to that package's src/, and the test imports ./FlowReferenceFieldrelatively — so vitest transforms the edited source directly. No dist of app-shell participates.
  • The spec side is a prebuilt artifact I do not build: @objectstack/spec has no vitest alias, so it resolves to the published node_modules/@objectstack/spec/dist/. That artifact is the pin under test, which is exactly what production consumes.
  • The dependency closure was stale and it showed: type-check first failed with Cannot find module '@object-ui/react' until pnpm --filter '@object-ui/app-shell^...' build ran. (That run's exit code also read 0 through the pipe while the output said Exit status 2 — the result was read from the output, not the code.)

Reverse verification — four legs, predictions declared before each run

Predictions were written down first; all four legs matched exactly. A wholesale revert would have deleted the exported membershipLevelOptions and broken the file's import — every test red, discriminating nothing — so each half of the fix was ablated separately, then together.

LegPredicted redObserved red
A — server enum ignored4: verbatim order; NARROWER; humanizes; server-says-outsideexactly those 4
B — fallback re-hardcoded to three3: derived-fallback; delegated_admin on fallback; DOM no-sourceexactly those 3
C — empty-vocabulary guard dropped1: falls back rather than empty selectexactly that 1
A+B — combined = origin/main behaviour7 (adds DOM server-publishes-enum)exactly those 7

Leg B reproduced the reported symptom verbatim — the DOM dump contains:

delegated_admin (invalid)

Note leg A vs A+B on still flags a value the SERVER says is outside its vocabulary: red under A, green under A+B. With the three-value list the tier genuinely is outside the vocabulary, so the flag is correct there for the wrong reason — which is why the combined leg is reported rather than leg A alone.

Assertions that pin nothing about this change, stated rather than counted:

  • still flags genuinely dirty legacy data (sales_manager) — green on all four legs. It guards against a fix-by-deletion of the (invalid) affordance, not against this regression. Annotated as such in the test.
  • falls back for a source that is not an enum at all — green on all four legs; it pins the type narrowing, which no leg exercises.

Every other assertion is red on at least one leg. Each leg was restored with git checkout HEAD -- FILE from the committed fix, and the restore was verified byte-identical (git status --porcelain empty) before the next run; the restored tree re-ran green at 11/11.

Counter-probes: toContain('delegated_admin') is paired with not.toContain('sales_manager') on the same array, so the positive assertion is shown capable of failing.

Scope

packages/app-shell/** only. In-flight #5254's surface was checked before starting — it is packages/components/** (renderers/form/* + its tests) and does not reach packages/app-shell, so no breach.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)25.3 KB350 KB
Entry fileindex-CkcCe5pW.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.83KB3.70KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.33KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.79KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.71KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.75KB113.40KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.34KB
fields (index.js)237.07KB59.46KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.42KB1.39KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)29.43KB7.15KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)39.16KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)127.92KB32.80KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)241.46KB60.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)124.19KB30.20KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)198.22KB53.28KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.66KB27.13KB
plugin-map (index.js)20.08KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.55KB20.67KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)36.10KB12.26KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-support-ai
os-support-ai marked this pull request as ready for review August 19, 2026 15:26
@os-support-aiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — PM review, round 17.

  • Path surface (git diff --name-only origin/main... @ 528bfe5, base 8477be5): 5 files — FlowReferenceField.tsx, flow-node-config.ts, two sibling tests, one changeset. All inside packages/app-shell/src/views/metadata-admin/inspectors/**; zero governed-surface hits.
  • Gates: every gate job completed: success — Lint, Type Check, Test shards 1–4, Build & E2E, Build Docs, Doc Snippet / Doc Component Type Check, Changeset Declaration / Bump Policy / Fixed Group, Bundle Analysis, Control Byte Scan, Internal Docs Link Check, Skill Guide Path Check. No cancelled, no in_progress.
  • Bump: patch on @object-ui/app-shell. Correct — no contract moves.
  • The pin trap was measured, and the answer was to route around it rather than to bump. This repo locks @objectstack/spec@17.0.0, whose APPROVER_VALUE_SOURCES.org_membership_level still publishes exactly ['owner','admin','member'] — so objectstack#9942 is not in this pin, and nothing here asserts that projection. The same pin already ships BUILTIN_MEMBERSHIP_ROLE_OPTIONS with all four values and labels, so deriving the fallback from upstream's own source makes the fix assertable today, with no spec bump in this PR. That is the right call: a pin bump as a rider is exactly what the protocol forbids.
  • Four ablation legs, predictions written before each run, all four matched exactly (4 red / 3 red / 1 red / 7 red). The wholesale revert was correctly rejected as non-discriminating — it would have deleted the export and turned every test red, proving nothing. The rebuild question was answered, not assumed: the root vitest.config.mts aliases @object-ui/* to src/ and the test imports relatively, so no dist sits in the path, and the red results are themselves the proof the edit reached the test.
  • Two assertions declared as pinning nothing (green on all four legs) rather than counted as coverage. That is the honest form of an ablation report.

Merging via the queue.


Generated by Claude Code

@os-support-ai
os-support-ai added this pull request to the merge queueAug 19, 2026
Merged via the queue into main with commit c7a74c8Aug 19, 2026
22 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-5309-membership-tier-server-enum branch August 19, 2026 15:27
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Approver membership-tier select hardcodes three tiers — delegated_admin is not offered and a stored value renders "(invalid)"

2 participants

@os-support-ai@claude