Skip to content

fix(approvals): derive approver identities from positions, not retired user.roles - #5555

Merged
os-sales merged 3 commits into
mainfrom
claude/issue-5424-user-roles-retired-readers
Aug 21, 2026
Merged

fix(approvals): derive approver identities from positions, not retired user.roles#5555
os-sales merged 3 commits into
mainfrom
claude/issue-5424-user-roles-retired-readers

Conversation

@claude

@claudeclaudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Refs #5424 — sites 2, 3 and 4 only. Site 1 (AuthGuard's requiredRoles) is
deliberately NOT touched and is deferred pending a maintainer ruling, and so is
the roles?: string[] declaration in packages/auth/src/types.ts (site 1 is
still its only live reader, so it cannot retire until site 1 is decided).

What landed

Framework ADR-0090 D3 renamed the session's roles key to positions with no
deprecation window, and the protocol-17 session face emits no roles key at
all. Three client read sites still followed the old spelling:

Site 2 — packages/app-shell/src/hooks/sharedUserFeeds.ts (the real injury).
approverIdentities() read u?.roles ?? [] and nothing else, so it emitted
no role: identity at all. Approvals addressed to a position rather than to
a person matched nothing and vanished from the bell badge, the bell's Approvals
tab and Home's To-do card — silently, with no error and no empty-state copy.
Now reads positions.

Site 3 — apps/console/src/services/approvalsApi.ts.
buildApproverIdentities() also splits the scalar user.role, so it degraded
rather than dying: on the measured payload it still yielded role:user while
dropping every business position name. Now reads positions; the scalar role
split stays (protocol 17 still emits role, and it is a separate identity
source that this card is not about).

Site 4 — packages/app-shell/src/console/AppContent.tsx.
Dropped roles: (user as any).roles from the expression user. It was always
undefined, so 'manager' in current_user.roles got a context that answered
wrongly rather than one plainly missing the key. positions and
isPlatformAdmin were already forwarded correctly beside it. The signed-out
branch never carried roles, so removing it also makes the two branches agree
on one shape.

The retired spelling is not kept as a fallback anywhere —
packages/auth/src/types.ts forbids pairing the two in so many words, and each
site now carries a pin that fails if the old key is ever readable again.

Verification

Every pin is shaped to fail because of the empty collection, not merely to
assert "it reads positions" — that kind of assertion is green against the
broken code too, which also read a key and also produced a request. Site 2's
pins assert on the identities that actually reach the wire (the approverId
query the server matches pending_approvers against).

Verified tree: 64330ca49 — the final commit; the union below ran on it
with a clean working tree.

checkresult
vitest run — app-shell hooks + AppContent.* + console services + ApprovalsInboxPage27 files, 253 tests, 0 failed
pnpm --filter @object-ui/app-shell --filter @object-ui/console type-checkexit 0 (both echoed type-check$ tsc --noEmit …)
pnpm --filter @object-ui/app-shell --filter @object-ui/console lintexit 0 — 201 + rest warnings, 0 errors
pnpm check:control-bytes✅ OK (scanned 4604 tracked text file(s))
node scripts/check-changeset-presence.mjs✅ 6 source file(s) of 2 released package(s) changed, 1 changeset
node scripts/check-changeset-no-major.mjs✅ No changeset declares a major bump

Dependency closure (--filter '@object-ui/app-shell^...' --filter '@object-ui/console^...' build) was built first — type-check is tsc --noEmit and reads dependency .d.ts.

Reverse verification (predictions written and committed BEFORE running)

All three sites were reverted to the roles spelling in one mutation, the
mutation was proved on disk with anchored grep -c on both the injected and
the deleted text (not on a bare diffstat, and not on the editor's exit code),
and the script restored via a trap … EXIT INT TERM. These suites resolve the
subject through vitest's workspace src aliases, not through dist, so no
rebuild is involved on either leg; the restore leg was confirmed by a clean
git status.

Result: 8 red, 11 green. Site 2's pin went red in exactly the shape the card
describes:

AssertionError: expected [] to deeply equal [ 'role:manager', …(1) ]
- [ "role:manager", "role:platform_admin" ]
+ []

and site 3's reproduced its own description verbatim — expected [ 'role:user' ] to deeply equal [ 'role:manager', 'role:finance_approver', 'role:user' ], i.e.
"yields role:user and loses every business position name".

Two predictions were wrong, and are recorded as wrong in the test docblocks
rather than quietly re-written:

  1. The site-3 de-duplication case was predicted GREEN both ways; it goes
    RED. Its fixture supplies manager only through positions, so the retired
    read drops it and the overlap it means to exercise never forms. It is a real
    assertion about the fixed behaviour, but it is not the independent control
    the prediction claimed.
  2. The two "does not resurrect the retired roles key as a fallback" cases
    were not named in the predictions; both go RED. That is correct and welcome
    — the ablation is precisely what makes the ghost key legible again, so those
    anti-alias pins are load-bearing rather than decorative.

Negative controls held: a user with neither positions nor role produces no
role identity, no role:undefined, and no throw; a session with no id issues no
request at all.

One disclosure — a new lint warning I did not suppress

Site 4's pin needs the expression-user construction to be reachable, so it was
extracted from AppContent's body into an exported buildExpressionUser().
That adds onereact-refresh/only-export-components warning to
AppContent.tsx (a warning, not an error; lint exits 0). The rule's own
remedy is "use a new file", which is outside this card's file fence, so I left
the warning visible rather than silencing it with a disable comment — it is an
accurate reminder that this function's long-term home is its own module. Happy
to move it if you'd prefer that in this PR.

Out of scope, filed separately

packages/auth/src/AuthProvider.tsx:244produces the retired key: the
preview-mode synthetic user is built as { role, roles: [role] } with no
positions. It is a producer rather than a reader, it is outside this fence,
and it is entangled with the site-1 ruling (preview mode is what still feeds
AuthGuard). Noted for triage rather than fixed here.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3784.9 KB3867.2 KB
Main entry chunk (gzip)151.5 KB350 KB
Entry fileindex-Bxo7Ybi8.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)8.91KB2.99KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)6.35KB2.43KB
auth (index.js)2.77KB1.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.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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.94KB113.63KB
core (index.js)4.51KB1.80KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.33KB
fields (index.js)237.52KB59.62KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)30.51KB7.57KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.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.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
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.72KB18.35KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.51KB32.94KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)242.15KB60.89KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)124.40KB30.26KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.70KB27.17KB
plugin-map (index.js)20.06KB6.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.52KB20.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.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)43.66KB14.77KB
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-sales
os-sales marked this pull request as ready for review August 21, 2026 11:04
@os-sales
os-sales added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 7e89836Aug 21, 2026
23 checks passed
@os-sales
os-sales deleted the claude/issue-5424-user-roles-retired-readers branch August 21, 2026 11:05
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.

2 participants

@os-sales@claude