Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/9108-props-bag-loud-refusal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@object-ui/react": patch
---

**A node-gate predicate parked under `props` is now REFUSED BY NAME on the console, instead of gating nothing in silence (objectui#9108).**

A node may spell its config bag `properties` (the spec spelling) or `props` (the annotated legacy alias). `SchemaRenderer` hoists `properties.*` onto the node; nothing copies `props.*`, and both node gates read the post-hoist node — so a predicate that arrived under the alias was never one of the keys either gate could see. Measured at node level, four rows: `props: { visible: false }` and `props: { hidden: true }` both RENDERED, while `properties: { visible: false }` and `properties: { hidden: true }` each hid correctly. Fail-**open** and silent by construction: a gate that never bit renders exactly like a gate that said yes, so no user and no screenshot can find it.

**No verdict moves, and that is the ruled outcome rather than a limitation.** The alias is refused, ⛔ not honoured: nothing is hoisted, `schema.<KEY>` stays undefined for a renderer declared as `({ schema })`, and every element receives the byte-identical props bag it received before. What changes is that the eight node-gate predicate keys (`visibleWhen` / `visible` / `visibleOn` / `visibility` / `hidden` / `hiddenOn` / `disabled` / `disabledOn`) are named on the console when they are parked there, with the migration that fixes them. The maintainer ruling of 2026-09-13 closed the honour arm (PR objectui#9144) on the ground that honouring the alias would have made *"8 predicate keys work while the rest stayed silently dropped — and partly working is harder to learn from than not working"*.

The line is `console.error`, in development **and in production** — the same posture objectui#6038 gave the unresolvable-predicate report, because a node gate that has stopped biting in production is exactly the defect that must not be able to sit live and undiscovered. It is rate-limited to one line per distinct authoring bug for the lifetime of the page. One measured carve-out: `disabled` on a bag-reading `element:*` node is honoured by that renderer itself (`disabled={props.disabled || running}`), so it is not refused there.
60 changes: 59 additions & 1 deletion packages/react/src/SchemaRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { usePageVariables } from './hooks/usePageVariables.js';
import { resolveKeyedI18nLabel } from './utils/i18n.js';
import { isConfigBag } from './utils/configBag.js';
import { reportUnevaluatedExpressions } from './utils/unevaluatedExpression.js';
import { reportDroppedPropsBag } from './utils/propsBagDiagnostic.js';
import { reportDroppedPropsBag, reportRefusedPropsPredicate } from './utils/propsBagDiagnostic.js';
import { expressionBindableTextKeysFor } from '@objectstack/spec/ui';
import {
reportUnresolvableVisibilityPredicate,
Expand Down Expand Up @@ -330,6 +330,25 @@ const PREDICATE_CHAIN_KEYS: ReadonlySet<string> = new Set<string>([
...ENABLEMENT_RENDERER_KEYS,
]);

/**
* Every key a NODE GATE in this file actually consults, as ONE lookup for the
* objectui#9108 refusal below. DERIVED from the same two declarations the gates
* are built from, so a leg added to either chain is refused under `props` by the
* same edit that adds it.
*
* {@link PREDICATE_CHAIN_KEYS} minus {@link ENABLEMENT_RENDERER_KEYS}, and the
* subtraction is the whole reason this is a second derivation rather than a
* reuse: `enabled` is in that union because the config-bag evaluation loops
* flatten it, but NO gate here consults it - the action renderers read it one
* layer down off the schema and negate it. Refusing it here would state, of a
* key this file never asks about, that a gate in this file could not see it.
* Its own `props` drop is objectui#6708's subject and is reported there.
*/
const NODE_GATE_PREDICATE_KEYS: ReadonlySet<string> = new Set<string>([
...VISIBILITY_CHAIN_KEYS,
...ENABLEMENT_NODE_GATE_KEYS,
]);

/**
* Is this value the canonical CEL envelope — `{ dialect: 'cel', source }` —
* that `@objectstack/spec` normalizes an authored predicate into?
Expand Down Expand Up @@ -1423,6 +1442,45 @@ export const SchemaRenderer: ForwardRefExoticComponent<
newSchema.props = newProps;
}

/**
* REFUSE, by name, a node-gate predicate parked under the legacy `props`
* alias (objectui#9108, maintainer ruling 2026-09-13, verbatim 「同意」 on
* the `domain:spec` seat's recommendation).
*
* ## Sited HERE, immediately in front of the two gates
*
* This is the one point where the gates' own input is final: the
* `properties` hoist above has run, both config-bag evaluation loops have
* run, and neither gate has consulted anything yet. It is also the only
* placement that survives its own subject - the late diagnostics near
* `createElement` are downstream of `if (shouldHide) return null`, so a node
* that parks `visible` under `props` while ALSO hiding through the canonical
* spelling would never reach them, and the refusal would go missing on the
* one shape that carries both spellings at once.
*
* ## Read-only, and that is the ruled outcome rather than a limitation
*
* Nothing below changes. The gates still read the post-hoist node only, so
* every verdict, every hoisted value and every byte the element receives is
* what it was - the alias is REFUSED, not honoured. The opposite arm was
* built and closed (PR objectui#9144): honouring it would have made *"8
* predicate keys work while the rest stayed silently dropped - and partly
* working is harder to learn from than not working"*.
*
* The bag handed over is {@link propsWithoutCanonicalKeys}'s, the SAME
* subtraction the outgoing props bag uses, so a key the canonical bag also
* declares is not reported as parked: there the author is already getting
* the canonical answer (objectui#5123). The key SET is
* {@link NODE_GATE_PREDICATE_KEYS}, derived from the two chain declarations
* above rather than re-listed here.
*/
reportRefusedPropsPredicate(
newSchema.type,
newSchema.id,
NODE_GATE_PREDICATE_KEYS,
propsWithoutCanonicalKeys(newSchema.props, newSchema.properties),
);

// Evaluate visibility: visibleWhen / visible / visibleOn / visibility / hidden / hiddenOn
const shouldHide = (() => {
// `visibleWhen` is the single canonical conditional-visibility predicate
Expand Down
Loading
Loading