Repository navigation
fix(react): a page node whose visibility predicate faults is withheld (objectui#13299) - #13302
Merged
objectstack-fleet[bot] merged 1 commit intoOct 21, 2026
Conversation
… (objectui#13299) A declared visibleWhen / visible / visibleOn predicate that cannot be evaluated (over the CEL bounds, a type error, an unbound root, a missing function) now fails closed: the node is not rendered, the direction objectui#13286 set for a page:tabs item. The fault is read off the single engine call's onFault in production (no second throwOnError evaluation), reported once through the shared reporter with the 'withholding' consequence. hidden / hiddenOn already hid on a fault and are unchanged; a blank gate still means "no gate". The page:tabs count-probe gate mirrors the node gate, so a badge does not count a withheld list. Form field rules keep their fail-open. Existing fail-open pins are flipped. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
This was referenced Oct 21, 2026
Closed
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
objectstack-fleet
Bot
deleted the
claude/issue-13299-node-visibility-fail-closed
branch
October 21, 2026 06:26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #13299
Clause-②: no
What
A page node whose DECLARED visibility predicate faults on evaluation (over the CEL AST bound, a type error, an unbound root, a missing function) is no longer rendered. As objectui#13286 already did for a
page:tabsitem, page visibility gates now fail closed.packages/react/src/SchemaRenderer.tsx:visibilityGateKindbecamevisibilityFaultOutcome(key, raw), which answers both what a fault DECIDES on a leg and which consequence the report prints.evaluateVisibilityPredicateuses it in both branches. Production keeps ONE engine call:onFaultrecords the fault on that call and the faulted call answers with the fail-closed verdict (no secondthrowOnErrorevaluation). The dev branch'sthrowOnErrorcatch returns the same verdict. Reports go through the sharedreportUnresolvableVisibilityPredicateand its dedupe set, with the'withholding'consequence on a declared SHOW-leg predicate. The docblocks that said the fail-open semantics were "not its to move" are rewritten.packages/components/src/renderers/layout/containers.tsx: thepage:tabscount-probe gateisGateOpenmirrors the node gate (sameonFaultflag, samehasDeclaredPredicatetest, same'withholding'copy), so a badge counts what is drawn.content/docs/guide/metadata-diagnostics.md: page visibility gates (node and tab item) fail closed; form field rules keep their fail-open. The node example's consequence line, which said the gate "did NOT bite", is replaced by a sentence naming what the fault does on each leg..changeset/13299-node-visibility-fail-closed.md: patch for@object-ui/reactand@object-ui/components.Every leg, and where a fault lands
shouldHidevisibleWhenfalse-> not rendered (was: shown) · copy'withholding'true-> shown · copy'visibility'visible'withholding''visibility'visibleOn'withholding''visibility'hiddentrue-> not rendered (unchanged) ·'concealment'hasDeclaredPredicateguard)hiddenOn'concealment'disabled/disabledOnroute throughevaluateEnablementPredicate, which shares no helper with the visibility chain; they are unchanged.hasDeclaredPredicateis asked only on the fault path, because it reports a blank gate as a side effect and a non-faulting predicate must stay as silent as it was.Pins
New:
packages/react/src/__tests__/SchemaRenderer.nodeVisibilityFailClosed-13299.test.tsx(rows a-d, each in the production and the development module graph) andpackages/components/src/__tests__/page-node-visibility-fail-closed-13299.test.tsx(rows e and f).visibleWhenis not rendered, and two renders log ONE warning that says WITHHELD.visible,visibleOn(and a faulting CEL envelope onvisibleWhen) are not rendered;hidden,hiddenOnare not rendered with the concealment copy.''and blank envelope show; no WITHHELD line.[bounds]reason is asserted in production only: the devthrowOnErrorprobe reports a generic CEL reason for the same fault (pre-existing, see Acceptance notes).visibleWhenthat faults is still drawn.Existing fail-open pins flipped (each now asserts withheld)
packages/react:SchemaRenderer.productionPredicateFaultWarning.test.tsx: the seven verdict assertions in groups 1, 2 and 4 (BARE, CEL, template, three-ids, distinct-source, two-hundred-row, dev-vs-prod).SchemaRenderer.concealmentGateFaultDiagnostic.test.tsx: group 2 (three negated legs) now asserts withheld + the withholding clause, with the old "did NOT bite" sentence pinned absent.SchemaRenderer.visibleWhenRecordBinding.test.tsx: "binds the record ROOT only", leg 3 "warns ... and WITHHOLDS", "the bare-string dialect is loud too", the production case, and the objectui#6487 advice case.SchemaRenderer.nodeGateDataPredicate.test.tsx: 3b and 5d.SchemaRenderer.dataRootUnbound-9308.test.tsx: leg 7.SchemaRenderer.disabledGateFaultDiagnostic.test.tsx: the visibility-copy tail in group 4 is now the withholding paragraph.Outside
packages/react(the same flip, made false by this change; declared as a file-surface extension):packages/components/src/__tests__/page-tabs-count-visible-when-12987.test.tsx: the fail-open count case now asserts the list is not counted.packages/components/src/renderers/complex/__tests__/data-table-empty-action-visible-when.test.tsx: the faulting emptyAction is withheld.packages/components/src/renderers/form/__tests__/predicate-scope-parity-6010.test.tsx: the FAULTED group now expects the page node closed and the four form surfaces open.packages/app-shell/src/providers/__tests__/hasCapability-13006.render.test.tsx: three cases (not loaded, unreported, refused map) now withhold the node.packages/plugin-detail/src/renderers/__tests__/record-alert.rowBinding.test.tsx: group C shorthand is withheld on both rows.Verification (at
321c1f244d)--maxWorkers=1, under the shared lock: react family namingSchemaRenderer(55 files, 893 tests) run first; the 7 react files touched re-run green (130/130);page:tabsfamily + new components file (12 files, 77/77); consumer files in app-shell / plugin-detail / components / apps/console / examples (24 files) re-run after the flips, then the four flipped ones (37/37); a wider sweep of 19 files that renderSchemaRendererwith a gate and a fault/warn assertion (586/586).--filter '@object-ui/app-shell^...' --filter '@object-ui/plugin-detail^...'exit 0, thentype-check(which runstsc --noEmit && tsc -p tsconfig.test.json) for react, components, app-shell, plugin-detail: all exit 0.lintscript runs eslint: exit 0, 0 errors. Population:eslint.config.jslints**/*.{ts,tsx}; count from--format json; invariance: the config sets noparserOptions.project/projectService, so type-aware linting is off and this diff cannot move a verdict on an untouched file.check:phantom-deps,check:self-import,check:unreferenced-sources,check:doc-example-readers,check:handler-key-reads,check:side-effects-array,check:element-data-source-declaration,check:spec-symbols,check:action-forward-parity,check:designer-field-key-parity,check:icon-record-names,check:i18n-keys,check:i18n-drift,check:i18n-designer-parity,check:control-bytes,check:new-line-citations,check:test-path-roots,scripts/check-hand-rolled-comment-mask.mjs,scripts/check-changeset-fixed.mjs,scripts/check-changeset-presence.mjs. Control-byte self-scan over the diff: no hits.packages/react,packages/components,packages/app-shell,packages/plugin-detail(no whole-directory runs here, per the dispatch), and every other package's suite.Ablation (one-shot, nothing left in the tree)
Restored the fail-soft verdict on the node gate in both branches (
return fault ? fault.verdict : verdict;toreturn verdict;, and the dev catch'sreturn fault.verdict;toreturn true;) throughablation-replace.mjs(anchor hits 1 -> 0 on disk for each, under a restore trap), then ran both new files: 10 failed / 12 passed. Red: (a) x2 builds, (b)visible/visibleOn/ CEL envelope x2 builds, (d) x2 builds. Green: (c) x2, (f), and (b)hidden/hiddenOnx2 builds. Thehidden/hiddenOnrows staying green is the expected direction, not a hole: their fail-softtruealready hid, so the ablation does not move them. (e) stays green too because the count-probe gate is a separate site the ablation does not touch. Restore proven by blob hash:SchemaRenderer.tsxblob after restore = HEAD blobe2f0700ce56e,git diff HEADempty.Acceptance notes
record.*node gate on a surface where no row is bound (including a record page before the row has loaded) is now withheld rather than shown; acurrent_user.hasCapability(...)node gate is withheld while the capability list is not loaded, and stays withheld on a backend that never reportssystemPermissions; a row-action shorthand (status == 'x', unbound since objectui#5741) withholds its node. None of these looked like essential content that needs fail-open, so no fork is reported.'withholding'consequence copy invisibilityDiagnostic.tswas written for the app-shell navigation gate and says "a navigation entry". It now prints for page nodes (and since objectui#13286 for tab items). That file is outside this card's surface; not changed here.throwOnErrorprobe reports a generic CEL reason for an over-budget source, while production'sonFaultreports the engine's[bounds]reason. Pre-existing; the verdict and the line count agree.record:alert's own renderer-leveluseCondition(gate 2) is still fail-soft. End to end the node chain now withholds the shorthand case first, so it only shows when a node-levelvisibleWhenwins the chain and a faultingproperties.visiblereaches gate 2.Generated by Claude Code