Uh oh!
There was an error while loading. Please reload this page.
fix(app-shell): publish the shared current_user normaliser from RecordFormPage - #6540
Conversation
…cordFormPage` (#6515) `RecordFormPage` built its own predicate identity — `{ name, email, role, positions }` — instead of calling `buildExpressionUser`. Against the normaliser that shape is missing `id` and `isPlatformAdmin`, both named by real gates (`ctx.user.isPlatformAdmin == true` on `sys_environment`'s "Change Plan (admin)"; `record.id == ctx.user.id` throughout `sys_user`). An absent key is not `false`: the predicate FAULTS, and a faulting visibility predicate fails OPEN, so the gate silently did not bite. The signed-out branch diverged on its own account too — it carried no `isPlatformAdmin` key at all. The normaliser moves from `console/AppContent.tsx` to `providers/expressionUser.ts`, a leaf module beside the `ExpressionProvider` it feeds. That move is what makes the fix available at all: this view is `lazy()`-loaded BY `AppContent`, so importing the normaliser from its old home would put a static edge from the split chunk back into the module it was split out of — the edge `check-eager-closure-budget` weighs. `console/AppContent.js` and the package entry both re-export the name, so nothing published moved. Fail-open on a genuine evaluation error is deliberately unchanged (objectui#6443 / #6487 / #6445); what changed is that these predicates no longer fault. Adds the shape pin objectui#6110's contract implied but never got: a render-level assertion that a mount site publishes exactly `buildExpressionUser(session)` under all four identity spellings, plus a source ratchet that refuses a NEW mount site which derives the descriptor by hand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
✅ Console Performance Budget
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
Size Limits
|
os-support-ai
commented
Aug 26, 2026
ACCEPT — objectui#6515 ( The card existed because the repair is a module move that a gate weighs, not a one-line import. That is exactly how it was treated. The budget question, answered with a measurement rather than a hopeI forwarded a figure mid-flight: eager closure 3234.1 KB against a 3266.6 KB budget, 32.5 KB of headroom, ~1%. The dev took its own base reading before any edit and got After the move: The chunk-name set is the reading that matters, not the +242 B. The risk was a new source edge pulling a chunk into the eager closure; an identical name set says that did not happen. A byte delta alone could not distinguish "no new edge" from "a new edge that happens to be small." ⭐⭐⭐ And the measurement was nearly not a measurement. An earlier The move itself
Published name proven unmoved on the built Face is seven files; ⭐⭐⭐ The load-bearing result is a GREEN, not a redSix predicted REDs observed, including the one that shows the actual harm: But the finding that justifies the card's existence is a green:
The existing pins could not see this defect. #6493 bound the right roots and its suite was honest about what it measured; the shape underneath was simply outside its reach. That is why "the area is already covered by tests" is never an argument that a defect is not there — and it is the cleanest demonstration of a blind instrument I have reviewed today, precisely because nobody wrote it blind on purpose. Also correctly noted: "grants the isPlatformAdmin gate to a platform admin" was predicted green and stayed green, because a fault and a genuine Scope disciplineTightening only — no gate relaxed, no accept set widened, fail-open on a genuine evaluation fault left alone per #6443/#6487/#6445. Two findings, one of which is bigger than this card
Both unlabelled/ungraded; ⛔ triage's to route. Landing on green once CI settles. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#6515
All measurements below were taken on
3e8d63500, the final commit of this branch, against base28ed9ce8f.The defect
packages/app-shell/src/views/RecordFormPage.tsxbuilt its own predicate identity instead of calling the shared normaliser:Against
buildExpressionUserthat is missingidandisPlatformAdmin. Both are named by real gates —ctx.user.isPlatformAdmin == truegatessys_environment's "Change Plan (admin)" action, andrecord.id == ctx.user.idis the shapesys_user's gates use throughoutplatform-objects. An absent key is notfalse: the predicate FAULTS, and a faulting visibility predicate fails OPEN, so the gate silently did not bite. The signed-out branch diverged on its own account too — it carried noisPlatformAdminkey at all, wherebuildExpressionUser(null)carriesfalse.This is a tightening and stays one. No gate was relaxed, no accept set widened, and fail-open on a genuine evaluation error is untouched (#6443 / #6487 / #6445 preserved it deliberately). What changed is that these predicates no longer fault in the first place.
The repair
The normaliser moved from
console/AppContent.tsxtoproviders/expressionUser.ts— a leaf module (it imports nothing) beside theExpressionProviderit feeds. That move is what makes the fix reachable: this view islazy()-loaded byAppContent, so importing the normaliser from its old home would put a static source edge from the split chunk back into the module it was split out of, which is whatcheck-eager-closure-budgetweighs.console/AppContent.jsand the package entry both re-export the name.File face (exact)
packages/app-shell/src/providers/expressionUser.tspackages/app-shell/src/console/AppContent.tsxpackages/app-shell/src/index.tspackages/app-shell/src/views/RecordFormPage.tsxbuildExpressionUser(user)packages/app-shell/src/providers/expressionUser.mountParity.test.tsxpackages/app-shell/src/providers/expressionUser.mountSites.ratchet.test.ts.changeset/6515-record-form-current-user-normaliser.mdservices/MetadataService.tsis not in this face — no collision with #6488.The eager-closure budget, before and after
Both figures from a built console in this worktree. The base reading was taken before any edit; the after reading from a forced clean rebuild (
rm -rf apps/console/dist && turbo run build --filter=@object-ui/console --force), which emitted a different entry-chunk hash (index-CNepzyiI.js→index-BE-9TM9n.js) — so it is a real build, not a replayed cache. A first, non-forced rebuild independently produced the identicaleagerGzipBytes.28ed9ce8fvendor-objectstackframeworkui-componentsThe gate's own verdict line on this branch:
Per-chunk movement (only non-zero rows):
index.js−427 B — the normaliser's bytes leavingAppContent's chunk — againstRecordFormPage.js+41 B,RecordDetailView.js+635 B and three chunks at +1/+2 B, which is rolldown re-balancing module→chunk assignment. No new chunk entered the eager closure, which is the directional result that matters: the edge went the safe way.The published name did not move
A source
exportkeyword is not the test, so this was checked against the built.d.ts, with each leg rebuilt from its own tree (distIS the artifact under test here) and the type resolved through the TypeScript compiler API the way a consumer's compiler resolves it:28ed9ce8f)3e8d63500)truetrue(user: unknown) => Record<string, unknown>(user: unknown) => Record<string, unknown>dist/console/AppContent.d.tsdist/providers/expressionUser.d.tsOnly the internal declaration file moved — invisible to a consumer. A runtime identity pin (
viaAppContent === buildExpressionUser,toBenottoEqual) guards the re-export against becoming a second copy that could drift.The pin #6110's contract implied
Two halves, and neither is sufficient alone:
expressionUser.mountParity.test.tsx— a probe standing in forObjectForminside the page's ownExpressionProviderreads the predicate scope that provider actually publishes, and assertstoStrictEqual(buildExpressionUser(session))across all four ADR-0068 D1 spellings (current_user,user,ctx.user,os.user). That is a shape comparison against the normaliser's live output, so it fails for a site that hand-rolls a descriptor by any route — including one that calls the normaliser and then spreads extra keys over it, which a spy on the function could not see.toStrictEqualso a key written as explicitundefinedfails too (the exact defect class Four more client sites still read the retireduser.roles— role-addressed approvals andrequiredRolesgates degrade silently at protocol 17 #5424 measured on this object).expressionUser.mountSites.ratchet.test.ts— enumerates every<ExpressionProvider user={…}>andcreateExpressionEvaluator({ user: … })inpackages/app-shell/srcandapps/console/srcfrom source and refuses one whose expression does not trace tobuildExpressionUser. That is what catches a third site added tomorrow, which no render test would know exists.Reverse verification — direction predicted before running, every prediction held
Mutation: restore the hand-rolled descriptor by checking
RecordFormPage.tsxout at the pinned base blob. Proved on disk before measuring —git hash-objectmatched the base blob exactly, and both text counts moved (buildExpressionUser(user)1 → 0;role: user.role ?? 'user'0 → 1). Restore was by hash against the pinned commit3e8d63500(neverorigin/main, which other worktrees move), verified bygit hash-objectback to the pin blob and an emptygit diff HEAD; the script carried atrap … EXIT INT TERMwith absolute paths.No
distsits between mutation and assertion on this leg — the root vitest config aliases every@object-ui/*topackages/*/src, and the view is imported by relative path, so the tests read the mutated source directly. (The.d.tscomparison above is the one leg wheredistis the target, and both of its legs were rebuilt.)RED, exactly as predicted (6):
buildExpressionUser(session)expected { name: 'Ada', …(3) } to strictly equal { id: 'u_admin', name: 'Ada', …(4) }buildExpressionUser(null)isPlatformAdmin-gated field from a non-adminexpected [ 'name', 'plan', …(2) ] to not include 'plan'— the admin-only field was on screen for a sales clerkctx.user.id-gated field from the user it excludesisPlatformAdmingate from a signed-out visitorRecordFormPage.tsx [ExpressionProvider]and[createExpressionEvaluator]GREEN both ways, also as predicted:
grants the isPlatformAdmin gate to a platform admin(fail-open and a correcttrueare indistinguishable at the call site, which is exactly why the excluded-user cases carry the pin), the published-name pins, and — the load-bearing one — all ofRecordFormPage.predicateScope.test.tsx, #6493's own pins. The hand-rolled shape still hadpositions, so #6493's tests cannot see this defect. That is the whole reason this card exists.Verification run
vitest runoverpackages/app-shell/src/providers/,RecordFormPage.predicateScope,RecordFormPage.i18n,AppContent.expressionUserShape,apps/consoleinternalFormShell+FormPage.predicateScopepnpm --filter @object-ui/app-shell --filter @object-ui/console type-checktype-check: Donefor bothpnpm --filter @object-ui/app-shell lint(the whole package — every changed file is in it)check:eager-closurecheck:control-bytes,check:self-import,check:phantom-deps,check:esm-specifiers,check:entry-guard,check:vi-mock-specifiers,check:published-dist,check:node-esm-loadcheck:readme-exports0 unbuilt, 0 fabricated. First run reportedELIFECYCLE 1naming@object-ui/cliand@object-ui/plugin-aias not built in this worktree; that is a prerequisite not met, not a red gate. Building those two turned it green.packages/app-shell/tsconfig.test.jsonreally does cover the new tests —tsc --listFilesshows 3 hits underproviders/expressionUser*, so "typecheck is clean" is a statement about them.Out of scope, filed rather than widened
buildExpressionUser(null)omitsid, so actx.user.idgate faults and fails OPEN for every signed-out visitor #6534 —buildExpressionUser(null)omitsidentirely, so actx.user.idgate faults and fails open for a signed-out visitor at every mount site,AppContentandInternalFormRouteincluded. Surfaced by this card's own shape pin, but it lives in the normaliser's anonymous branch and closing it changes the shapeAppContent.expressionUserShape.test.tspins — a decision (id: nullvsundefinedvs absent), not a mechanical fix. Today's behaviour is pinned here, in a case explicitly namedRECORDS:, so the follow-up has a red test to turn green.AppContentdeclareslazy()are in the EAGER closure anyway — the package barrel re-exports them and the entry imports it statically #6535 — 6 of the 8 viewsAppContentdeclareslazy()are in the eager closure anyway; the entry chunk imports them statically through the package barrel. Observational. It does not change this repair — the source-graph edge the budget gate weighs is real either way, and the leaf module inproviders/is the right shape regardless — but the adjacent belief that these chunks are off the first-paint path is measurably false.Generated by Claude Code