Skip to content

feat(app-shell,core,permissions): an action predicate can ask current_user.can(object, verb) once the permissions payload has loaded (objectui#4421) - #11208

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-4421-current-user-can-wiring
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-4421-current-user-can-wiring

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #4421 — the objectui half: the binding, its pins and the Rider 2 wording. What stays open on the card is Rider 1 on the fail-soft legs measured below (a decision for the seat, see "Rider 1 — measured per leg"). Server parity is objectstack#18783's.
Clause-②: yes

What this does

An action's visible / disabled predicate can now ask current_user.can(object, verb) and get the caller's object-permission verdict from the loaded /auth/me/permissions payload — the card's case: an app turns the built-in Delete off, ships a logical delete in its place, and gates it with visible: current_user.can('account', 'delete').

The engine half landed as @objectstack/formula@17.5.0 (dyn.can(dyn, dyn), answered from EvalContext.permissions, receiver compared by identity with the bound current_user, loud refusal when the context carries no permission data). This PR is the wiring, through ONE seam:

  • @object-ui/permissions — the permission context carries effectiveObjects: the response's objects map verbatim, or undefined when the provider holds no such response (the role-based PermissionProvider, or no provider). undefined and {} stay distinct.
  • @object-ui/core — evalFieldPredicate, the one place every action surface reaches the CEL engine, hands the acting subject's permissions to ExpressionEngine.evaluate as permissions. The map rides on the subject object under a symbol (bindSubjectPermissions / subjectPermissionsOf, new file evaluator/subjectPermissions.ts): every key of the scope bag is a CEL root and the engine keeps this map out of the variable namespace, while the subject is the one object every bag already carries by reference under all four aliases, through every spread and ExpressionContext. No surface got its own copy of the hand-off.
  • @object-ui/app-shell — ExpressionProvider reads usePermissions() itself and binds the map only while isLoaded is true (useExpressionPermissions, which adapts the payload through the formula package's toEvalPermissions once per payload object, outside React). One subject object under current_user / user / ctx.user / os.user, the one carrying the map. The two imperative field-visibility evaluators (AppContent's record-form modal and RecordFormPage) take the same input, so one predicate answers the same under the provider and beside it (objectui#6493's one-bag rule).
  • @objectstack/formula floor raised to ^17.5.0 in @object-ui/core and @object-ui/app-shell — app-shell now calls toEvalPermissions, which does not exist before 17.5.0. The lockfile moves only the two specifier lines; the resolved version was already 17.5.0.
  • Docs (Rider 2) — content/docs/layout/page-header.mdx gains "Gating an action on the caller's object permissions", ending with the scope sentence: client-side UI gating only, not an authorization boundary, the server still answers 403; server-side evaluation is objectstack#18783.
  • Changeset .changeset/4421-current-user-can-binding.md (minor on the three packages).

Zone 2 — the PM's mechanism assumptions, measured

  1. Installed engine — read in node_modules/@objectstack/formula (17.5.0 dist): registerPermissionPredicate registers dyn.can(dyn, dyn): bool; subject = buildScope(ctx).current_user (which extra can supply); verbs via the spec's resolveObjectPermissionVerb; no permissions in the context ⇒ throws (ok: false, kind: 'runtime', message names the missing input). A probe through extra with a symbol-bearing subject: granted true, denied false, missing map ⇒ fault, all four aliases identical, a COPY of the subject as receiver ⇒ refused. Holds.
  2. Client already holds the payload — MePermissionsProvider holds it but did NOT expose the objects map (only verdict functions). Falsified in part: the producer needed effectiveObjects; the isLoaded gate is applied as assumed.
  3. Rider 1 at the binding via each surface's fault policy — falsified for some legs, table below.
  4. Global usePermissions default — the binding never calls usePermissions().can; it reads effectiveObjects, which the no-provider answer does not carry. Measured in the render pin: with no provider, usePermissions().can('account','delete') answers true while the bound action is hidden. Changing the global default would move no binding verdict, so Rider 1 does not need it. Left untouched (usePermissions.ts is not in this diff); the ruling-text conflict is recorded in the report for the seat.
  5. Rider 2 home — the page that documents header-action visible predicates and the identity scope (page-header.mdx); the section also covers the row menu.

Rider 1 — measured per leg

Three states on one verb (delete): not loaded (no permission provider — the state a mount outside MePermissionsProvider is in permanently), loaded-granted, loaded-denied. Real MePermissionsProvider + real ExpressionProvider + the real surface.

surface / leg not loaded granted denied pinned here
row menu visible (evalRowPredicate, fallback: false) hidden shown hidden yes
record header visible (page:header) hidden shown hidden yes
action:button visible (useCondition, throwOnError) hidden shown hidden yes
action:button disabled: !can(…) (fail-soft, fallback true) disabled enabled disabled no (measured)
record:quick_actions / ActionEngine context hidden hidden hidden no (measured)
action:group inline visible (fail-soft) shown shown hidden no (measured)
action:icon visible (fail-soft) shown shown hidden no (measured)
related-list toolbar visible (RelatedToolbarButton, fail-soft) shown shown hidden no (measured)
record header disabled: !can(…) (fallback: false) enabled enabled disabled no (measured)
  • The bold "not loaded" cells are the legs where Rider 1 does not hold: they apply their existing fail-soft fault policy to the engine's refusal. action:group / action:icon live in packages/components/src/renderers/action/**, serial behind objectui#11185; the others would need a fault-policy change for every faulting predicate on the leg, wider than "fail closed on the no-permissions error". Neither was done here.
  • Reachability: on the console's app routes the not-loaded state does not render — MePermissionsProvider shows its loading screen until the first answer, and after it isLoaded stays true (its refetch effect keys on values that do not change). It is reachable where a predicate scope is published outside that provider: the /forms/:name route (InternalFormRoute mounts its own ExpressionProvider), and standalone embeds.
  • The ActionEngine row: its context is the ActionProvider context, which binds user / ctx.user but never current_user, so can is refused in every state — it never leaks, and it never answers. Out of this card's claimed surface; reported.
  • The measurement for the rows marked "measured" was a throwaway probe, not committed: pinning a fail-open leg as expected would fossilize it.

Tests

Head of the runs: d4bf561239. Commands from the worktree root, each through the shared verify lock; verdicts are the tools' own lines.

  • pnpm exec vitest run packages/app-shell/src/providers/__tests__/currentUserCan-4421.render.test.tsx packages/core/src/evaluator/__tests__/subjectPermissions-4421.test.ts — Tests 25 passed (25).
  • pnpm exec vitest run packages/permissions/ packages/core/ — Test Files 201 passed (201), Tests 3882 passed | 27 skipped (3909).
  • pnpm exec vitest run packages/app-shell/ in shards — see the report for the per-shard verdicts and which shards ran.
  • pnpm --filter @object-ui/core type-check / @object-ui/permissions / @object-ui/app-shell — exit 0 each (after turbo run build --filter="@object-ui/app-shell^...", 28 tasks successful).
  • node scripts/check-changeset-presence.mjs — exit 0, "12 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)".
  • pnpm check:control-bytes — OK. pnpm check:doc-types — "Every documented component type is registered." pnpm check:doc-snippets — "679 of 679 block(s) judged, 0 failed" (after its scoped build). pnpm check:new-line-citations — "0 new citation(s)".
  • No user-facing i18n string was added (the one new console.error is a developer diagnostic), so the i18n gates were not run.

Reverse verification (ablation), direction predicted in the pin's docblock before running. Committed first; the mutation went through ablation-replace.mjs (anchor ...(permissions !== undefined ? { permissions } : {}), deleted from fieldRules.ts; "ok mutation landed: anchor 1 to 0, blob 4028931a8fdc to da7c89d41dab"). Result: Tests 14 failed | 11 passed (25) — every GRANTED arm and every DENIED arm red (the denied arms through their "nothing reported" assertion; hidden either way), every NOT-LOADED arm green, as predicted. Restore: "ok restored: blob == HEAD (4028931a8fdc) and git diff HEAD is empty"; the pins re-ran green after (Tests 25 passed (25)).

The first ablation run disagreed with the prediction on the DENIED arms, and the reason is worth keeping: every fault report on these surfaces is warn-once per (locator, predicate), so with one action name across arms the not-loaded arm's report silenced the same report in the denied arm, and its "nothing reported" assertion passed for a reason unrelated to the verdict. The fix was one action name per arm; the second run is the one quoted above.

Surface notes

  • Beyond the claim's listed files: RecordFormPage.tsx (the same builder's second imperative caller, one-bag rule), packages/permissions/src/{PermissionContext,MePermissionsProvider,PermissionProvider} (the producer of the map — the claim said "if the real producer is elsewhere, fix it there"), and the two package.json floors plus two lockfile specifier lines.
  • In-flight overlap: objectui#11185 (components/renderers/action/**) and objectui#11186 (app-shell/src/hooks/**, layout/**) — this diff touches neither set.

Acceptance notes

  • Not filed, recorded: the throwing useCondition leg reports only "its predicate threw — CEL predicate failed to evaluate: SOURCE", dropping the engine's reason (here "carries no permission data"); and its line prints Predicate: [object Object] for an envelope. The evalRowPredicate family forwards the engine reason. Diagnostics only; no verdict moves.
  • Not filed, recorded: the engine answers from the server's enforcement fold (objectPermissionGrants: === true, absent object ⇒ false, super-user bits folded, export needs allowExport), while the client's MePermissionsProvider.check reads !== false, falls back to a '*' entry, and maps export to allowRead. On a payload that materialises every registered object with explicit booleans the two agree; on a hand-built payload they can differ. Which one the built-in buttons should follow is outside this card.
  • isLoaded is the gate, as the dispatch assumed: during a refetch MePermissionsProvider still holds its previous map with isLoaded false, so a can-gated action hides for that window while the built-in buttons keep answering from the old map. In the console that refetch does not occur today.

Generated by Claude Code

… into the predicate scope once permissions have loaded (objectui#4421)

The acting subject carries the loaded /auth/me/permissions objects map
under a symbol; evalFieldPredicate hands it to the engine as
EvalContext.permissions. Not loaded: nothing is bound and the engine
refuses can() loudly, so each surface applies its own fault policy.

Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ
Co-authored-by: Claude <noreply@anthropic.com>
…tes per surface family and document it as client-only UI gating (objectui#4421)

Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ
Co-authored-by: Claude <noreply@anthropic.com>
…s (objectui#4421)

Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ
Co-authored-by: Claude <noreply@anthropic.com>
…e is measured, not inherited from warn-once (objectui#4421)

Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 11 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/5793-spec-range-floors.md

  • names pnpm-lock.yaml → pnpm-lock.yaml — edited by this change

    Nothing a consumer installs today changes: normal resolution already picks the newest 17.x, and pnpm-lock.yaml still resolves 17.2.0 on this edge after the bump. The change is to the declared floor only, which is why it is scored patch rather than minor — the same reasoning objectui#5753 used for the other direction on this dependency.

.changeset/6361-spec-floor-17-2-0.md

  • names pnpm-lock.yaml → pnpm-lock.yaml — edited by this change

    Nothing a consumer installs today changes: normal resolution already picks the newest 17.x, and pnpm-lock.yaml still resolves 17.2.0 on both edges after the bump — only the recorded specifier: moves. No source and no behaviour changes, which is why this is scored patch, on the reasoning 111741454 used for the same remediation on @object-ui/plugin-detail.

.changeset/6444-evaluator-fault-warn-dedupe.md

  • names fieldRules.ts → packages/core/src/evaluator/fieldRules.ts — edited by this change

    This is the one-per-source rate limit both sibling reporters already carry (warnPredicateFailure in fieldRules.ts, visibilityDiagnostic.ts in @object-ui/react), not a third mechanism. The dedupe key is the predicate's authoring identity — the fault site plus the source text, never the scope it ran against — which is both the siblings' precedent and the defect itself: the 200-row flood is one authored source evaluated against 200 distinct scopes, so a scope-sensitive key would emit all 200 lines again.

.changeset/6515-record-form-current-user-normaliser.md

  • names console/AppContent.tsx → packages/app-shell/src/console/AppContent.tsx — edited by this change

    The normaliser moved from console/AppContent.tsx to providers/expressionUser.ts, beside the ExpressionProvider it feeds. That move is what made the fix available: RecordFormPage is lazy()-loaded BY AppContent, so importing the normaliser from its old home would have put a static edge from the split chunk back into the module it was split out of. Both console/AppContent.js and the package entry re-export the name, so buildExpressionUser is published exactly as before.

.changeset/6559-expression-user-input-contract.md

  • names console/AppContent.tsx → packages/app-shell/src/console/AppContent.tsx — edited by this change

    NO RUNTIME BEHAVIOUR MOVES. All four in-repo production call sites pass useAuth().user, typed AuthUser | null, and type cleanly unchanged — two in console/AppContent.tsx, one in views/RecordFormPage.tsx, one in apps/console's InternalFormRoute.tsx. The body is byte-equivalent: the same keys, the same ?? defaults, the same anonymous branch. ⛔ No consumer-side fallback was added; id: u.id ?? null remains the rejected shape (triage ruling 2026-08-26), because a lenient default in the consumer is what AGENTS.md #0.1 forbids and it silently equates "signed in, no id" with "signed out".

  • names views/RecordFormPage.tsx → packages/app-shell/src/views/RecordFormPage.tsx — edited by this change

    NO RUNTIME BEHAVIOUR MOVES. All four in-repo production call sites pass useAuth().user, typed AuthUser | null, and type cleanly unchanged — two in console/AppContent.tsx, one in views/RecordFormPage.tsx, one in apps/console's InternalFormRoute.tsx. The body is byte-equivalent: the same keys, the same ?? defaults, the same anonymous branch. ⛔ No consumer-side fallback was added; id: u.id ?? null remains the rejected shape (triage ruling 2026-08-26), because a lenient default in the consumer is what AGENTS.md #0.1 forbids and it silently equates "signed in, no id" with "signed out".

.changeset/6776-metadata-admin-lazy-registration.md

  • names packages/app-shell/package.json → packages/app-shell/package.json — edited by this change

    • packages/app-shell/package.json's sideEffects array now names views/metadata-admin/register-builtins (the new leaf that performs the five registrations) instead of views/metadata-admin/index. The five registrations still run at package load, bare-imported by the package entry, so nothing a consumer could observe changes — but the array is a contract every consumer's bundler reads, so the swap is stated here rather than left to a diff. - The package barrel's 25 metadata-admin runtime re-exports (and 11 type-only ones) now point at their leaf modules. Same names, same types. They are unreachable from outside the package by any other path — exports is root-only — so no import an out-of-package consumer can write is affected.

.changeset/7122-objectstack-family-17-3-0.md

  • names pnpm-lock.yaml → pnpm-lock.yaml — edited by this change

    @objectstack/client, core, formula and lint each pin @objectstack/spec EXACTLY, so resolving the spec alone to 17.3.0 left the console bundling TWO copies of it. Moving the family with it in pnpm-lock.yaml collapses the duplicate; every declared range already admitted 17.3.0, so no manifest moved.

.changeset/7727-conditional-formatting-record-scope.md

  • names providers/ExpressionProvider.tsx → packages/app-shell/src/providers/ExpressionProvider.tsx — edited by this change

    buildExpressionScope (providers/ExpressionProvider.tsx) therefore no longer binds app, and ROW_PREDICATE_ROOTS no longer advertises it.

.changeset/8166-record-scope-data-root.md

  • names fieldRules.ts → packages/core/src/evaluator/fieldRules.ts — edited by this change

    The fault is loud, not fatal. The verdict a faulting predicate resolves to is unchanged: visibleWhen still fails OPEN, readonlyWhen / requiredWhen still fail permissive (@object-ui/core's fieldRules.ts; the direction is objectui#8069's open question, not this change's). So a record form renders exactly as before except that the console now names the root. Nothing throws.

.changeset/console-formpage-visible-predicates-5594.md

  • names evaluator/fieldRules.ts → packages/core/src/evaluator/fieldRules.ts — edited by this change

    The wiring is Form-view FormField.visibleOn (CEL) is never evaluated — conditional fields always render #2212's ruling applied verbatim rather than a second predicate semantics invented for this renderer, because two form renderers disagreeing about what visibleWhen means would be a worse defect than one renderer ignoring it. The predicate goes through the canonical engine — evalFieldPredicate (@object-ui/core, evaluator/fieldRules.ts) — so the accepted wire shapes (bare CEL string and { dialect, source }), the bound scope (record.* = the live input values, previous.* = the stored record an edit form started from), and the fail-open-but-loud behaviour on an unevaluable predicate are the shared ones by construction. Resolution is canonical-first, visibleWhen ?? visibleOn, matching both sibling readers: sectionFields.ts and app-shell's readVisibility.

.changeset/record-alert-row-binding-4807.md

  • names providers/ExpressionProvider.tsx → packages/app-shell/src/providers/ExpressionProvider.tsx — edited by this change

    • row-action shorthand (status == 'x') resolved nothing, so the evaluator threw. The legacy ${…} path answers a throw with its own source text, a non-empty and therefore truthy string, so the verdict was SHOWN on every row. A banner the author had gated was permanently on screen. - legacy data.* (data.status == 'x') did not throw at all. App-shell's ambient predicate scope (providers/ExpressionProvider.tsx) carries data: {}, so the predicate read that object instead of the row, compared undefined, and the verdict was a constant false — never shown.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 9419df198 (merge-base with origin/main): 16 file(s) changed outside .changeset/, read against 1804 pending declaration(s) that publish a body (2414 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3573.0 KB 3607.4 KB
Main entry chunk (gzip) 149.6 KB 350 KB
Entry file index-DYMyx7_Z.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 559.46KB 134.18KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 228.51KB 63.39KB
fields (index.js) 261.19KB 66.27KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 40.51KB 11.36KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 240.43KB 63.18KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.30KB 44.19KB
plugin-gantt (index.js) 172.43KB 42.85KB
plugin-grid (index.js) 230.25KB 63.22KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.82KB 28.67KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 32.26KB 9.42KB
plugin-tree (index.js) 11.20KB 3.89KB
plugin-view (index.js) 90.43KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.17KB 2.73KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 22.61KB 7.40KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.82KB 7.15KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Spec Main Shape Gate is red on this head (d4bf56123, check run 109878835995), and the failure is not this PR's.

  • It compiles objectui against @objectstack/spec built from objectstack main (the drifted main). The four diagnostics are the known drift: packages/app-shell/src/providers/writeWarningToast.ts:119 (TS2741, no computed entry in STRIPPED_LINE, three times) and packages/data-objectstack/src/spec-symbol-batch6.test.ts:242 (TS2344). This PR touches neither file.
  • The cause is objectstack b2805465, which adds computed to DroppedFieldsEvent['reason']; the repair is objectui#11206, PR objectui#11210 (the lane's p0, in review).
  • When it lands, this PR merges main and the gate re-runs; nothing in this diff changes for it.

domain:ui seat 1 · session_0122Knsowci76D2rBWReCzzZ · 2026-09-30T12:34Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d4bf5612394bbece7538580def5ded28352d5b9c
Local-runs: none

Inputs read: card #4421 (body and all 10 comments, rulings 5321072032 and 5474563898 included), PR #11208 (body, 17-file list, net diff against main at 846cec0e), the 45 check-runs on the head, and the installed engine at the @objectstack/formula@17.5.0 tag (objectstack 0f6dcac5e9: packages/formula/src/stdlib.ts, cel-engine.ts, eval-permissions.ts, types.ts). Read-only throughout: git fetch and git show at refs, REST GETs, and the record template. Nothing built, run, or re-run.

① Derived judgments

Every accept-set and public-surface change the diff implies, each judged against the rulings and the engine it wires to.

  1. @object-ui/core — new public exports SUBJECT_PERMISSIONS, bindSubjectPermissions, subjectPermissionsOf (new file evaluator/subjectPermissions.ts, re-exported through evaluator/index.ts, which src/index.ts already spreads). Right. Additive; nothing removed or renamed. The carrier is a Symbol.for('@object-ui/core:subject-permissions') on the subject object, and that is the correct shape for this engine: EvalContext.permissions is by contract NOT a CEL variable (types.ts at the tag), every string key of the scope bag becomes a CEL root, and a symbol is the one key no predicate dialect can spell — the core pin has(current_user.permissions) reads false. bindSubjectPermissions returns a shallow copy (the caller's user is never mutated) and strips a stale map when handed undefined; pinned.
  2. @object-ui/core — evalFieldPredicate hands permissions to ExpressionEngine.evaluate only when subjectPermissionsOf(scope?.current_user) is defined. Right, and it is the one seam: the engine (cel-engine.ts lines 1804 to 1807 at the tag) builds scope = buildScope(ctx), binds subject: scope.current_user and permissions: ctx.permissions, and buildScope ends with Object.assign(scope, ctx.extra), so the app's subject under extra.current_user is the receiver can compares by identity. Because the same object sits under current_user / user / ctx.user / os.user, the four aliases answer identically (pinned), a copied receiver is refused (pinned, ACTING SUBJECT), and a map under a string key is ignored (pinned). Behaviour for every existing caller is unchanged unless the subject carries the symbol, which only app-shell sets. The ablation on this head (anchor deleted from fieldRules.ts, 14 of 25 red in the predicted direction, restored to an empty diff) shows the hand-off is load-bearing.
  3. @object-ui/permissions — PermissionContextValue.effectiveObjects? (optional), set to data?.objects verbatim by MePermissionsProvider, to undefined explicitly by PermissionProvider, absent from the untouched no-provider object in usePermissions.ts (so it reads undefined). Right. Optional member, so no consumer or implementer breaks; identity, not a copy, so the adapter cache keys on the payload; undefined and {} stay distinct (four-row truth table pinned). This is the producer the claim allowed ("if the real producer is elsewhere, fix it there"): the provider held the payload but exposed only verdict functions.
  4. @object-ui/app-shell — ExpressionProvider reads usePermissions() itself and binds the map only while isLoaded is true and effectiveObjects is present (useExpressionPermissions). Right on Rider 1 as ruled at the binding: not loaded, no provider, the role-based PermissionProvider (loaded but payload-less), and a refetch window (stale map with isLoaded false) all leave the subject unbound, and the 17.5.0 engine then throws a loud runtime fault ("carries no permission data") — never true, never a quiet false. No {} stand-in, which the engine contract forbids (an empty map means "holds nothing"). A payload toEvalPermissions refuses is console.errored once per payload object and left unbound, not repaired. @object-ui/permissions is already a workspace:* dependency of app-shell.
    • useExpressionPermissions is a module export of providers/ExpressionProvider.tsx, consumed by AppContent.tsx and RecordFormPage.tsx; it is NOT re-exported from src/index.ts (unchanged) and the package exports map is . only. So it is not a new public export of @object-ui/app-shell, and neither the changeset nor the PR body claims it is. Right. ExpressionScopeInput.permissions? is likewise internal (the type is not on the index).
    • One bag. The console mounts MePermissionsProvider above DefaultAppContent (apps/console/src/AppContent.tsx line 297) with a loadingFallback, so app-shell's AppContent (imperative evaluator at its line 700, the ExpressionProvider it renders at line 1030) and RecordFormPage read the same permission context through the same hook; the two imperative field evaluators and the provider cannot drift. Right.
  5. @objectstack/formula floor ^17.0.0 to ^17.5.0 in packages/core/package.json and packages/app-shell/package.json. Right and required, not cosmetic: app-shell calls toEvalPermissions at runtime and core imports the EvalPermissions type; at the @objectstack/formula@17.4.0 tag both toEvalPermissions (index.ts) and dyn.can(dyn, dyn) (stdlib.ts) count zero, and at the 17.5.0 tag both count one. Every other importer stays on ^17.0.0, which is correct — none of them calls the new surface.
  6. pnpm-lock.yaml — exactly two lines move: the specifier: under importers.packages/app-shell (line 774) and under importers.packages/core (line 1190), each ^17.0.0 to ^17.5.0; both version: 17.5.0(ai@7.0.65(zod@4.6.5)) lines are unchanged; no package entry, snapshot, or settings line moves. That is precisely what pnpm install writes when a specifier changes and its resolution does not (the base already resolved 17.5.0 via objectui#11086). No hand-edit divergence; Lockfile Integrity Check and Lockfile Dedupe Check are green on the head.
  7. content/docs/layout/page-header.mdx — the Rider 2 face. Right. The new section ends with the ruled sentence in substance and in words: client-side UI gating only, not an authorization boundary, the server still enforces object permissions on the request and answers 403, server-side evaluation is objectstack#18783, and do not rely on can in an expression the server evaluates. Every mechanism claim on the page matches the tag: the verb table is OBJECT_PERMISSION_VERBS exported from @objectstack/spec/security (security/index.ts spreads permission.zod; the package has a ./security export) and an unknown verb throws rather than answering false; user / ctx.user / os.user are the same call and a bare can(...) or record.can(...) is refused (receiver-only registration, identity check). The list_item sentence is pinned by the RowActionMenu arms. One page-scoped sentence to note, not fail: "a disabled predicate that faults leaves the action enabled" is true for the header's evalRowPredicate disabled leg (this page's surface) and for the row menu, while action:button's fail-soft disabled leg lands DISABLED when not loaded (the dev's own table); on this page the sentence is accurate, and it correctly steers authors to visible. Doc gates green: Build Docs, Doc Snippet Type Check, Doc Component Type Check, Doc Fence Language Check, Doc Example Id Check, Internal Docs Link Check.
  8. Pins. Three surfaces (row menu, page:header, action:button) times three states on one verb (delete), with allowEdit granted in both loaded states so a verb mix-up would show in the denied arm, a companion ungated action so "hidden" is never "nothing rendered", one action name per arm so warn-once dedupe cannot fake the denied arm's silence, and the built-in usePermissions().can read in the same tree (false:true beside a hidden bound action) so the no-provider true is provably not inherited. That is the three-state fixture the ruling and the unlock note asked for, on the fail-closed legs. Right.
  9. Surface discipline. No file under packages/components/src/renderers/action/** (serial behind objectui#11185), none under packages/app-shell/src/hooks/** or layout/** (objectui#11186), usePermissions.ts net untouched (base-to-head diff empty for that path). The files beyond the claim's list — the permissions producer, RecordFormPage.tsx, the two floors and the lockfile — are each named in the PR body and each judged right above.

② Semver level

  • Changeset .changeset/4421-current-user-can-binding.md: @object-ui/core: minor, @object-ui/permissions: minor, @object-ui/app-shell: minor. Right. The diff publishes additive exports (core), an optional context member (permissions), a behaviour addition on a public provider (app-shell), and a dependency-floor raise (core, app-shell); nothing an author can write is removed or renamed, so no major and no migration is owed. The three named packages are exactly the released packages the diff touches (content/docs is the site; the presence check counts 12 source files of 3 released packages); all three sit in the repo's one fixed group, so the bump propagates together. Changeset gates green: Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Claim Re-read, Changeset Overwrite Report.
  • Changeset prose: accurate and correctly scoped. "Until then it gives no answer: the predicate faults, and a surface that evaluates visible fail-closed does not render the action" claims fail-closed only where it holds; the client-side-only paragraph carries Rider 2; "@objectstack/formula is now required at ^17.5.0, the first release that answers can" is verified against the 17.4.0 tag (no can).
  • Clause-②: yes (PR body line 2, no arm): right and consistent with the claim comment 5908600287. The diff adds a published evaluation input and new exports, a widening of the contract surface; under the repo rule yes takes at least minor, which is what the changeset declares. An arm is "at most one", so none is well-formed.
  • First body line Part of #4421 — the objectui half: ...: right. Not a closing keyword, which matches the seat's Q1 disposition below.

③ Boundary flags

  • Q1 — Rider 1 unmet on the fail-soft legs (action:group and action:icon inline visible, the related-list toolbar visible, the evalRowPredicate disabled legs: SHOWN or ENABLED while not loaded; reachable only outside MePermissionsProvider, i.e. /forms/:name and standalone embeds). Seat disposition: the PR stays Part of #4421 and the residue becomes an in-scope sub-issue of Action visible CEL cannot see the caller's object permissions — a custom action replacing a built-in CRUD button has no way to express the gate it replaced #4421 on this seat, serial behind objectui#11185 for the action renderers. The diff is consistent with that: no renderer fault policy is touched, no file under components/src/renderers/action/**, the first body line is Part of, and the changeset and doc prose promise fail-closed only on fail-closed surfaces. Not contradicted. The dev's option C ({} while not loaded) is correctly not taken: it would fabricate "holds nothing" against the engine contract.
  • Q2 — ruling batch feat: Add enterprise data table component with Airtable-like features including column resizing and reordering #13 item 3, the usePermissions no-provider default, can answering true for every call. Seat disposition: not changed in this PR; the conflict between the ruling's literal text and the measurement goes to the maintainer as a decision card. The diff is consistent: usePermissions.ts is not in the file list and the base-to-head diff on it is empty; the binding reads effectiveObjects, never can, so the default is off the binding's path (pinned in both the render test and the permissions truth table); the ExpressionProvider docblock records that measurement and decides nothing. Not contradicted.
  • Dev deviations (report 5911355276): producer files, RecordFormPage.tsx, floors and lockfile — judged right in ①; usePermissions.ts edited then restored before the first commit — confirmed net untouched; app-shell suite run as shards — the head's own Test (shard 1/8) to Test (shard 8/8), Test (dist pins) and Test are the gate verdicts and are green; harness footer form — not a diff matter.
  • Dev out-of-scope findings: RowActionInlineButton never reads def.disabled; ActionRunner.execute blocks a faulting disabled gate; ActionProvider contexts bind user / ctx.user but never current_user, so record:quick_actions hides in every state; the useCondition throw text drops the engine reason; engine objectPermissionGrants and client MePermissionsProvider.check can diverge on a hand-built payload (the batch feat: Add enterprise data table component with Airtable-like features including column resizing and reordering #13 item 1 single-source obligation sits on the objectstack half). None is touched or worsened by this diff and none blocks it; the two with carrier "none" are the seat's to file or fold. Escalated as recorded.
  • Check-runs on d4bf5612: 45 total — 41 success, 3 skipped (Test (coverage shard ...), Test (coverage), dependabot), 1 failure, 0 in progress at the time of this read. The one failure is Spec Main Shape Gate (run 109878835995): its file annotations name packages/app-shell/src/providers/writeWarningToast.ts line 119 (TS2741, computed missing from the stripped-line record) and packages/data-objectstack/src/spec-symbol-batch6.test.ts line 242 (TS2344), compiled against objectstack c90f9fb6e218; the workflow-level lines (app-shell build and type-check, apps/console and data-objectstack type-check exit 2) are downstream of those two files. Neither file is in this diff: this is the known objectstack-main drift objectui#11206, fixed by PR objectui#11210, not a verdict on this PR. Type Check, Lint, Build & E2E, Bundle Analysis, README Export Check, Pre-Install Import Graph Check, Control Byte Scan, Line Citation Gate, Governed Surface Queue Guard, Action Ref Convention are green.
  • Noted, no action: Lint carries one new react-refresh/only-export-components warning on useExpressionPermissions (no --max-warnings in the workflow; the check is green). The PR is mergeable_state: behind main; the head's check-runs, not the merge base, are what this record judges.

Implemented-by: claude/issue-4421-current-user-can-wiring
Reviewed-by: session_0122Knsowci76D2rBWReCzzZ

VERDICT: PASS

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3573.1 KB 3607.4 KB
Main entry chunk (gzip) 149.6 KB 350 KB
Entry file index-B2P6WIYk.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 559.46KB 134.18KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 228.51KB 63.39KB
fields (index.js) 261.19KB 66.27KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 40.51KB 11.36KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 240.43KB 63.18KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.30KB 44.19KB
plugin-gantt (index.js) 172.43KB 42.85KB
plugin-grid (index.js) 230.25KB 63.22KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.82KB 28.67KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 32.26KB 9.42KB
plugin-tree (index.js) 11.20KB 3.89KB
plugin-view (index.js) 90.43KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.17KB 2.73KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 22.61KB 7.40KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.82KB 7.15KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 947b8d38bb124be5f68d679e2229a5a84b143ed5
Local-runs: none

Delta record. This head is the GitHub update-branch merge of main into the head that record 5911520748 reviewed; nobody edited a file. Inputs, and nothing else: record 5911520748 (head d4bf5612394bbece7538580def5ded28352d5b9c); origin/main at 9419df198f06856c3746738c99dd807fd3f7dd04 and the two heads, read after a fetch with git log, git merge-base, git diff, git show --cc and git patch-id --stable at refs; PR #11208 and its /files at this head; the objectui#11210 merge commit's 14-file stat; this head's check-runs. Nothing checked out, installed, built or run.

① Derived judgments

Carried over from record 5911520748 at head d4bf5612394bbece7538580def5ded28352d5b9c, the net diff being identical (patch-id 09b0cf3ff1ad6ecd98913173f032dd80d83c036c at both heads). The three measurements that make the carry-over valid:

  1. Parents. 947b8d38bb is a merge commit with exactly two parents: first parent d4bf5612394bbece7538580def5ded28352d5b9c, the reviewed head; second parent 9419df198f06856c3746738c99dd807fd3f7dd04, the tip of origin/main (the merge of objectui#11210). git rev-list --first-parent d4bf5612..947b8d38 is the merge commit alone, so no other commit sits between the reviewed head and this one. git show --cc on the merge is empty: an automatic merge with no hand-resolved hunk.
  2. Net diff. merge-base(origin/main, 947b8d38) is 9419df198f; merge-base(origin/main, d4bf5612) is 81778b955575b61d56c7563ef854fa73b7745ed7. git diff --name-status over the two ranges lists the same 17 paths with the same A / M status each (the changeset, content/docs/layout/page-header.mdx, five app-shell paths, five core, four permissions, pnpm-lock.yaml), and GitHub's /files on the PR at this head lists those same 17. git diff 9419df19 947b8d38 | git patch-id --stable = 09b0cf3ff1ad6ecd98913173f032dd80d83c036c; git diff 81778b95 d4bf5612 | git patch-id --stable = 09b0cf3ff1ad6ecd98913173f032dd80d83c036c. Identical hunks. In particular pnpm-lock.yaml, the file a branch merge most often disturbs, is not among the files main changed between the two merge bases, so the two-specifier-line lockfile change the earlier record judged is exactly what this head carries.
  3. What main brought in. Nine merges between the two merge bases (objectui PRs test(types): a padded grouping field name is refused on every face that declares grouping (objectui#7347) #11191, feat(types,layout,app-shell): a navigation entry with no label renders its target's current label at render time (objectui#9868) #11186, fix(types): the six held public blocks refuse the content channels no renderer reads (objectui#10872 batch 5) #11193, feat(types,plugin-detail): declare the field-security triple on record:details / record:highlights / record:related_list and read it un-cast (objectui#8649) #11184, feat(types): the object-form zod mirror declares the members its TypeScript twin declared (objectui#6152, round 1) #11125, fix(app-shell): Studio resolves an app's own locale-map label in the designer locale instead of printing [object Object] (objectui#11181) #11194, fix(data-objectstack): the entry-form filter refuses an empty or non-string icontains comparand, reading the spec's own predicate (objectui#9048) #11192, fix(core,plugin-tree): the record-source ladder judges map / gantt / tree against their spec rows; the tree stops honouring an undeclared bare-array data (objectui#8348) #11200, fix(app-shell,i18n,data-objectstack): the computed strip reason is worded, and the table and the batch-6 pin compile against both the pinned spec and objectstack main (objectui#11206) #11210), 100 files. Intersection with this PR's 17 paths: none. objectui#11210's two files, packages/app-shell/src/providers/writeWarningToast.ts and packages/data-objectstack/src/spec-symbol-batch6.test.ts, are among the 100 and are not in this PR's file list.

So every judgment in ① of record 5911520748 (the three new core exports and the symbol carrier, the one evalFieldPredicate seam, effectiveObjects on the permissions context, the ExpressionProvider binding under Rider 1, the one-bag reading, the ^17.5.0 floor on @objectstack/formula and its two lockfile lines, the Rider 2 doc face, the three-by-three pins, the surface discipline) is a judgment on hunks this head carries byte for byte, merged onto a main that touches none of the same files. Unchanged.

② Semver level

Carried over from record 5911520748 at head d4bf5612: .changeset/4421-current-user-can-binding.md is byte-identical at the two heads (git diff d4bf5612 947b8d38 -- .changeset/4421-current-user-can-binding.md is empty), the levels it declares (@object-ui/core minor, @object-ui/permissions minor, @object-ui/app-shell minor, no major) still match what the identical diff publishes, and the PR body's Clause-②: yes and its Part of #4421 first line are unchanged. The changeset gates on this head (Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Claim Re-read, Changeset Overwrite Report) are success, as are Lockfile Integrity Check and Lockfile Dedupe Check.

③ Boundary flags

  • The boundary flags of record 5911520748 carry over as recorded: the Q1 disposition (Rider 1 residue on the fail-soft legs becomes an in-scope sub-issue of Action visible CEL cannot see the caller's object permissions — a custom action replacing a built-in CRUD button has no way to express the gate it replaced #4421, the PR stays Part of), the Q2 disposition (the usePermissions no-provider default goes to the maintainer as a decision card; usePermissions.ts is still net untouched at this head), the dev deviations and the out-of-scope findings are dispositions on the diff, and the diff is the same; nothing the merge brought touches them.
  • What main brought in: no file of this PR (① item 3). Any cross-file effect of the 100 main files on these 17 is what this head's Type Check, Lint, Build & E2E and test shards measure, listed below.
  • objectui#11210's two files are not in this PR's file list. The Spec Main Shape Gate failure the earlier record attributed to those two files (objectstack-main drift, objectui#11206) is what this update-branch merge was made to pick up; the earlier record's mergeable_state: behind note is answered by this head, whose merge base is the main tip.
  • Check-runs on 947b8d38bb, read 2026-09-30T13:06Z: 44 runs. 34 success: Action Ref Convention, Build & E2E, Build Docs, Bundle Analysis, Changeset Bump Policy, Changeset Claim Re-read, Changeset Declaration, Changeset Fixed Group Check, Changeset Overwrite Report, Control Byte Scan, Doc Component Type Check, Doc Example Id Check, Doc Fence Language Check, Doc Snippet Type Check, Docs Route Eager Closure Check, Governed Surface Queue Guard, Inert vi.mock Specifier Check, Internal Docs Link Check, label, Line Citation Gate, Lint, Live E2E (informational), Lockfile Dedupe Check, Lockfile Integrity Check, Pre-Install Import Graph Check, README Export Check, Shell Escape Residue Scan, Skill Eval Token Check, Skill Example Check, Skill Guide Path Check, Test (dist pins), Test (shard 1/8), Test (shard 3/8), Type Check. 3 skipped: dependabot, the coverage-shard matrix job, Test (coverage), which do not run on this event. 0 failure. 7 in_progress, not yet concluded and named here as such: Spec Main Shape Gate, Test (shard 2/8), Test (shard 4/8), Test (shard 5/8), Test (shard 6/8), Test (shard 7/8), Test (shard 8/8). Spec Main Shape Gate is still in_progress (job 109895563574 of run 36717979599), not yet concluded and named here as such. On the reviewed head it was the one failure, attributed to the two objectui#11210 files; this head is the first of this PR to carry those files at their objectui#11210 state. Its conclusion on this head is the one reading this record could not yet take; the seat reads CI convergence separately, and this record is a judgment of the diff, which the gate does not change.

Implemented-by: claude/issue-4421-current-user-can-wiring
Reviewed-by: session_0122Knsowci76D2rBWReCzzZ

VERDICT: PASS

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants