You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
SchemaRenderer's node visibility gate resolves a data.* predicate against the data-source ADAPTER, so a hoisted properties.visible written that way hides the block on every row, silently #5687
Found while implementing #4807 (row binding for record:alert's own properties.visible). It is one tier up from that card and was deliberately left alone there. Duplicate search run before filing: keyword + symbol sweep over open issues (SchemaRenderer / node-level visible / data.* / adapter / #5330 follow-ups) found nothing.
packages/react/src/SchemaRenderer.tsx hoists every properties.* value onto the node, so properties.visible arrives at the node-visibility chain as schema.visible and is evaluated there.
That chain's evaluator is built with data: dataSource — the data-source ADAPTER from SchemaRendererContext, defaulting to the module constant NO_DATA_SOURCE = {} when no provider is mounted. The docblock states this binding deliberately: data is what ${data.total} in a props bag resolves against, and the row must not overwrite it.
Consequence: an author who writes properties: { visible: "data.status == 'draft'" } on any block gets data.status resolved against the adapter, not the row. It does not throw, so it reads as a real verdict: undefined == 'draft' is a constant false, and the block is hidden on every row.
#5330 ruled B (maintainer, 2026-08-20): canon is record.*, and the row-action shorthand plus legacy data.* are deprecated but kept behind a survey-sized window. usePredicateRecordContext implements exactly that at the useCondition tier. Whether the ruling's reach extends to the node tier is a genuine contract question rather than a mechanical fix, because at that tier data already means something else and something documented:
binding the row over data there would change ${data.*} interpolation in props bags — the docblock calls that out as "a silent interpolation change nobody asked for";
packages/plugin-detail/src/renderers/__tests__/record-alert.rowBinding.test.tsx (added by #4807) is the standing evidence: its data.* leg has to hold the node gate open with a declared visibleWhen: cel('true') before the row-tier binding is observable at all, and the file's header states why. Dropping that isolation is the reproduction — the block goes hidden on both rows.
Scope note
record:alert was fixed at its own tier by #4807 and is not affected by whatever is decided here for the canonical record.* spelling; only the deprecated data.* spelling is, and only through the node gate. This issue is filed unassigned for triage.
Cross-reference — this finding is adjacent to #5401 / #5454, and the two must not be conflated.
This card records that SchemaRenderer's node visibility gate resolves data.* against the data-source adapter. #5401 was re-ruled on 2026-08-22 to option B — bind record into that same node-level evaluator so visibleWhen means what packages/spec/src/ui/page.zod.ts:189 advertises — and is now a child of umbrella #5454.
Same evaluator, two different unbound/misbound identifiers:
Consequence for whoever picks either up: a sweep for one bucket will surface the other if it is written loosely, and the two have different correct fixes. #5401's survey (dispatched now, round 16, survey-only per the ruling's ordering constraint) is instructed to label each site by bucket for exactly this reason. Don't let a data.* site be counted as evidence for the record.* binding change, or vice versa.
Concentration-round first-touch (triage seat): finding → decision inbox (needs-user-decision) — this is an instance the #5330 ruling (2026-08-20, option B) did not explicitly cover: at the node tier, data already means the adapter by documented design, so extending the row-binding there collides with a documented interpolation contract. That reach question belongs to the ruling's owner.
Maintainer ruling recorded — A: the node tier keeps its documented data = adapter semantics; the deprecated spelling's constant-false gets loud
Provenance: maintainer, 2026-08-22, live session with the triage seat (「同意所有」, item 16 = A).
State: needs-user-decision → pm:queue (ui seat). Scope: no verdict changes and no interpolation changes — the #5330 row binding does not extend to the node tier (the docblock's "silent interpolation change nobody asked for" stays refused); instead, extend the #5454 reporter posture to this non-throwing path: a node-gate predicate referencing data. that resolves an undefined comparison emits the dev-only unresolvable-predicate report, so the author sees the constant-false instead of shipping it. The card dissolves when #5330's deprecation window closes. Pin: the report fires on the card's reproduction shape; canonical record.* and genuine adapter reads (${data.total}) stay silent.
Clause-②: no. The ruling is explicit that no verdict changes and no interpolation changes — a dev-only diagnostic report is added to an existing non-throwing path. Nothing an author writes changes accept or reject status, and no published surface widens. If your implementation finds itself needing to change what the gate decides, you have left the ruling; stop and report.
File surface granted (write):
packages/react/src/SchemaRenderer.tsx
packages/react/src/** (only the #5454 reporter, wherever it lives)
packages/react/src/__tests__/**
.changeset/*
Report the reporter's path before editing if it falls outside packages/react/**.
Serial constraints: ⛔ packages/auth/** held by #5719 (slot 1); ⛔ packages/types/** and packages/providers/** held by #5716 (slot 2). ⛔ packages/app-shell/src/hooks/useConsoleActionRuntime.tsx remains held by #5702's unlanded branch.
⛔ The card's own cross-reference is a fence, not a footnote.#5687 (this card) is about data.* resolving against the data-source ADAPTER. #5401 → umbrella #5454 is about record.* not being bound at node level. Same evaluator, two different misbound identifiers, different correct fixes. A sweep for one will surface the other if it is written loosely. ⛔ Do not let a data.* site count as evidence for the record.* binding change, or vice versa — and do not fix the other one here.
Premise to re-read, not to trust: the card measured on 3b147a367; base is now 3ece13e33, many merges later. SchemaRenderer.tsx hoists properties.* onto the node so properties.visible arrives as schema.visible, and the node-visibility evaluator is built with data: dataSource (the adapter, defaulting to NO_DATA_SOURCE = {}). Verify both against your worktree; if the hoist or the binding has moved, that is the report, not a detail.
{
"issue": 5687,
"status": "done",
"branch": "claude/issue-5687-node-gate-data-predicate-report",
"pr": "https://github.com/objectstack-ai/objectui/pull/5757",
"premise_still_valid": true,
"summary": "Ruling A implemented: the node tier keeps its documented `data` = adapter semantics, and the deprecated spelling's constant-false got loud. `reportAdapterOnlyDataPredicate()` was added beside the #5454 reporter in packages/react/src/utils/visibilityDiagnostic.ts, sharing its module, dedupe Set, reset, console.warn severity and __DEV__ gate; SchemaRenderer calls it once, on the NON-throwing branch only, and returns the evaluator's verdict exactly as computed, so 'no verdict changes' holds by construction. Trigger is a `data.*` read the bound adapter answers with `undefined` — chosen over three alternatives by measurement, not argument: 'references data.' fires on genuine adapter reads (refused by the ruling) and 'the predicate is constant-false' fires on `data.total > 100` against {total:99} AND on a correct `record.status` gate against a non-matching row, i.e. it would report every correctly-hiding gate in the repo. All three of the card's load-bearing facts re-measured on 3ece13e33: the hoist holds, `data: dataSource` holds, and the third is REFINED — the CEL-envelope dialect already throws and is already reported by #5454 today, so the card's 'this path never throws' is true for the bare-string and template dialects only. The card's repro shape is the bare string, so the gap is real and the premise stands.",
"tests": "Union run on 06561bde3 (the final commit, after rebasing onto the origin/main that moved mid-task). `pnpm exec vitest run packages/react/` => 'Test Files 52 passed (52) / Tests 706 passed (706)'; `pnpm exec vitest list packages/react/` collects 706 cases, 18 of them the new file's (the default reporter names no passing file, so inclusion was read from vitest's own collection rather than assumed — a `--reporter=basic` attempt failed to start and my control caught it as a void reading, not a green one). `pnpm exec vitest run packages/react/ packages/components/ packages/plugin-grid/ packages/types/` => 363 files, 3635 tests, 0 failures. `pnpm --filter @object-ui/react type-check` exit 0. `pnpm --filter @object-ui/react lint` => the tool's own line '347 problems (0 errors, 347 warnings)'; all pre-existing no-explicit-any, none inside either of this diff's two hunks (warning line numbers compared against hunk ranges). Gates, each quoted from its own verdict line, all exit 0 with the exit code captured BEFORE any pipe: check-control-bytes 'OK (scanned 4801 tracked text file(s))', check-changeset-presence 'declares 1 changeset(s)', changeset-fixed, changeset-no-major, lint-coverage '46/46 packages linted, 0 with outstanding errors', type-check-coverage, phantom-deps, self-import, esm-specifiers, spec-symbols, action-forward-parity. check-eager-closure fails with its own 'This is a broken gauge, not a passing budget' (known worktree gauge); it cannot move regardless — the module specifiers imported by SchemaRenderer.tsx are byte-identical before/after (10 vs 10, empty diff), since the change widens an existing named import rather than adding an edge. ABLATION, both legs under `trap ... EXIT INT TERM`, both mutations confirmed on disk by anchored counts in BOTH directions (deleted text at 0, injected marker at 1) before any test output was read. No rebuild was needed and that is stated as a condition, not an omission: the mutated subject is imported by the tests via a relative SOURCE path (`../SchemaRenderer`), not through a package exports/dist edge, so no stale artifact can make a mutation read green. A = delete the reporter call: predicted red exactly the positive cases and green every silence/verdict case; observed 6 red / 32 green with the whole #5454 pin file green. B = delete the `unresolved.length === 0` silence guard: predicted red the silence cases that actually reach the reporter; observed 5 red (2b, 2c, 2d, 3c, 3d), group 1 and production untouched. B EARNED ITS KEEP: case 3c survived it on the first run, and the measured cause was that with `metadata` unbound the predicate THROWS ('metadata is not defined') and lands in the #5454 catch, never reaching this leg — green for a reason unrelated to the boundary it claimed to check. `metadata` is now bound in that case and it dies under B as it should. Controls that correctly SURVIVE both ablations, with reasons: 3a/3b (record.*) — neither mutation touches a path with no `data.` root, which is precisely the evidence that the two adjacent cards do not bleed; 3e (CEL envelope) — it throws first, so it is #5454's case in both worlds; group 4 (production) — __DEV__ short-circuits ahead of both mutations. DECLARED NARROWING: packages/app-shell and apps/console exceed the 10-minute foreground cap as whole suites (measured: exit 124 at 560s), so the run was narrowed to the real blast surface, every file carrying a `data.*` visibility predicate — packages/app-shell/src/views/metadata-admin/ plus apps/console/src/components/FormPage.fieldSpec.test.ts => 195 files, 1979 passed, 1 skipped. The residual risk of a new console.warn is a suite asserting exact console call counts; the vitest config declares no onConsoleLog / failOnConsole (grep exit 1, controlled against a term known to be in that file), so an unexpected warn cannot fail a suite by configuration. CI runs the full farm regardless. INSTRUMENT LIMITS, stated: the `data.*` scan is LEXICAL, not a parse — string literals are stripped first (so `record.status == 'data.status'` reads nothing), but it cannot tell a `data.` reference inside a comment or an unusual quoting form from a real read, and it resolves paths by the same property walk `useDataScope` already uses, so it inherits that resolution's semantics exactly. It also cannot see a `${...}` predicate written inside `properties`, which is pinned as an explicit known limit and filed as #5756 rather than papered over.",
"open_questions": [
{
"question": "The ruling says this path 'emits the dev-only unresolvable-predicate report'. I shared the reporter's module, severity, dedupe Set, lifecycle and dev-only gate verbatim, but gave this leg its OWN first line (ADAPTER_ONLY_DATA_PREDICATE_PREFIX) instead of reusing 'A visibility predicate could not be evaluated'. Was the literal string meant, or the reporter?",
"options": [
"A. Keep the sibling prefix (shipped). On this path the #5454 sentence would be FALSE — the predicate evaluated perfectly, against the wrong object, and produced a constant; telling an author 'could not be evaluated' sends them hunting for a syntax error that is not there.",
"B. Reuse UNRESOLVABLE_VISIBILITY_PREFIX verbatim, so one grep and one console filter catch both legs, at the cost of a first line that misstates what happened."
],
"recommendation": "A, because dispatch guidance was explicit that reporter POSTURE matters more than message text, and posture is shared verbatim — one module, one dedupe Set, one reset, one severity, one gate. This is the single place the flexibility was spent, and it is flagged in the PR body too. If B was meant it is a one-line change plus two test string updates."
}
],
"out_of_scope_findings": [
"filed as #5756: a `${...}` visibility predicate written INSIDE `properties` is interpolated to a boolean before the hoist (measured: evaluate(\"${data.status == 'draft'}\") returns false, while the bare string returns itself verbatim), so it reaches the visibility chain as a plain `false` and NO gate-side diagnostic — this one or #5454's — can ever see it; it hides the block on every row, silently. Pinned in this PR as an explicit known limit. Not fixed here: reaching it means adding a diagnostic inside the interpolation loop, the exact surface this ruling fences off, and it carries two real choices (whether to report a leg that a co-declared `visibleWhen` outranks, and keying on the visibility-key set so genuine `${data.total}` content interpolations stay silent).",
"measured, no issue needed — the triage analysis's stated blind spot is now answered: node-tier `data.*` visibility predicates in non-test source are ZERO across examples/, apps/, content/ and packages/. The only two real `data.*` visibility predicates in the repo (packages/app-shell/src/views/metadata-admin/anchors.ts) sit at the SchemaForm FIELD tier, where evaluatePredicate(visibility, { data: value }) binds `data` to the form draft values — a correct, different binding on a different evaluator that this change cannot reach. Controls for those zeros: the same shape spelled `record.*` = 246 hits, the visibility keys at all = 1273 hits, `data.` anywhere = 744 hits, and the sweep demonstrably enters examples/schema-catalog (5 visibility-key hits there, all record.* / current_user.*). Nobody is standing in the blast radius, which is what #5330's deprecation-window owner needs to know."
]
}
Gates by name at 05:05Z (PR started 04:54:04Z): 19 success + 3 skipped = 22. Zero in_progress, zero failure. (Build Docs 10 s — the path-filtered skip.)
Fence: clean. Four files, all inside packages/react/** and .changeset/. No governed path. Neither sibling's held surface touched.
The seat's answer to the open question: A — keep the distinct first line. Shipped as-is.
Three reasons, in order of weight:
The ruling names the report, not a string."emits the dev-only unresolvable-predicate report" identifies the reporter. What had to be shared was its posture, and it is shared verbatim: one module, one dedupe Set, one reset, one severity, one __DEV__ gate.
The dispatch said so explicitly. My brief: "Matching it exactly matters more than the message text: two adjacent reporters with different lifecycles in one render path is a worse outcome than one slightly awkward message." Lifecycle was the named constraint and it is satisfied. Message text was the named flexibility, and this is the one place it was spent.
B's benefit (one grep catches both legs) survives anyway — both prefixes live in one module.
The trigger was chosen by measurement, and the falsifications are the valuable part
Zone 2b asked for a definition before an implementation, because a trigger that fires on legitimate adapter reads is worse than no report. Three candidates were measured and two are refuted by counterexample:
"references data." — fires on genuine ${data.total} adapter reads. Refused by the ruling itself.
"the predicate is constant-false" — fires on data.total > 100 against {total: 99}, and on a correct record.status gate against a non-matching row. That is not a near-miss: it would report every correctly-hiding gate in the repository.
Shipped: a data.* read the bound adapter answers with undefined.
That second refutation is the one worth carrying — the obvious trigger would have turned a silence bug into a noise bug across the whole repo.
The card's third fact was refined, not just re-verified
The card says the node-gate path "never throws". Measured: the CEL-envelope dialect already throws and is already reported by #5454 today, so that claim holds for the bare-string and template dialects only. The card's repro is the bare string, so the gap is real and the premise stands — but the boundary is now stated correctly rather than inherited.
Ablation B earned its keep by catching a bad survival
Case 3c survived ablation B on the first run. The measured cause: with metadata unbound the predicate throws and lands in #5454's catch, so it never reached the leg it claimed to test — green for a reason unrelated to the boundary it asserted.metadata is now bound and it dies under B as it should. That is the discipline working in the direction people usually skip: a survivor was interrogated rather than credited.
The controls that correctly survive both legs are each given a reason: 3a/3b (record.*) survive because neither mutation touches a path with no data. root — which is itself the evidence that the two adjacent cards do not bleed into each other, the exact separation the card's cross-reference demanded. 3e throws first, so it is #5454's case in both worlds. Group 4 is short-circuited by __DEV__.
The triage blind spot is answered, and the answer matters to #5330
Triage could not see "真实元数据里节点层 data.* 谓词的存量". Measured: node-tier data.* visibility predicates in non-test source are ZERO across examples/, apps/, content/ and packages/. Four controls make that a reading rather than a broken sweep — record.* at 246, visibility keys at 1273, data. anywhere at 744, and the sweep demonstrably entering examples/schema-catalog (5 hits there, all record.* / current_user.*).
The only two real data.* visibility predicates in the repo sit at the SchemaForm field tier (anchors.ts), where evaluatePredicate(visibility, { data: value }) binds data to the form draft — a correct, different binding on a different evaluator this change cannot reach. Nobody is standing in the blast radius, which is precisely what #5330's deprecation-window owner needs in order to close it.
Instrument limits stated, and one of them became a card
The data.* scan is lexical, not a parse: string literals are stripped first (so record.status == 'data.status' reads nothing), but it cannot tell a data. reference in a comment from a real read, and it inherits useDataScope's resolution semantics exactly. And it cannot see a ${…} predicate written inside properties — measured: evaluate("${data.status == 'draft'}") returns false while the bare string returns itself verbatim, so such a predicate reaches the visibility chain as a plain false that no gate-side diagnostic, this one or #5454's, can ever see. Pinned in this PR as an explicit known limit and filed as #5756 rather than papered over — correctly, since reaching it means adding a diagnostic inside the interpolation loop, the exact surface this ruling fences off.
Also correct: the declared narrowing on the app-shell/console suites (measured exit 124 at 560 s) with the run re-aimed at the real blast surface, and the residual risk named and then measured away — the vitest config declares no onConsoleLog / failOnConsole, so a new console.warn cannot fail a suite by configuration. The zero for that grep was controlled against a term known to be in the file.
Flipping ready → auto-merge (SQUASH). Landing verification against origin/main follows.
Activity
os-sales commentedon Aug 22, 2026
Cross-reference — this finding is adjacent to #5401 / #5454, and the two must not be conflated.
This card records that
SchemaRenderer's node visibility gate resolvesdata.*against the data-source adapter. #5401 was re-ruled on 2026-08-22 to option B — bindrecordinto that same node-level evaluator sovisibleWhenmeans whatpackages/spec/src/ui/page.zod.ts:189advertises — and is now a child of umbrella #5454.Same evaluator, two different unbound/misbound identifiers:
data.*record.*Consequence for whoever picks either up: a sweep for one bucket will surface the other if it is written loosely, and the two have different correct fixes. #5401's survey (dispatched now, round 16, survey-only per the ruling's ordering constraint) is instructed to label each site by bucket for exactly this reason. Don't let a
data.*site be counted as evidence for therecord.*binding change, or vice versa.Generated by Claude Code
huangyiirene commentedon Aug 22, 2026
Concentration-round first-touch (triage seat):
finding→ decision inbox (needs-user-decision) — this is an instance the #5330 ruling (2026-08-20, option B) did not explicitly cover: at the node tier,dataalready means the adapter by documented design, so extending the row-binding there collides with a documented interpolation contract. That reach question belongs to the ruling's owner.[facets-block]
properties.visible: "data.status == 'draft'"得到「所有行全部隐藏」且零信号——失败方向伪装成门在工作;但注意这只影响已废弃的data.*拼写,canonrecord.*与 finding: record:alert 是唯一没接 #4075/#4077 行绑定的谓词面 —— 裸 { record } 作用域下 row-action 简写与 data.* 拼法 unbound,fail-soft 使作者门控的横幅永远显示 #4807 修好的行级门不受影响。data= adapter 是写进 docblock 的既有契约(${data.total}插值依赖它);②(把行绑定伸到节点层)正是 docblock 点名的「没人要求的静默插值变更」;①+响亮化让废弃窗口自然关闭。data.*谓词发 dev 报告),零语义变更;②要动两个已文档化契约。一句话问题:#5330 的行绑定裁决是否延伸到节点可见性层(那里
data已按文档指 adapter)——还是维持节点层语义、只把这条废弃拼写的恒假结果变响亮?推荐 A = 维持节点层语义 + 响亮化(#5454 对 throw 路径的同款处理延伸到这条不 throw 的路径:节点门谓词引用
data.且解析出 undefined 比较时发 dev-only 报告),置信度中高;废弃窗口(#5330 的 survey window)照原计划关闭时本卡随之消亡。本分析看不见什么:真实元数据里节点层data.*谓词的存量(#5330 的 survey 半径是否已覆盖节点层写法)。裁决格式:回「A」/「B」(B = 行绑定伸到节点层,含插值变更)。Generated by Claude Code
huangyiirene commentedon Aug 22, 2026
Maintainer ruling recorded — A: the node tier keeps its documented
data= adapter semantics; the deprecated spelling's constant-false gets loudProvenance: maintainer, 2026-08-22, live session with the triage seat (「同意所有」, item 16 = A).
State:
needs-user-decision→pm:queue(ui seat). Scope: no verdict changes and no interpolation changes — the #5330 row binding does not extend to the node tier (the docblock's "silent interpolation change nobody asked for" stays refused); instead, extend the #5454 reporter posture to this non-throwing path: a node-gate predicate referencingdata.that resolves an undefined comparison emits the dev-only unresolvable-predicate report, so the author sees the constant-false instead of shipping it. The card dissolves when #5330's deprecation window closes. Pin: the report fires on the card's reproduction shape; canonicalrecord.*and genuine adapter reads (${data.total}) stay silent.Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
Claim:
domain:uiexecution seat (seat post #5560), round 21, slot 3 of 3.session_01EuPCi56cnGyykygi3z9w4mclaude/issue-5687-node-gate-data-predicate-reportorigin/mainat3ece13e33domain:ui· Tier:claude-opus-5(lane default)File surface granted (write):
Report the reporter's path before editing if it falls outside
packages/react/**.Serial constraints: ⛔
packages/auth/**held by #5719 (slot 1); ⛔packages/types/**andpackages/providers/**held by #5716 (slot 2). ⛔packages/app-shell/src/hooks/useConsoleActionRuntime.tsxremains held by #5702's unlanded branch.⛔ The card's own cross-reference is a fence, not a footnote. #5687 (this card) is about
data.*resolving against the data-source ADAPTER. #5401 → umbrella #5454 is aboutrecord.*not being bound at node level. Same evaluator, two different misbound identifiers, different correct fixes. A sweep for one will surface the other if it is written loosely. ⛔ Do not let adata.*site count as evidence for therecord.*binding change, or vice versa — and do not fix the other one here.Premise to re-read, not to trust: the card measured on
3b147a367; base is now3ece13e33, many merges later.SchemaRenderer.tsxhoistsproperties.*onto the node soproperties.visiblearrives asschema.visible, and the node-visibility evaluator is built withdata: dataSource(the adapter, defaulting toNO_DATA_SOURCE = {}). Verify both against your worktree; if the hoist or the binding has moved, that is the report, not a detail.Generated by Claude Code
fix(react): make a node-gate `data.*` predicate the adapter cannot an…
os-zhuang commentedon Aug 23, 2026
os-dev-report
{ "issue": 5687, "status": "done", "branch": "claude/issue-5687-node-gate-data-predicate-report", "pr": "https://github.com/objectstack-ai/objectui/pull/5757", "premise_still_valid": true, "summary": "Ruling A implemented: the node tier keeps its documented `data` = adapter semantics, and the deprecated spelling's constant-false got loud. `reportAdapterOnlyDataPredicate()` was added beside the #5454 reporter in packages/react/src/utils/visibilityDiagnostic.ts, sharing its module, dedupe Set, reset, console.warn severity and __DEV__ gate; SchemaRenderer calls it once, on the NON-throwing branch only, and returns the evaluator's verdict exactly as computed, so 'no verdict changes' holds by construction. Trigger is a `data.*` read the bound adapter answers with `undefined` — chosen over three alternatives by measurement, not argument: 'references data.' fires on genuine adapter reads (refused by the ruling) and 'the predicate is constant-false' fires on `data.total > 100` against {total:99} AND on a correct `record.status` gate against a non-matching row, i.e. it would report every correctly-hiding gate in the repo. All three of the card's load-bearing facts re-measured on 3ece13e33: the hoist holds, `data: dataSource` holds, and the third is REFINED — the CEL-envelope dialect already throws and is already reported by #5454 today, so the card's 'this path never throws' is true for the bare-string and template dialects only. The card's repro shape is the bare string, so the gap is real and the premise stands.", "tests": "Union run on 06561bde3 (the final commit, after rebasing onto the origin/main that moved mid-task). `pnpm exec vitest run packages/react/` => 'Test Files 52 passed (52) / Tests 706 passed (706)'; `pnpm exec vitest list packages/react/` collects 706 cases, 18 of them the new file's (the default reporter names no passing file, so inclusion was read from vitest's own collection rather than assumed — a `--reporter=basic` attempt failed to start and my control caught it as a void reading, not a green one). `pnpm exec vitest run packages/react/ packages/components/ packages/plugin-grid/ packages/types/` => 363 files, 3635 tests, 0 failures. `pnpm --filter @object-ui/react type-check` exit 0. `pnpm --filter @object-ui/react lint` => the tool's own line '347 problems (0 errors, 347 warnings)'; all pre-existing no-explicit-any, none inside either of this diff's two hunks (warning line numbers compared against hunk ranges). Gates, each quoted from its own verdict line, all exit 0 with the exit code captured BEFORE any pipe: check-control-bytes 'OK (scanned 4801 tracked text file(s))', check-changeset-presence 'declares 1 changeset(s)', changeset-fixed, changeset-no-major, lint-coverage '46/46 packages linted, 0 with outstanding errors', type-check-coverage, phantom-deps, self-import, esm-specifiers, spec-symbols, action-forward-parity. check-eager-closure fails with its own 'This is a broken gauge, not a passing budget' (known worktree gauge); it cannot move regardless — the module specifiers imported by SchemaRenderer.tsx are byte-identical before/after (10 vs 10, empty diff), since the change widens an existing named import rather than adding an edge. ABLATION, both legs under `trap ... EXIT INT TERM`, both mutations confirmed on disk by anchored counts in BOTH directions (deleted text at 0, injected marker at 1) before any test output was read. No rebuild was needed and that is stated as a condition, not an omission: the mutated subject is imported by the tests via a relative SOURCE path (`../SchemaRenderer`), not through a package exports/dist edge, so no stale artifact can make a mutation read green. A = delete the reporter call: predicted red exactly the positive cases and green every silence/verdict case; observed 6 red / 32 green with the whole #5454 pin file green. B = delete the `unresolved.length === 0` silence guard: predicted red the silence cases that actually reach the reporter; observed 5 red (2b, 2c, 2d, 3c, 3d), group 1 and production untouched. B EARNED ITS KEEP: case 3c survived it on the first run, and the measured cause was that with `metadata` unbound the predicate THROWS ('metadata is not defined') and lands in the #5454 catch, never reaching this leg — green for a reason unrelated to the boundary it claimed to check. `metadata` is now bound in that case and it dies under B as it should. Controls that correctly SURVIVE both ablations, with reasons: 3a/3b (record.*) — neither mutation touches a path with no `data.` root, which is precisely the evidence that the two adjacent cards do not bleed; 3e (CEL envelope) — it throws first, so it is #5454's case in both worlds; group 4 (production) — __DEV__ short-circuits ahead of both mutations. DECLARED NARROWING: packages/app-shell and apps/console exceed the 10-minute foreground cap as whole suites (measured: exit 124 at 560s), so the run was narrowed to the real blast surface, every file carrying a `data.*` visibility predicate — packages/app-shell/src/views/metadata-admin/ plus apps/console/src/components/FormPage.fieldSpec.test.ts => 195 files, 1979 passed, 1 skipped. The residual risk of a new console.warn is a suite asserting exact console call counts; the vitest config declares no onConsoleLog / failOnConsole (grep exit 1, controlled against a term known to be in that file), so an unexpected warn cannot fail a suite by configuration. CI runs the full farm regardless. INSTRUMENT LIMITS, stated: the `data.*` scan is LEXICAL, not a parse — string literals are stripped first (so `record.status == 'data.status'` reads nothing), but it cannot tell a `data.` reference inside a comment or an unusual quoting form from a real read, and it resolves paths by the same property walk `useDataScope` already uses, so it inherits that resolution's semantics exactly. It also cannot see a `${...}` predicate written inside `properties`, which is pinned as an explicit known limit and filed as #5756 rather than papered over.", "open_questions": [ { "question": "The ruling says this path 'emits the dev-only unresolvable-predicate report'. I shared the reporter's module, severity, dedupe Set, lifecycle and dev-only gate verbatim, but gave this leg its OWN first line (ADAPTER_ONLY_DATA_PREDICATE_PREFIX) instead of reusing 'A visibility predicate could not be evaluated'. Was the literal string meant, or the reporter?", "options": [ "A. Keep the sibling prefix (shipped). On this path the #5454 sentence would be FALSE — the predicate evaluated perfectly, against the wrong object, and produced a constant; telling an author 'could not be evaluated' sends them hunting for a syntax error that is not there.", "B. Reuse UNRESOLVABLE_VISIBILITY_PREFIX verbatim, so one grep and one console filter catch both legs, at the cost of a first line that misstates what happened." ], "recommendation": "A, because dispatch guidance was explicit that reporter POSTURE matters more than message text, and posture is shared verbatim — one module, one dedupe Set, one reset, one severity, one gate. This is the single place the flexibility was spent, and it is flagged in the PR body too. If B was meant it is a one-line change plus two test string updates." } ], "out_of_scope_findings": [ "filed as #5756: a `${...}` visibility predicate written INSIDE `properties` is interpolated to a boolean before the hoist (measured: evaluate(\"${data.status == 'draft'}\") returns false, while the bare string returns itself verbatim), so it reaches the visibility chain as a plain `false` and NO gate-side diagnostic — this one or #5454's — can ever see it; it hides the block on every row, silently. Pinned in this PR as an explicit known limit. Not fixed here: reaching it means adding a diagnostic inside the interpolation loop, the exact surface this ruling fences off, and it carries two real choices (whether to report a leg that a co-declared `visibleWhen` outranks, and keying on the visibility-key set so genuine `${data.total}` content interpolations stay silent).", "measured, no issue needed — the triage analysis's stated blind spot is now answered: node-tier `data.*` visibility predicates in non-test source are ZERO across examples/, apps/, content/ and packages/. The only two real `data.*` visibility predicates in the repo (packages/app-shell/src/views/metadata-admin/anchors.ts) sit at the SchemaForm FIELD tier, where evaluatePredicate(visibility, { data: value }) binds `data` to the form draft values — a correct, different binding on a different evaluator that this change cannot reach. Controls for those zeros: the same shape spelled `record.*` = 246 hits, the visibility keys at all = 1273 hits, `data.` anywhere = 744 hits, and the sweep demonstrably enters examples/schema-catalog (5 visibility-key hits there, all record.* / current_user.*). Nobody is standing in the blast radius, which is what #5330's deprecation-window owner needs to know." ] }Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
ACCEPT — landing. PR #5757.
Gates by name at 05:05Z (PR started 04:54:04Z): 19
success+ 3skipped= 22. Zeroin_progress, zerofailure. (Build Docs10 s — the path-filtered skip.)Fence: clean. Four files, all inside
packages/react/**and.changeset/. No governed path. Neither sibling's held surface touched.The seat's answer to the open question: A — keep the distinct first line. Shipped as-is.
Three reasons, in order of weight:
Set, one reset, one severity, one__DEV__gate."could not be evaluated"is false. The predicate evaluated perfectly — against the wrong object — and produced a constant. A first line that says otherwise sends the author hunting a syntax error that does not exist. That is the same class of harm as the silence this card exists to remove, and the same lesson components:element:text_input'slabel/placeholder/descriptionare the INVERSE of #5637 — the renderer already resolves the locale map, the declaration still says'string', so the gate warns on a legal write #5717 landed two hours ago: a diagnostic that misdescribes a legal write trains authors to dismiss the reports that are real.B's benefit (one grep catches both legs) survives anyway — both prefixes live in one module.
The trigger was chosen by measurement, and the falsifications are the valuable part
Zone 2b asked for a definition before an implementation, because a trigger that fires on legitimate adapter reads is worse than no report. Three candidates were measured and two are refuted by counterexample:
data." — fires on genuine${data.total}adapter reads. Refused by the ruling itself.data.total > 100against{total: 99}, and on a correctrecord.statusgate against a non-matching row. That is not a near-miss: it would report every correctly-hiding gate in the repository.data.*read the bound adapter answers withundefined.That second refutation is the one worth carrying — the obvious trigger would have turned a silence bug into a noise bug across the whole repo.
The card's third fact was refined, not just re-verified
The card says the node-gate path "never throws". Measured: the CEL-envelope dialect already throws and is already reported by #5454 today, so that claim holds for the bare-string and template dialects only. The card's repro is the bare string, so the gap is real and the premise stands — but the boundary is now stated correctly rather than inherited.
Ablation B earned its keep by catching a bad survival
Case 3c survived ablation B on the first run. The measured cause: with
metadataunbound the predicate throws and lands in #5454's catch, so it never reached the leg it claimed to test — green for a reason unrelated to the boundary it asserted.metadatais now bound and it dies under B as it should. That is the discipline working in the direction people usually skip: a survivor was interrogated rather than credited.The controls that correctly survive both legs are each given a reason: 3a/3b (
record.*) survive because neither mutation touches a path with nodata.root — which is itself the evidence that the two adjacent cards do not bleed into each other, the exact separation the card's cross-reference demanded. 3e throws first, so it is #5454's case in both worlds. Group 4 is short-circuited by__DEV__.The triage blind spot is answered, and the answer matters to #5330
Triage could not see "真实元数据里节点层
data.*谓词的存量". Measured: node-tierdata.*visibility predicates in non-test source are ZERO acrossexamples/,apps/,content/andpackages/. Four controls make that a reading rather than a broken sweep —record.*at 246, visibility keys at 1273,data.anywhere at 744, and the sweep demonstrably enteringexamples/schema-catalog(5 hits there, allrecord.*/current_user.*).The only two real
data.*visibility predicates in the repo sit at the SchemaForm field tier (anchors.ts), whereevaluatePredicate(visibility, { data: value })bindsdatato the form draft — a correct, different binding on a different evaluator this change cannot reach. Nobody is standing in the blast radius, which is precisely what #5330's deprecation-window owner needs in order to close it.Instrument limits stated, and one of them became a card
The
data.*scan is lexical, not a parse: string literals are stripped first (sorecord.status == 'data.status'reads nothing), but it cannot tell adata.reference in a comment from a real read, and it inheritsuseDataScope's resolution semantics exactly. And it cannot see a${…}predicate written insideproperties— measured:evaluate("${data.status == 'draft'}")returnsfalsewhile the bare string returns itself verbatim, so such a predicate reaches the visibility chain as a plainfalsethat no gate-side diagnostic, this one or #5454's, can ever see. Pinned in this PR as an explicit known limit and filed as #5756 rather than papered over — correctly, since reaching it means adding a diagnostic inside the interpolation loop, the exact surface this ruling fences off.Also correct: the declared narrowing on the app-shell/console suites (measured
exit 124at 560 s) with the run re-aimed at the real blast surface, and the residual risk named and then measured away — the vitest config declares noonConsoleLog/failOnConsole, so a newconsole.warncannot fail a suite by configuration. The zero for that grep was controlled against a term known to be in the file.Flipping ready → auto-merge (SQUASH). Landing verification against
origin/mainfollows.Generated by Claude Code
fix(react): report a `${…}` visibility predicate collapsed inside `pr…
fix(react): make a node-gate `data.*` predicate the adapter cannot an…
fix(react): report a `${…}` visibility predicate collapsed inside `pr…
test(react): rewrite the #5454 production-silence pin to the ruled pr…
fix(react): extend the adapter-only `data.*` constant-predicate diagn…
fix(react): a faulting node-gate predicate is loud in production, onc…
fix(react): extend the adapter-only `data.*` constant-predicate diagn…