Skip to content

fix(react,app-shell,components): choose the visibility-fault advice paragraph by scope tier - #6495

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-6487-reporter-advice-roots
Aug 26, 2026
Merged

fix(react,app-shell,components): choose the visibility-fault advice paragraph by scope tier#6495
os-support-ai merged 1 commit into
mainfrom
claude/issue-6487-reporter-advice-roots

Conversation

@claude

@claudeclaudeBot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Fixes#6487

formatUnresolvableVisibilityMessage closed every report with the node tier's roots. Since objectui#6443 wired the app-shell chrome gate onto the same reporter, that paragraph also printed for an evaluator that binds neither of the two roots it names — so a nav, area or field visible predicate that faulted told its author to go and check record and page state, at a tier where neither exists, while the roots that do exist went unnamed.

Both exported functions now take an optional sixth argument, a PredicateScopeTier, selecting the closing paragraph. No verdict moves on any surface, and everything above that paragraph is unchanged.

The root sets were derived from the code that binds the scope, not from the card

The card asked for this and it was worth doing — one of its numbers does not survive the measurement.

'page-component'packages/react/src/SchemaRenderer.tsx:585:

newExpressionEvaluator({
...predicateScope,current_user: predicateScope?.user,
...(boundRecord ? {record: boundRecord} : null),data: dataSource,page: pageVariables,})

record, current_user and page state — the three roots @objectstack/spec's ui/page.zod.ts declares for the tier. page:tabs item predicates sit here too, measured rather than assumed: packages/components/src/renderers/layout/containers.tsx:451 builds the same three roots (plus the row spread flat and data aliased to the row — breadth that is undeclared on both surfaces, so it is advertised on neither). This paragraph is byte-for-byte what it was.

'app-shell'packages/app-shell/src/providers/ExpressionProvider.tsx:59:

constcontext={current_user: user, user,ctx: { user },os: { user }, app, data, features };

No record, no page. The line now names current_user with its three ADR-0068 alias spellings, app, and features.

Where the measurement disagrees with the card

The card says five bound roots go unnamed. Measured, six are: user, ctx.user, os.user, app, featuresand data, which the card's own quoted context bag contains but its "go unnamed" list omits.

data is still deliberately not advertised, for a reason the card did not state: every mount of ExpressionProvider in this repo passes data={{}} (console/AppContent.tsx:925, views/RecordFormPage.tsx:270) or omits it (apps/console/.../InternalFormRoute.tsx:81). Naming it would point an author at a root that answers nothing. So the card's advice is right and its count is off by one; the difference is recorded here rather than smoothed over.

Why parameterised, and why the copy was not generalised

Three callers, and the tiers genuinely differ — record and page state are bound at one and absent at the other. Generalising would have made one paragraph true everywhere at the cost of making it useful nowhere: an author who mistyped a root needs to know which roots exist at their tier. A test cell pins that refusal directly (still names CONCRETE roots), and the ablation below shows the generalisation turning both tiers red.

Deducing the tier from the existing type slot was also refused: type carries the schema node type at the node tier and a surface label at the other two, and page:tabs is a real registry key — so a page:tabsnode faulting its own visibleWhen inside SchemaRenderer is indistinguishable by type from one of its item predicates. An explicit argument cannot collide that way.

tier is not in the dedupe key: the objectui#6038 rate limit is untouched. Adding it could only loosen the limit, and loosening needs one type shared across two tiers — which production cannot produce, since the app-shell site's type is the constant 'app-shell:visible'.

Published-surface check (clause ②) — reachable, and the shape grew

Followed to the built entry rather than inferred from an export keyword. @object-ui/react's exports map declares exactly one entry ("." -> ./dist/index.d.ts), and packages/react/src/index.ts:30-35 re-exports both functions by name (no wildcard hop is involved for this module — grep confirms utils/visibilityDiagnostic.js is imported from exactly two places, the entry and SchemaRenderer.tsx).

Yes, both are reachable from the package entry, and yes, the shape grew. From the rebuilt packages/react/dist/utils/visibilityDiagnostic.d.ts:

exporttypePredicateScopeTier='page-component'|'app-shell';exportdeclarefunctionformatUnresolvableVisibilityMessage(type: unknown,id: unknown,key: string,raw: unknown,reason: string,tier?: PredicateScopeTier): string;exportdeclarefunctionreportUnresolvableVisibilityPredicate(type: unknown,id: unknown,key: string,raw: unknown,err: unknown,tier?: PredicateScopeTier): void;

Nothing breaks: the argument is optional and defaults to 'page-component', and a cell pins that the five-argument call is byte-identical to the explicit node-tier call. The default is a compatibility shim for callers outside this repo — all three in-repo call sites state their tier.

Tests

Everything below was run at df9de7836, the final commit. Vitest resolves @object-ui/react to packages/react/src (vitest.config.mts:262), so the suites read source and no rebuild sits between a source edit and a test verdict — that is why the ablations below rebuild nothing and why saying so is part of the claim.

SuiteResult
packages/react/ (whole package)Test Files 58 passed (58) / Tests 854 passed (854)
packages/app-shell/src/providers/Test Files 10 passed (10) / Tests 73 passed (73)
the 7 page:tabs / page-container component suitesTest Files 7 passed (7) / Tests 49 passed (49)

New: packages/react/src/utils/visibilityDiagnostic.adviceTier.test.ts (17 cells) — the tier matrix, with load-bearing assertions placed only where the tiers disagree. current_user is named at both tiers, so the cell asserting it is kept but labelled degenerate, because on its own it is green against the defect too.

Extended, one end-to-end cell per real call site, each asserting a fault reached the reporter in the same run so none can pass on a reporter that stopped reporting: ExpressionProvider.visibleFaultDiagnostic.test.ts, page-tabs-visible-when-fault-warning.test.tsx, SchemaRenderer.visibleWhenRecordBinding.test.tsx.

Ablations — direction predicted before each run, mutation proven on disk, restore proven

Each leg pinned its restore to the literal SHA df9de783675bbf3b27aa90124f25086e7cb7e00c (never origin/main, which is shared across worktrees), verified the mutation with a grep -c on the exact injected and deleted text, and proved the restore with git diff HEAD --quiet -> 0.

A. Revert the app-shell call site to 'page-component' (literally the pre-fix behaviour). Predicted: the app-shell tier cells red, everything else green. Measured: Test Files 1 failed | 3 passed (4) / Tests 2 failed | 61 passed (63) — red were exactly a faulting nav predicate is NOT told to check ... and it names the roots this provider really binds — including features. The two app-shell cells labelled controls stayed green, as labelled.

B. Delete the concrete root names from both paragraphs (the fork the card fenced off). Predicted: both tiers red, at all three call sites. Measured: Test Files 4 failed (4) / Tests 14 failed | 49 passed (63).

C. Type ablation — pass a bogus tier at the app-shell call site. Predicted red, and specifically that @object-ui/app-shell's tsc reads the rebuilt.d.ts rather than a cached five-argument one. Measured Exit status 2:

src/providers/ExpressionProvider.tsx(209,7): error TS2345: Argument of type '"app-shellABLATION6487"'
is not assignable to parameter of type 'PredicateScopeTier | undefined'.

The error names the new union by name, so the green cross-package type-check above is a measurement of the new signature, not of a stale declaration.

Gates

Quoted from each gate's own verdict line; exit codes captured before any pipe.

  • pnpm --filter @object-ui/react|@object-ui/app-shell|@object-ui/components run type-checkEXIT=0 for all three (tsc --noEmit && tsc -p tsconfig.test.json, so the new tests are in the program: --listFiles reports the new file present, 1 match).
  • check:control-bytescheck-control-bytes: OK (scanned 5369 tracked text file(s); skipped 85 binary).
  • check:readme-exports — first run exited 1 on its own non-vacuity guard (36 unbuilt, census collapsed below FLOORS), which is a missing prerequisite and not measured, not red. After the build the workflow itself runs (turbo run build --filter='./packages/*'): check-readme-exports: OK (... 378 self-imports judged (378 real, 0 wrong-path, 0 fabricated); 3257 export symbol(s) read from 37 of 40 package(s) ... 0 unbuilt ...). No baseline or floor was touched.
  • check:self-import, check:phantom-deps, check:vi-mock-specifiers — all EXIT=0 with a real census.
  • check-changeset-presence8 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s); check-changeset-no-majorNo changeset declares a major bump.

Lint, narrowed — and the narrowing measured

eslint on the 10 changed files: 0 errors, 122 warnings, and 0 of those warnings land on a line this commit added (checked by intersecting the --format json message line numbers with the commit's added-line ranges). Every warning is pre-existing @typescript-eslint/no-explicit-any in files this PR only appends to. The lint job blocks on errors only, by its own workflow comment.

The narrowing is a measurement, not an omission:

  1. Population, read from eslint's own config rather than guessed — ESLint#isPathIgnored over every tracked file with a lintable extension: 4462 candidates, 3818 not ignored. That is what a full run checks.
  2. Files linted this run: 10, counted from the length of the --format json array.
  3. Invariance for the other 3808: eslint.config.js declares no parserOptions, no project and no projectService, so type-aware linting is off; and none of the 8 custom object-ui/* rules reads the filesystem (grep -l readFileSync|require('fs')|from 'fs'|from 'node:fs'|globSync eslint-rules/*.js -> no non-test match). Every rule judges one file's own AST, so a file this PR does not touch cannot change verdict because of this PR.

Filed, not fixed

#6493 (finding, unassigned) — evaluateVisibility is reached by three evaluators, not one. views/RecordFormPage.tsx:183 and console/AppContent.tsx:671 build ad-hoc ExpressionEvaluators binding only { user, app, data }, bypassing the provider they sit beneath, so current_user, the alias spellings and features are unbound for an object field's visible there — and the app-shell paragraph this PR lands over-promises at those two sites. The repair belongs at the producer, and binding current_user is a behaviour change (objectui#6110's changeset documents inert gates going live and asks for a metadata audit), so it needs its own card and its own changeset warning rather than a rider here. Not a duplicate of objectui#6262, which is the provider being under-fed on /forms/:name rather than bypassed.

Note for #6445

objectui#6445 adds a third caller — the disabled / disabledOn gate in packages/react/src/SchemaRenderer.tsx. This shape barely constrains it: that gate uses the sameevaluator as the visibility chain, so its roots are identical and it passes 'page-component', or nothing at all. What #6445 changes is the first paragraph's polarity, which is per-message and not per-tier, so the two cards do not collide in the advice table. A cell in the app-shell suite pins that the first paragraph is unchanged by this card, so if #6445 does re-tier it, that cell is where it will say so.


Generated by Claude Code

…y scope tier
`formatUnresolvableVisibilityMessage` closed every report with the NODE tier's
roots — `record`, `current_user`, `page.<var>`. Since the app-shell chrome gate
was wired onto the same reporter, that paragraph also printed for an evaluator
built from `{ current_user, user, ctx: { user }, os: { user }, app, data,
features }`, which binds neither `record` nor `page`. A nav author whose
`visible` predicate faulted was sent to check two roots that cannot exist at
their tier, while the roots that do exist went unnamed.
Both exported functions now take an optional `PredicateScopeTier` selecting the
closing paragraph, and all three in-repo call sites state their tier rather than
inherit the default. Each tier's root set is derived from the code that builds
the bag: `SchemaRenderer` and the `page:tabs` item gate bind the three roots the
spec declares for a page component, `ExpressionProvider` binds the ADR-0068
identity aliases plus `app` and `features`.
The concrete root names are deliberately kept at both tiers. Generalising the
copy would have made one paragraph true everywhere and useful nowhere: an author
who mistyped a root needs to know which roots exist at their tier.
No verdict moves, on any surface. Everything above the advice paragraph — the
prefix, the node, the gate key, the predicate source, the engine's reason and
the "gate did NOT bite" sentence — is unchanged, and so is the objectui#6038
dedupe key.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3234.6 KB3266.6 KB
Main entry chunk (gzip)157.4 KB350 KB
Entry fileindex-DKoSokmu.js
StatusPASS

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


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.30KB4.28KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.01KB114.64KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.91KB12.92KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.60KB44.82KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)211.90KB42.74KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)131.78KB32.19KB
plugin-gantt (index.js)164.14KB39.87KB
plugin-grid (index.js)201.66KB54.57KB
plugin-kanban (index.js)53.16KB14.65KB
plugin-list (index.js)112.74KB27.50KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.85KB20.79KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)57.05KB19.13KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.05KB1.04KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Sign up for freeto 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.

reportUnresolvableVisibilityPredicate's advice paragraph names node-tier roots (record, page.<var>) that no app-shell visible predicate binds

2 participants

@os-support-ai@claude