Skip to content

feat(app-shell,data-objectstack): a refused dashboard or list view opened by URL says it was refused (objectui#12112) - #12162

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-12112-meta-refusal-surface
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-12112-meta-refusal-surface

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #12112
Clause-②: yes

The console half of objectstack-ai/objectstack#22611's ruling A (director's ruling 6095014058), as triage 6103491895 scoped it. Dispatched by the domain:ui#2 seat; claim 6109832410; implementing session https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD.

What changes

Since objectstack-ai/objectstack#22731 (5081090b71) the server lists only the dashboards and list views a caller may open, and refuses a by-name read of one they do not hold with 403 PERMISSION_DENIED and the sentence You do not have permission to open the 'NAME' dashboard. (or view). The console read only the lists.

  • Dashboard URL (DashboardView). A dashboard missing from the caller's list is now asked by name once the list has loaded. A PERMISSION_DENIED answer shows the refusal: the title "You don't have access" (dashboard.widgetForbiddenTitle, an existing key) and, under it, the server's own sentence, which names the dashboard. Every other answer (not_found, unreachable, granted) keeps today's "Dashboard Not Found". While the list is still loading, the page now shows its skeleton instead of a premature "not found".
  • List view by name (ObjectView). A requested view id that misses the merged view list is asked by name, by its canonical OBJECT.KEY name (a short name is the key). A PERMISSION_DENIED answer replaces the list area, in both layouts (the plain list and split navigation), with "You don't have access" (list.loadErrorForbiddenTitle, an existing key) and the server's sentence; no switcher tab is marked active. Every other answer keeps today's fall-back to the default view.
  • The by-name door (@object-ui/data-objectstack). ObjectStackAdapter.probeMetaItemAccess(type, name) is the existing probeAppAccess door, widened to a type parameter: the same client.meta.getItem read, the same verdicts, read off the PERMISSION_DENIED code and never off the status. It also returns the refusal's message with a denied verdict. probeAppAccess now delegates to it and answers exactly as before (its pin file is unchanged and green).
  • One reader for both pages: useMetaItemRefusal (packages/app-shell/src/hooks/useMetaItemRefusal.ts) asks once per missed name, holds the answer with the name it is about, caches nothing across mounts, and asks nothing in draft preview or when the host adapter has no probe (the same two skips AppContent makes for its app probe).

Clause-②: yes: @object-ui/data-objectstack exports a new interface, MetaItemAccess, and ObjectStackAdapter gains a public method, probeMetaItemAccess; no translation key is added. The refusal's sentence is the server's, shown untranslated, as metadataReadWarningToast already does for server text.

Mechanism readings (the dispatch's hypotheses, measured)

  • H1, confirmed. DashboardView resolves the dashboard from the /meta/dashboard LIST (preferLocal(dashboards, …)), so a refused dashboard is absent and fell into the not-found branch. Proven on BASE by the new pin, run before the view was edited: Unable to find an element by: [data-testid="dashboard-access-denied"], with empty.dashboardNotFound and empty.dashboardNotFoundDescription in the DOM. The same pin run showed the not-found face also renders while the list is still loading.
  • Where the 403 was lost. @objectstack/client keeps it: its fetch stamps error.code from body.error.code, error.httpStatus and error.message. The console flattened it in two places: MetadataProvider.getItem resolves null on any failure (so useMetadataItem's error slot is never filled), and the adapter's getView / getApp / getPage resolve null too. probeAppAccess was the one by-name door that read the code, for apps only, so that is the door this widens rather than adding a second fetch path.
  • H2, measured. A refused view id opened by URL fell back SILENTLY to the default view: BASE pin DOM shows data-active-view="clm_contract.default" and the console warning Requested view "clm_contract.legal_review" not found …; falling back to the default view.
  • H3, the D4 class, measured: listed, and served — nothing refuses it. For a studio.access holder without a view's capability:
    • a view that is a /meta/view item (an item, or a container's expanded entry) is pruned from /meta/view for every caller (createMetaListReadGate's view arm has no D4 exemption) and is never in the switcher;
    • an object's OWN listViews entry is served whole inside /meta/object to a caller who may edit the object (isObjectSchemaMaskExempt), so the console's switcher lists it (mergeViewsIntoObjects keeps the object's entries, buildViewTabs draws them) and it opens. There is no by-name /meta/view item for such an entry (the server's own pin fixture, meta-view-dashboard-audience-gate.test.ts, has legal_desk only inside the object), so a by-name read answers absence, not a refusal. The triage premise "the by-name read refuses it" does not hold for this path. The spec describe says the same: an object's own list views are pruned "for a user who may not edit the object".
    • ⛔ No client rule was added. The pin D4 (measured) holds that the entry is listed, opens, and nothing is asked or refused.
  • H4. No dashboard- or view-level refusal key exists; the two titles reused are dashboard.widgetForbiddenTitle and list.loadErrorForbiddenTitle ("You don't have access", present in all ten packs). The item is named by the server's sentence.

Pins

  • DashboardView.refusal-12112.test.tsx: a refused dashboard URL (with and without a sentence); CONTROL a held dashboard opens as today and asks nothing; CONTROL not_found / unreachable / granted keep "not found"; CONTROL a host adapter with no probe; CONTROL draft preview asks nothing; nothing is asked or said while the list is loading.
  • ObjectView.viewRefusal-12112.test.tsx: a refused list view by name (plain and split navigation), a short name asked canonically; CONTROL a held view opens by name and asks nothing; CONTROL a missing view falls back to the default; the D4 reading.
  • metaItemAccessProbe-12112.test.ts: the adapter over the real client and a stubbed fetch: a refused dashboard and a refused view (denied + sentence, by-name address), the item-less 200, a served item, a 403 without the code, empty inputs, and probeAppAccess as the same probe.

Verification

All on this branch. "HEAD" below is 1104e4a (the merge of main at faf4ce9) unless a line names 8e35d3c (the tree before that merge; the merged commits touch none of this branch's files). Heavy runs went through the shared verify lock.

  • BASE reading (the new view pins, run before either view was edited): 9 failed / 5 passed. The refused dashboard rendered empty.dashboardNotFound; the refused view rendered data-active-view="clm_contract.default".
  • Build: turbo run build --filter=@object-ui/app-shell^... --concurrency=2, 28/28 tasks, on 1104e4a.
  • Type-check on 1104e4a: @object-ui/data-objectstack tsc --noEmit exit 0 (its file set includes the new pin, per tsc --showConfig); @object-ui/app-shell tsc --noEmit && tsc -p tsconfig.test.json exit 0 (the test config includes src/**/*.test.tsx, so both new view pins are type-checked).
  • Pins, neighbours and ratchets on 1104e4a: the three new pin files, appAccessProbe.test.ts, the four existing DashboardView.* pins, AppContent.deniedVsUnpublished and AppContent.missingRecheckKeying, column-identity.ratchet and one-authority-per-exported-name-6273: 12 files, 98 tests passed.
  • Ablation on 8e35d3c, each leg through ablation-replace.mjs (anchor hit 1 to 0, blob changed, restore proven: blob equals HEAD and git diff HEAD empty), with an EXIT/INT/TERM restore trap:
    • leg A, the hook's denied branch made unreachable: 5 red (both dashboard refusal cases, the view refusal in plain and split navigation, the short-name case); every CONTROL green.
    • leg B, the adapter's PERMISSION_DENIED branch made unreachable: 5 red (the dashboard and view denied cases, probeAppAccess as the same probe, and two probeAppAccess denied cases in appAccessProbe.test.ts, which shows the app probe now goes through the widened door).
    • restore leg: the four pin files 39/39 green.
  • Whole suites on 8e35d3c: packages/app-shell/ 1,272 files (1 skipped), 12,314 tests passed and 9 skipped (8 shards; shards 5 to 8 exceeded the shard timeout under load and were re-run as twelve 53-file chunks, whose file lists were checked identical to vitest's own sequencer's); packages/data-objectstack/ 83 files, 1,109 tests; scripts/__tests__/ 4 shards, 182 files (2 skipped), 5,489 tests passed and 2 skipped.
  • Declared narrowing: the whole-suite runs above were not repeated after the faf4ce9 merge; the post-merge reading is the 12-file set above plus both type-checks. CI runs the full suites.
  • Repo gates on 1104e4a: check-changeset-presence, check:changeset-claims, check:pending-changeset-literals, check:new-line-citations (0 new citations), check:control-bytes, check:test-path-roots, the three check:vi-mock-*, check:phantom-deps, check:i18n-keys, check:i18n-dead-keys, check:unreferenced-sources and docs:check-links: all exit 0.
  • Lint: eslint (inline config honoured) on the seven touched source and test files: 0 errors; 306 warnings, 0 of them on an added line.
  • NOT MEASURED:
    • check:doc-snippets: PRECONDITION NOT MET (exit 2), it needs a 34-package build; CI's doc-snippet workflow builds it. It now covers the new ts fence in the adapter README.
    • check:readme-exports: exit 1 for 94 self-imports in 8 packages outside the built closure ("could not be judged"). The judged part, the adapter README included, reads 0 fabricated and 0 wrong-path.
    • Console eager-closure delta: two console builds under a contended lock. The adapter (eager) gains one method and one interface; the two views and the hook are route code.
    • The i18n family is not owed: no pack moved.

Acceptance notes

  • The D4 reading above is the card's item 2. The switcher lists an object's own gated list view to a caller who may edit the object, and it opens. No refusal exists for the console to surface, and nothing was added to hide it.
  • While the dashboard list is still loading, the page now shows its skeleton rather than "Dashboard Not Found". Before, a cold load briefly said "not found" for every dashboard. The refusal needs a settled list, so this is part of the change, not a separate fix.
  • Noted, not filed (read, not run; no carrier): MetadataProvider.getItem resolves null on every failure, and its extractItem returns the whole envelope as the item when the by-name answer carries no item key. That is the server's 200 for a name with nothing behind it. A cold useMetadataItem(type, absentName) would therefore hold an envelope as if it were the document. Nothing in this change reads getItem.

Generated by Claude Code

…it was refused (objectui#12112)

The console reads `/meta/dashboard` and `/meta/view` as lists, and since
objectstack#22731 those lists carry only what the caller may open. A
dashboard or view withheld by `requiredPermissions` was therefore read as
"not found" (dashboard) or silently replaced by the default view (list
view). A miss is now asked by name through the adapter's new
`probeMetaItemAccess` (the generalised `probeAppAccess` door); a
`403 PERMISSION_DENIED` renders a permission refusal in the server's own
sentence, every other answer keeps today's face.

Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD
Co-authored-by: Claude <noreply@anthropic.com>
Brings in objectui#12140 (158c75d) and objectui#12142 (dc6d28e); neither
touches this branch's files.

Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD
Co-authored-by: Claude <noreply@anthropic.com>
…re; docs and changeset (objectui#12112)

The adapter pin reads a refused dashboard and a refused list view through
the real client over a stubbed fetch. The list page pin now mounts real
i18n and covers split navigation. The console guide and the adapter
README say what a refused dashboard or list view URL shows.

Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD
Co-authored-by: Claude <noreply@anthropic.com>
Brings main to faf4ce9 (objectui#12151, #12152, #12153, #12154, #12148,
#12149, #12147, #12158, #12159); none of them touches this branch's files.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 293 chunks) 3178.5 KB 3204.6 KB
Main entry chunk (gzip) 74.2 KB 350 KB
Entry file index-CZZz60ZD.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) 20.05KB 7.41KB
app-shell (runtime-config.js) 22.59KB 7.89KB
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.15KB 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) 41.19KB 11.12KB
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.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 587.98KB 141.60KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 241.78KB 67.52KB
fields (index.js) 276.26KB 70.32KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
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.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
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) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
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.04KB 3.92KB
plugin-calendar (index.js) 54.31KB 15.86KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.72KB 48.07KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.77KB 50.02KB
plugin-detail (index.js) 250.93KB 66.24KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.80KB 46.04KB
plugin-gantt (index.js) 178.63KB 45.19KB
plugin-grid (index.js) 255.20KB 70.94KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 122.82KB 31.24KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 92.11KB 23.27KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
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) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
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 (node-slots.js) 7.18KB 2.34KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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

… public probe (objectui#12112)

`MetaItemAccess` and `ObjectStackAdapter.probeMetaItemAccess` widen the
package's published surface, so the declaration is minor, the level this
repository gives an additive published widening. `@object-ui/app-shell`
stays patch.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 293 chunks) 3178.6 KB 3204.6 KB
Main entry chunk (gzip) 74.2 KB 350 KB
Entry file index-CSsvdSmV.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) 20.05KB 7.41KB
app-shell (runtime-config.js) 22.59KB 7.89KB
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.15KB 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) 41.19KB 11.12KB
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.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 587.98KB 141.60KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 241.78KB 67.52KB
fields (index.js) 276.26KB 70.32KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
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.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
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) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
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.04KB 3.92KB
plugin-calendar (index.js) 54.31KB 15.86KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.72KB 48.07KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.77KB 50.02KB
plugin-detail (index.js) 250.93KB 66.24KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.80KB 46.04KB
plugin-gantt (index.js) 178.63KB 45.19KB
plugin-grid (index.js) 255.50KB 71.02KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 122.82KB 31.24KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 92.11KB 23.27KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
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) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
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 (node-slots.js) 7.18KB 2.34KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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: a965c32f8e2cd92f0009eebcea55c9e4efadd8a7
Local-runs: none

Read at 2026-10-11T21:55Z, through the REST API only: card objectui#12112 (body, triage 6103491895, claim 6109832410, dev report 6114091125), PR #12162 (body, the 10-file list, the net diff against main at faf4ce9), the check-runs on the head, and the head's own files where the judgments below needed them (packages/data-objectstack/package.json and src/index.ts, packages/app-shell/package.json, src/index.ts, src/hooks/index.ts, MetadataProvider.tsx, AdapterProvider.tsx, ObjectView.tsx, DashboardView.tsx, console/AppContent.tsx, useServedViewItems.ts, i18n/src/locales/en.ts, @object-ui/core resolve-view-id.ts). The server half was read at objectstack main: packages/rest/src/meta-item-read-gate.ts, meta-view-dashboard-audience-gate.test.ts, ADR-0106, PR #22731 and its contract review 6102543061, ruling 6095014058, scope note 6096254862. Nothing was built, run or re-run.

① Derived judgments

Public surface.

  • @object-ui/data-objectstack publishes one entry (. to dist/index.d.ts, built from src/index.ts). The diff adds export interface MetaItemAccess { verdict: AppAccessVerdict; message?: string } and the public method ObjectStackAdapter.probeMetaItemAccess(type: string, name: string): Promise of MetaItemAccess on the exported class. Both are reachable from the entry, so the published surface grows by one type and one method. RIGHT, additive: nothing existing is renamed, removed or re-typed. probeAppAccess(appName) keeps its signature and its Promise of AppAccessVerdict; it now returns (await this.probeMetaItemAccess('app', appName)).verdict, and the branch-by-branch reading is the same as before (unknown on an empty name, granted or not_found off metaEnvelopeCarriesItem, denied off isAppPermissionDeniedError, not_found off isMetaItemAbsentError, else unreachable). The existing appAccessProbe.test.ts is untouched in the diff.
  • @object-ui/app-shell publishes . and ./styles.css only. The new module src/hooks/useMetaItemRefusal.ts (and its two interfaces) is named neither in src/index.ts nor in src/hooks/index.ts, so it reaches no published entry. DashboardView and ObjectView keep their props. RIGHT: no app-shell surface change.
  • No @object-ui/i18n key is added. The two titles are dashboard.widgetForbiddenTitle (en.ts, "You don't have access") and list.loadErrorForbiddenTitle (same text), both present at the head. RIGHT; the server's sentence is shown as served, the precedent being metadataReadWarningToast.

Accept set (what each page now answers).

  • The adapter's verdict is read off error.code equal to PERMISSION_DENIED, never off the status (a 403 with another code is unreachable, pinned); message rides only with denied and only when the server wrote a non-blank string. This matches what PR #22731 emits: audienceRefusal in meta-item-read-gate.ts writes { status 403, code PERMISSION_DENIED, message "You do not have permission to open the 'NAME' view." or "dashboard." } through the app arm's envelope. RIGHT. The by-name address is the singular type (/meta/dashboard/NAME, /meta/view/NAME), which the server's fixture addresses the same way. RIGHT.
  • DashboardView: a name absent from the /meta/dashboard list is asked by name only once getTypeStatus('dashboard') is ready or error (the dashboards getter starts the fetch and ensureType stamps loading synchronously, so a first render is never read as settled), and never in draft preview. denied draws the dashboard-access-denied face with the title and the server's sentence; not_found, unreachable and granted keep "Dashboard Not Found"; a host adapter without the probe asks nothing and keeps that face. While the list loads or the probe is out the page draws its skeleton instead of a premature "not found". RIGHT, and the skeleton change is a correction of a face that was wrong for every dashboard on a cold load, not a scope creep.
  • ObjectView: a requested view id that resolveViewId cannot find in the merged list is asked by its canonical OBJECT.KEY name (a short name is prefixed with the object, the same prefix rule resolveViewId applies), never in draft preview. denied replaces the list area, in both the plain and the split layout, with view-access-denied and clears the switcher's active tab; every other verdict keeps the fall-back to the default view. RIGHT: the silent fall-back the BASE pin recorded (data-active-view of the default view) no longer stands in for a refusal.
  • useMetaItemRefusal: reads the adapter off AdapterCtx (ObjectStackAdapter or null), asks only when probeMetaItemAccess is a function, keys the answer by type and name and reads it back for that name only, maps a rejecting host probe to unreachable, caches nothing across mounts. RIGHT: a verdict about the caller cannot outlive a grant, and a previous URL's answer cannot speak for the next.
  • The D4 reading (card item 2). The server's own text is unambiguous: filterObjectListViewsForUser says an object's own list views "are served inside /meta/object, never as /meta/view items" (scope note 6096254862 item 1), and the object arm serves the whole document to isObjectSchemaMaskExempt callers on every door (ADR-0106 D4). The server fixture holds legal_desk only inside the object and refuses by name only the view item clm_contract.legal_review. So for a studio.access holder without the capability the switcher lists the object's own entry (mergeViewsIntoObjects keeps the object's listViews under their record keys), it resolves without a probe, and it opens; no by-name read exists to refuse it. The triage's conditional premise ("the by-name read refuses it") does not hold for this path, which the dev measured and pinned (D4 (measured)), and no client rule was added. RIGHT under the ruling's one-evaluator constraint and the triage's "not hidden by a second client rule". The contrary sentence in objectstack's review 6102543061 ("while /meta/view/OBJECT.KEY refuses them by name") conflated an object's own entry with a container's expanded item; the server code and fixture, not that sentence, govern.
  • Pins: the three new files cover both refusal faces (with and without a sentence), the short-name canonicalisation, the split layout, the held CONTROLs (nothing asked), the three non-denied verdicts, the host adapter without a probe, draft preview, the loading window, the item-less 200, the code-not-status rule, empty inputs, and probeAppAccess as the same probe. The adapter pin runs the real client over a stubbed fetch, so the client's error stamping is inside the measurement. RIGHT.

Check-runs on the head, read at 2026-10-11T21:55Z (their conclusions are the gate verdicts): success for Lint, Type-adjacent and doc gates (Build & E2E, Build Docs, Lint, README Export Check, Doc Snippet Type Check, Doc Component Type Check, Doc Fence Language Check, Doc Example Id Check, Internal Docs Link Check, Docs Route Eager Closure Check, Skill Example Check, Skill Guide Path Check, Skill Eval Token Check), the changeset family (Changeset Declaration, Changeset Claim Re-read, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Overwrite Report), Governed Surface Queue Guard, Pre-Install Import Graph Check, Action Ref Convention, Control Byte Scan, Line Citation Gate, Inert vi.mock Specifier Check, Shell Escape Residue Scan, Bundle Analysis, Live E2E (informational), label, Test (dist pins) and Test (shard 2/8). Still in_progress, no conclusion yet: Test (shard 1/8), 3/8, 4/8, 5/8, 6/8, 7/8, 8/8, Type Check, Spec Main Shape Gate. Skipped by design: the two coverage jobs and dependabot. No failure read. The queue still waits for every required context to conclude; this record does not stand in for them.

② Semver level

  • .changeset/12112-meta-refusal-surface.md: @object-ui/data-objectstack: minor, @object-ui/app-shell: patch. RIGHT on both: the adapter package gains a published type and a public method (additive, so minor, and yes takes at least minor); app-shell publishes nothing new (a route-level behaviour change behind unchanged exports, so patch). No major, as the fixed group requires; Changeset Bump Policy and Changeset Declaration are success on the head. The body names what ships, including the D4 sentence, and names no translation key.
  • Clause-②: yes on the PR body's second line, at the line start, no arm. RIGHT: the public surface of a published package is expanded, which is the clause's own criterion ("does this card widen the accept set or expand the public surface"); the accept set of no published schema moves, so no (widening) or (narrowing) arm is owed. The claim comment 6109832410 still carries Clause-②: no, expressly labelled a prediction conditional on an i18n key. The diff overtook that prediction on a different ground (the adapter surface, not a pack key); the PR body carries the declaration the changeset gate reads, and this record is the contract-tier review that yes owes. The seat should re-declare yes on the card in its ACCEPT, as objectstack's precedent did on #22639, so the claim's arm and the PR's agree for any reader of the claim.

③ Boundary flags

Dev report 6114091125, each deviation:

  1. useMetaItemRefusal.ts is a new file outside the claim's file surface: ANSWERED. It is the one reader both claimed views use instead of two copies of the probe effect; as a new file it holds no other claim; the claim's own wording ("the console's existing refusal or empty-state component, if one fits") anticipated a shared piece. packages/data-objectstack/README.md gained the section AGENTS Add automated testing infrastructure and CI/CD workflows #2 owes a new public method. Accepted.
  2. The by-name door is the adapter's probeAppAccess widened, not MetadataProvider.getItem: ANSWERED. getItem resolves null on every failure for every useMetadataItem consumer; changing it would move all of them, and the adapter already owned the one by-name read that discriminates on the code. Accepted; it is also the smaller public-surface change.
  3. D4 measured as listed-and-served, nothing added: ANSWERED in ①. Accepted.
  4. The dashboard skeleton while the list loads: ANSWERED in ①. Accepted as part of the change (the refusal needs a settled list).
  5. Whole suites not re-run after the faf4ce9 merge: procedural; the declared narrowing is accepted, and the head's Test shards are the record (seven still running at the stamp above).
  6. Merge commits amended locally before push to carry the trailer pair: procedural, history not rewritten after push. Accepted.

open_questions, the one entry (the D4 class): ANSWERED, option A, from governing text. Ruling 6095014058 makes the gate "server-enforced" with the console's switcher following the server's list and "the switcher needs nothing"; scope note 6096254862 item 1 puts an object's own list views on the object door, judged by the server's one predicate; ADR-0106 D4 and the object arm of createMetaItemReadGate serve the whole object to isObjectSchemaMaskExempt callers on every door by design (a pruned load saved back would delete the withheld entries); triage 6103491895 forbids a second client rule. So the behaviour the dev measured is the declared contract, this card owes nothing for it, and A is the only option inside the ruling. B (a runtime reading of the object door distinct from its authoring reading) would be a new objectstack surface and a second reading of one document; nothing pulls for it, and if anyone wants it, it is a new objectstack card through the decision box, not a condition on this PR. C is refused by the ruling and the triage. Not escalated.

Reviewer flags, read in the diff, neither a FAIL:

  • A /meta/view ITEM the caller holds, opened by URL on a cold load, is probed once before the view list and savedViews land: ObjectView gates the probe on resolvedViewId alone, with no settled-status guard like DashboardView's getTypeStatus('dashboard'), and views is composed from the view cache and the async dataSource.listViews read. The answer is granted, so the face is today's once the list lands; the cost is one by-name read per such cold open and the list area withheld for that round trip where the default view used to flash first. An object's own entries resolve synchronously and are never probed. No accept-set change and no wrong answer; a tightening (gate the view probe on getTypeStatus('view') settling, as the dashboard path does) is a follow-up for the seat, carrier this seat, not a condition here.
  • The existing console.warn "falling back to the default view" still fires on a refused name before the refusal face replaces the list. Cosmetic; the user-facing answer is the refusal.

out_of_scope_findings, the one entry (MetadataProvider.getItem resolving null on every failure, and extractItem returning the item-less envelope as the item): NOTED, carrier none, as the dev filed it; read, not run, no public-door wrong answer measured, so not a class (a) finding; it stays in the PR's Acceptance notes. Nothing in this diff reads getItem.

Prose faces (.changeset body, content/docs/guide/console.md, the adapter README section) are the dispatching seat's ACCEPT to read sentence by sentence; they are outside the three contract faces and were not graded here beyond the front matter and the sentences ① relied on.

Implemented-by: claude/issue-12112-meta-refusal-surface
Reviewed-by: session_01TYgwmFK1q4KJ6Qq2WRLzsD

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 11, 2026 22:03
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 11, 2026 22:03
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit 43e1505 Oct 11, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12112-meta-refusal-surface branch October 11, 2026 22:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

data-adapter documentation Improvements or additions to documentation package: app-shell tests

Projects

None yet

2 participants