Skip to content

fix(components): stop the sparse-predicate warning blaming hidden: true (#5399) - #5592

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-5399-sparse-predicate-warning-cause
Aug 21, 2026
Merged

fix(components): stop the sparse-predicate warning blaming hidden: true (#5399)#5592
os-sales merged 1 commit into
mainfrom
claude/issue-5399-sparse-predicate-warning-cause

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#5399

The defect

packages/components/src/renderers/layout/containers.tsx — the [page:header]
missing-field diagnostic named the action, the missing fields and the predicate
source (all measured, all correct), then closed with a causal sentence:

Hidden (hidden: true) fields are stripped from detail payloads server-side, so a
predicate gating on one may evaluate to a hide-by-default verdict.

That cause is false, and it names a mechanism this repo does not own. An author who
read it went looking for a hidden: true on the field and found either none, or one
on a field the payload demonstrably still returns — while the real source of the
sparseness (a projected or partial read) went unexamined.

Premise check — confirmed false, not taken on trust

Verified against the framework checkout at 112a8c6, four independent ways. Three
of these are my own reading of the running framework, not a restatement of triage:

  1. The spec calls it a UI concern.packages/spec/src/data/field.zod.ts:1234
    hidden: z.boolean().default(false).describe('Hidden from default UI').
  2. ObjectQL says so in the code that had to work around it.
    packages/objectql/src/search-companion.ts:370 and its conformance suite:
    the companion column is declared hidden + readonly + system, and
    "None of them is a PROJECTION rule". A query naming no fields reaches the
    driver with ast.fields undefined and every driver answers that with SELECT *.
    The one column that is withheld needed a purpose-built strip rule — which is
    only necessary becausehidden does not project.
  3. The framework enumerates what the read path actually drops.
    packages/metadata-protocol/src/protocol.ts:8073 names the complete set:
    omitInternalFields for internal: true columns, and
    stripSearchCompanionFromRead for the __search companion. hidden is not in it.
  4. The only read-side uses of field.hidden are something else entirely.
    protocol.ts:7020 / :7053 are auto-view and auto-form column generation (a UI
    concern, exactly as the spec describe says), and search-companion.ts:136 is an
    eligibility gate for what may be denormalized into the companion. Neither removes
    a key from a record body.

Triage's live-server probe (both hidden: true business fields present in the payload,
__search the only absent key) is consistent with all four and I did not re-run it.

The fix

Message text only. The measured half is untouched; the causal sentence is replaced with
the fact this surface can actually see plus the consequence it does own:

This page bound a record payload that does not carry those key(s) — a projected or
partial read ($select, an embedded card, a custom page passing a projected record)
will not include them — so the predicate fails closed and the action stays hidden.

No behaviour change. What triggers the warning is byte-identical; the only changed
executable line in the diff is the string literal inside console.warn.

The two things the card asked me to check

1. Does a test assert this message? Yes — page-header-actions.test.tsx:842 asserts
/not present in the record payload/ and toContain('secret_level_2358'). Both sit in the
measured half of the message, which is unchanged, so no assertion went red. Confirmed
by the suite passing, not by inspection alone.

2. Is the same claim repeated elsewhere? Yes, in four places, all of them annotations of
this same call site — not other warners, not prose docs. Because they document the exact
function being corrected, leaving them would ship the fix underneath a comment restating the
falsehood, and the next reader would revert the message to match. All four are inside the two
files this card's scope already names, and each is called out here rather than swept silently:

  • containers.tsx:977 — the paragraph introducing the warner ("fields stripped from the
    payload server-side")
  • containers.tsx:983 — the docstring of warnMissingRecordFields itself ("the server strips
    hidden: true fields from detail payloads"); it now also carries an explicit note against
    re-attributing the cause, so this cannot quietly regress
  • page-header-actions.test.tsx:828 and :837 — the comments of the test that pins the message

Nothing outside those two files was touched. The sweep found no other site carrying this
claim: every other "server strips" hit in the repo is about write stripping (readonly
fields dropped from a save), which is a different and real mechanism. One adjacent site worth
recording but not a duplicate:
apps/console/src/pages/system/ApprovalsInboxPage.rawPayloadGate.test.tsx:39 explicitly says
trimming a payload by hidden: true is not asserted there and is tracked elsewhere — which
is consistent with this finding rather than a repeat of it. No new issue filed; nothing found
that needs one.

Verification

There is no meaningful ablation for a message-text change and I did not manufacture one —
mutating a string literal to watch an assertion flip would be theatre, and the assertion that
matters deliberately does not cover the changed sentence.

All gates run at the tree now committed as 059cb503d (working tree was clean at commit),
exit codes captured before any pipe, each gate's own verdict line quoted:

GateVerdict
pnpm --filter @object-ui/components type-checkVERDICT command-exit 0 (script echoed tsc --noEmit && tsc -p tsconfig.test.json, so not a zero-match)
pnpm --filter @object-ui/components lint896 problems (0 errors, 896 warnings) — all pre-existing warnings
pnpm exec vitest run packages/components/ (repo root)Test Files 174 passed (174) · Tests 1576 passed (1576)
node scripts/check-control-bytes.mjscheck-control-bytes: OK (scanned 4645 tracked text file(s); skipped 85 binary)
node scripts/check-changeset-presence.mjs2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
node scripts/check-changeset-no-major.mjsNo changeset declares a major bump.
node scripts/check-changeset-fixed.mjsAll workspace packages are in the changeset fixed group.

Re-derived from the actual changed paths rather than trusting the dispatch list, and ran the
additional gates that could plausibly be implicated: check-i18n-call-site-keys,
check-i18n-en-drift, check-i18n-dead-keys, check-action-forward-parity,
check-lint-coverage, check-type-check-coverage, check-doc-links — all exit 0.

Declared narrowing: lint was run package-scoped rather than repo-wide. It cannot hide a
failure for this diff — pnpm --filter @object-ui/components lint runs eslint . across the
whole package, and both changed files are inside packages/components and appear by name in
its output. The dependency closure was built first
(pnpm --workspace-concurrency=2 --filter "@object-ui/components^..." build, exit 0), so the
type-check read fresh .d.ts rather than stale or absent output. CI runs the full farm.


Generated by Claude Code

…rue` (#5399)
The `[page:header]` missing-field diagnostic ended with a causal sentence that
named a mechanism this repo does not own, and named it wrongly:
Hidden (hidden: true) fields are stripped from detail payloads server-side,
so a predicate gating on one may evaluate to a hide-by-default verdict.
`hidden` is a UI concern, not a projection rule. Verified against the framework
checkout rather than taken on trust: the spec describes the key as "Hidden from
default UI"; ObjectQL's dedicated strip for the `__search` companion documents
that the `hidden` / `readonly` / `system` markers are "None of them is a
PROJECTION rule" — which is exactly why that one column needed a purpose-built
strip; drivers answer a query with no `fields` using `SELECT *`; and
metadata-protocol enumerates what the read path does drop (`internal: true`
columns and the `__search` companion, nothing else). The only read-side uses of
`field.hidden` in the framework are auto-view/auto-form column generation and
companion-source eligibility, neither of which removes a key from a record body.
So the diagnostic sent authors hunting for a `hidden` flag they would not find,
or would find on a field the payload still returns, while the real source of the
sparseness — a projected or partial read — went unexamined.
The replacement states the fact this surface can see and the consequence it owns:
the page bound a payload without those keys, a projected or partial read will not
carry them, so the predicate fails closed and the action stays hidden. The
measured half of the message (action name, missing fields, predicate source) is
unchanged, and what triggers the warning is untouched.
Message text only, no behaviour change. The same false claim also sat in this
warner's own doc comments and in the comments of the test that pins the message;
both are corrected, and the docstring now carries an explicit note against
re-attributing the cause. No other call site was swept.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3785.0 KB3867.2 KB
Main entry chunk (gzip)151.2 KB350 KB
Entry fileindex-CDxss6tw.js
StatusPASS

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


📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)8.91KB2.99KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.99KB113.73KB
core (index.js)4.51KB1.80KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.33KB
fields (index.js)237.61KB59.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.72KB18.35KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.53KB32.97KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.15KB60.89KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.07KB30.43KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.70KB27.17KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.50KB20.68KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)43.66KB14.77KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sales
os-sales marked this pull request as ready for review August 21, 2026 16:20
@os-sales
os-sales added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 5a07e67Aug 21, 2026
23 checks passed
@os-sales
os-sales deleted the claude/issue-5399-sparse-predicate-warning-cause branch August 21, 2026 16:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sales@claude