Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

A ${…} visibility predicate written inside properties is interpolated to a boolean before the visibility chain sees it, so no node-gate diagnostic can reach it #5756

Closed

Description

@os-zhuang

Where this came from

Measured while implementing #5687 (making a node-gate data.* predicate the adapter cannot answer emit a dev-only report). It is a different silence in the same chain, deliberately left alone there because the 2026-08-22 ruling on #5687 fences off interpolation ("no interpolation changes"). Filed unassigned for triage.

Duplicate search run before filing: semantic + keyword sweep of open issues over the properties hoist, the interpolation ordering, and the visibility-diagnostic surface (#5454, #5401, #4051, #3067, #5399) found nothing covering this.

Facts (measured on origin/main 3ece13e, plus @object-ui/core's built evaluator)

packages/react/src/SchemaRenderer.tsx's evaluation memo evaluates every properties.* value before the hoist that copies them onto the node. That ordering is deliberate and documented in place — "Evaluating first is what makes one key mean one thing whether it is read as schema.properties.x, as schema.x, or as the x prop."

Measured on ExpressionEvaluator.evaluate:

evaluate("data.status == 'draft'")    -> "data.status == 'draft'"   (string, verbatim)
evaluate("${data.status == 'draft'}") -> false                      (boolean)

Consequence for the two spellings of the same authored gate:

authored as reaches the visibility chain as a node-gate diagnostic can see it?
properties: { visible: "data.status == 'draft'" } the string, intact yes — this is #5687's pinned repro
properties: { visible: "${data.status == 'draft'}" } false (a plain boolean) no — nothing is left to inspect

The second row hides the block on every row exactly like the first, and the author gets no signal at all. Pinned as a known limit in packages/react/src/__tests__/SchemaRenderer.nodeGateDataPredicate.test.tsx ("KNOWN LIMIT, measured and pinned"), which asserts the verdict and the silence rather than papering over either.

Why this is not just a data.* problem

The pre-interpolation is spelling-agnostic. Any predicate written as a ${…} template inside properties arrives at the chain as an already-collapsed value, so no diagnostic that inspects the predicate at the gate — #5454's unresolvable-predicate reporter included — can ever fire for it. A record.* typo written as properties: { visible: "${record.staus == 'draft'}" } is in the same position.

This matters because ${…} is not an exotic spelling. @object-ui/types' own protocol comments use it for exactly these keys:

hidden?: string;    // expression: "${data.role != 'admin'}"
disabled?: string;  // expression

Scope note / why it was not fixed in #5687

Reaching it means adding a diagnostic inside the interpolation loop, gated on the key being one of the visibility keys. That is a small change, but it sits directly on the surface the #5687 ruling fenced off, and it needs a decision this card cannot take for itself:

Neither is hard; both are choices, not mechanics.

Reproduction

packages/react/src/__tests__/SchemaRenderer.nodeGateDataPredicate.test.tsx, case "KNOWN LIMIT, measured and pinned: a ${…} template INSIDE properties is invisible to this leg" — mounts the template spelling on two different rows, asserts the block is hidden on both, and asserts zero diagnostics. Flipping the expected report count from 0 to 1 is the failing reproduction.

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 23, 2026
  2. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Triage: pm:queue, domain:ui, Task — a ${…}-spelled visibility predicate collapses to a boolean before the chain, so the #5454/#5687 diagnostic family can never see it; the KNOWN-LIMIT pin already names the failing repro. Scope ruling carried with the card: the 2026-08-22 "no interpolation changes" fence on #5687 was that card's scope constraint, not a standing ban — this card is the deliberate carrier for that surface, and the constraint that DOES stand is: diagnostic only, interpolation results must not change (the evaluation memo's documented one-key-one-meaning ordering stays). The two design points the card names are the dev's to settle with stated rationale in the PR body, not escalation material: key the check on the visibility key set (the card's own analysis), and pick a defensible posture on the outranked-leg false positive — mirroring #5454's leg semantics if its reporter already solved this. Genuine interpolations (content: '${data.total}') must stay silent — pin that too.


    Generated by Claude Code

  3. claude commented on Aug 23, 2026

    @claude
    Contributor

    Claim — domain:ui execution seat.

    Dispatching this round (R27). ⚠️ The scope ruling triage carried is the load-bearing part and is repeated in the brief: the 2026-08-22 「no interpolation changes」 fence was #5687's own scope constraint, not a standing ban — this card is the deliberate carrier for that surface. What DOES stand: diagnostic only; interpolation results must not change.


    Generated by Claude Code

  4. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    os-dev-report

    {
      "issue": 5756,
      "status": "done",
      "branch": "claude/issue-5756-properties-predicate-diagnostic",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5820",
      "premise_still_valid": true,
      "summary": "Confirmed the PM's mechanism hypothesis: `SchemaRenderer`'s `properties.*` evaluation loop interpolates every `${…}` template before the visibility chain sees it, so a template-spelled predicate arrives at the gate already collapsed to a boolean with no text left to diagnose. Fixed by moving `evaluateVisibilityPredicate` earlier in the render memo and adding a second diagnostic-only call site inside the properties loop, on the raw pre-interpolation text of whichever visibility key actually WINS the precedence chain (a new `winningVisibilityKey` helper, computed post-properties-eval/pre-hoist so its declaredness answers agree bit-for-bit with `shouldHide`'s later ones). Reports only the winning key (mirrors #5454's own leg semantics, so an outranked `properties.visible` template does not get reported for deciding nothing) and only visibility keys (a `properties.content` interpolation stays untouched). No verdict change, no interpolation change: the diagnostic call's return value is discarded.",
      "tests": "At 212e14a37 (HEAD): `pnpm exec vitest run packages/react/src/__tests__/SchemaRenderer.nodeGateDataPredicate.test.tsx` -> 23/23 passed (18 pre-existing, one flipped 0->1 report as the card predicted, one production case extended; 5 new in a `#5756 group 5` block covering the two named design points plus a value-level interpolation-unchanged control). `pnpm exec vitest run packages/react/ --maxWorkers=2` -> 711/711 passed, no regressions. `pnpm --filter '@object-ui/react^...' build` clean (dependency closure). `pnpm --filter '@object-ui/react' type-check` (tsc --noEmit + tsconfig.test.json) clean. `pnpm --filter '@object-ui/react' build` (tsc) clean. `pnpm exec eslint <2 touched files> --no-inline-config` -> 0 errors, 19 pre-existing any-warnings, diff adds zero new `any` (verified via `git diff origin/main | grep any`). `node scripts/check-control-bytes.mjs`, `check-changeset-presence.mjs`, `check-changeset-no-major.mjs` all OK; changeset declared as `@object-ui/react: patch`. Reverse verification: disabled the new call site (`if (false && __DEV__)`), reran the pinned file -> exactly the 3 tests asserting a NEW positive report turned red (flipped KNOWN-LIMIT test, hidden-leg 5c, value-control 5e); the negative/control tests (outranked-leg 5a, genuine-read-silent 5b, unresolvable-goes-to-other-reporter 5d) stayed green since they assert silence regardless. Restored; `git diff` against the committed fix came back empty (byte-identical round trip).",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:queue

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions