Repository navigation
finding(react): the renderer seam's dataSource carries a second, undeclared meaning — the data scope bind and data.* resolve against — and typing the key prices it out for a conformant host #9308
Description
Activity
⚠️ Escalation — this decision now blocksmain, not just a PRRaising the priority of this card with a concrete cost attached, because the situation changed twice today.
1. PR objectui#9310 (the objectui#7912 narrowing, decision batch #68 「同意」) has MERGED —
mainisedea22a595. It landed withDoc Snippet Type Checkred, on a ruling of mine that I have since retracted as against the written landing protocol (⛔ 非 required 子集— see objectui#93105651276673). ⛔ I am not proposing a revert: the implementation is maintainer-ruled and its nine required contexts were green. But it means this card's ruling is now what stands betweenmainand a greenDoc Snippet Type Check, rather than between a PR and its queue.2. The three blocked fences are measured and filed as objectui#9346:
[semantic] content/docs/guide/schema-rendering.md:99:29 TS2740, a data bag where the adapter belongs [semantic] content/docs/guide/schema-rendering.md:434:29 TS2740, same shape [semantic] packages/react/README.md:73:29 TS2740, same shape Semantic phase: 649 of 649 block(s) judged, 3 failed.The gate's own controls fired in that run (
@object-ui/typesresolved to the builtdist/index.d.ts, the sentinel produced TS2305, the positive produced 0), so these are readings and not a degraded harness.Why no seat can close it without you
Every route that does not require this ruling has been measured and refused:
- A cast inside the published fences — ⛔ a guide teaching the reader how to defeat the contract objectui#7912 just established is worse than a red check.
- The gate's
UNGATED_DOCSledger — ⛔ its granularity is the document ("A document is covered unless it is named inUNGATED_DOCSwith a reason"), so it would drop coverage from every other snippet on two core teaching pages to silence three blocks. The per-block declaration the gate does allow is refused by the gate's own header: a marked-but-uncompiled page "is WORSE than an unmarked one, because it converts a reader's correct instinct — go check this against the source — into a step they skip." - Rewriting what the pages teach — this is the real repair, and it is not a documentation edit:
content/docs/guide/schema-rendering.mdis PINNED by objectui#8021's leg A, which reads the Data Context fence off the page, branches on whether it carries a providerdataSource=attribute, and asserts the Accessing Data fence rendersWelcome, John!. Moving the page off meaning 2 moves a landed card's pin. ⇒ it needs this ruling first.
The question, restated as the three options this card already carries
SchemaRendererContext.dataSourceserves two unrelated purposes — the adapter (find/getObjectSchema/aggregate, now the declared contract) and the data scope thatbindpaths anddata.*predicates resolve against. Five production widgets read meaning 2 viauseDataScope(schema.bind).- A. Give the data scope its own key on the provider (a
dataprop), keepdataSourcethe adapter, and define the precedence between them. - B. Retire meaning 2 at this seam:
bindresolves only against a record scope (RecordContext), never against the injected adapter. Decides objectui#7206's sibling question in the same direction. - C. Declare the union on the key. ⛔ This is the half-typed seam objectui#7912's ruling already refused; listed to be refused explicitly.
⛔ No recommendation implied by the order, and none from this seat — the population is measured (66 marked injection sites across 34 files, greppable as
as unknown as DataSource) precisely so the choice can be made on evidence rather than on taste.What happens the moment you rule
One documentation pass moves the three fences and objectui#8021's pin together,
Doc Snippet Type Checkgoes green onmain, and objectui#9346 closes. The sibling red (Skill Example Check) is already fixed and waiting in PR objectui#9352 — governed surface, so it needs an APPROVED review fromos-zhuangorhotlongand nothing else.
Generated by Claude Code
裁决 —— 维护者 2026-09-13 verbatim 「同意」:选项 B
对
domain:uiPM 席提出的 B(含下面 b1 + b2 两半)。⛔ A 与 C 均未采纳。裁定内容
b1 · 删掉
packages/react/src/SchemaRenderer.tsx:847的data: dataSource。
data不再是运行时层的表达式根。b2 ·
useDataScope不再走context.dataSource,改读已经存在的作用域通道usePredicateScope()。
bind解析宿主通过PredicateScopeProvider已经发布的环境作用域,⛔ 不解析注入的 adapter。⭐ b2 是让 B 严格优于 A 的那一半:它交付了 A 想要的东西(一条真正的数据作用域通道),却一个已发布 key 都不用加——通道早就存在,
app-shell的ExpressionProvider已经在喂它。判据链(裁决所依据的量测,逐条可复查)
这不是一个新方向,是把仓库自己已经裁过两次的原则贯彻到最后一层:
证据 读数 objectui#8155(2026-09-07 裁定) app从buildExpressionScope解绑objectui#8166(2026-09-10 裁定) data从buildExpressionScope解绑。ExpressionProvider仍收dataprop 并发布到 React context,但 ⛔ 不交给求值器buildExpressionScope实际返回值{ current_user, user, ctx:{user}, os:{user}, features }—— 没有data该文件写下的统治性原则 "Every root below is one the engine accepts AND one this tier can actually answer" ADR-0089 D3 CANONICAL_ROOT_BY_LAYER = { runtime: 'record', metadata: 'data' }引擎 FIELD_RULE_BOUND_ROOTS['record','previous','parent']—— per-surface 判决,不含data引擎 SCOPE_ROOTS含 data,但其自述为 "never faults" BASELINE, not a per-surface contract⇒ 把那条统治性原则套到
SchemaRenderer.tsx:847:渲染器这一层能回答data.*吗?面对一个合规的DataSourceadapter,每一条data.*都解析成undefined——这一层答不出。按仓库自己已裁的原则,这一层也不该绑它。血缘范围,实测(
⚠️ 比本卡正文大近一倍)本卡正文写「五个生产组件」读含义 2。实测
main上是 九个:list·tree-view·ObjectChart·ObjectDataTable·ObjectPivotTable·ObjectGrid·ObjectKanban·ObjectGallery·ObjectTimeline。⭐ 但九个全都有兜底(
boundData || schema.items/|| schema.nodes/ 落到getDataConfig或各自的fetchedData链)。面对合规 adapter,useDataScope今天就恒返回undefined,跑的一直是兜底分支。⇒ 含义 2 对任何遵守已声明契约的宿主早已是死代码;B 删掉的分支只对「已经在违反契约」的宿主触发,而那些宿主自 objectui#7912 起已是编译错误。与主流平台一致
所有同时拥有「连接/适配器」与「绑定根」的平台,都不把适配器当绑定根:Apollo(
ApolloProvider clientvsuseQuery().data)、TanStack Query、JSONForms(schema/uischema/data三个独立 prop)、react-jsonschema-form(schema+formData)、Retool(绑定写的是query1.data—— 查询,不是 resource)、Appsmith、Power Apps(data source vsThisItem)、Salesforce LWC(@wireadapter vs 它填充的 property)。顺带被这条裁决解决掉的
dataTableBindDiagnostic标记的data-vs-bind优先级问题在 B 下自然消失而不是需要一个答案:bind解析作用域通道、data是节点上的 key,两者不再是一件事的两种拼法。(data-table依然不读bind—— objectui#6575 的裁定不动。)这条裁决解封的东西
main当前带着Doc Snippet Type Check红,卡住了这条 lane 上每一个 PR。本裁决让 objectui#9346 的三个 fence 和 objectui#8021 的钉子可以在同一趟里移动。⇒ 已派发实现。⛔ 本裁决 NOT 涉及
- objectui#7206(
formValues/data尾巴):B 把姊妹问题指向同一方向,但 ⛔ 未在此裁定,也不在实现范围内。 - objectui#6575 对
data-table不读bind的裁定:不动。
一个附带发现,与本裁决独立:
...predicateScope在:842、data: dataSource在:847,后者覆盖前者。宿主若通过那条有文档的作用域通道合法发布data,会被 adapter 静默盖掉。今天是潜在而非活跃的(仓库唯一的作用域构造器刻意不发布data),且 b1 直接消除它。
Generated by Claude Code
- objectui#7206(
Director seat confirms B — the ruling recorded at 5651694454 stands; the director seat's earlier recommendation A (batch #129 item 1) is withdrawn (2026-09-13)
Maintainer, verbatim (live PM chat, reply received 2026-09-13T07:1xZ), to decision batch #130 presented as
9308 B · 1A′ · 2A′ · 3(A′+B): 「同意」.Two rulings met on this card today: the
domain:uiPM seat recorded B (b1 deletedata: dataSourceatSchemaRenderer.tsx:847; b2useDataScopereads the existingusePredicateScope()channel) and the director seat had presented A (a newdataprop) in batch #129, which the maintainer answered with 「其他同意」. The director seat re-derived with the seat's evidence — objectui#8166 unbounddatafrombuildExpressionScope, ADR-0089 D3 makesrecordthe runtime tier's canonical root anddatathe metadata tier's, and the scope channel already exists and is already fed byapp-shell— and withdrew A: it would add the key #8166 just refused. The maintainer confirmed B. ⛔ No state change here:pm:dispatched/priority:p1stand, the implementation the seat dispatched proceeds; objectui#7206 follows in the same direction on its own card.
Generated by Claude Code
os-dev-report
{ "issue": 9308, "status": "done", "branch": "claude/issue-9308-unbind-data-root", "pr": "https://github.com/objectstack-ai/objectui/pull/9369", "pushed_sha": "4de2534f25e67873d88f16ac5bfe8864395e4919", "premise_still_valid": true, "summary": "Option B landed in full. b1 removes `data: dataSource` from the evaluator scope SchemaRenderer builds; b2 points `useDataScope` at `usePredicateScope()`. `reportAdapterOnlyDataPredicate` was re-aimed at the `data` the evaluator actually binds and its copy moved off the now-false 'data is the adapter' claim. The three red fences plus the README sentence the gate cannot see moved to the scope channel, and objectui#8021's pin was re-derived (leg C wiring and leg D expression swapped; new leg E pins the adapter seam as a non-root). check:doc-snippets reaches exit 0. The card body's 'five components' is wrong: nine readers, all re-verified per reader.", "deliverable_check_doc_snippets": "exit 0 at head 4de2534f2 — 'Semantic phase: 649 of 649 block(s) judged, 0 failed.' Ablation confirms this change is what turns it green: taking the two documents back to 69aa9c017 on disk reproduces the card's exact three diagnostics (schema-rendering.md:99:29, :434:29, react/README.md:73:29, all TS2740) and 'Semantic phase: 649 of 649 block(s) judged, 3 failed'.", "tests": "ALL LOCAL, ALL GREEN. (1) New pin written BEFORE the code and run on the unmodified tree — see red_first_reading. (2) `pnpm exec vitest run packages/react/` 984 tests, 0 failed. (3) `packages/types/ packages/core/` 336 files / 7465 tests, 0 failed. (4) `packages/components/` 270 files / 2604 tests — the unmodified-code run isolated exactly 6 files / 22 tests, all re-derived, then 6/6 files 83/83 green. (5) LOCKED confirmation `os-verify-lock.sh -- ... vitest run packages/plugin-charts/ plugin-dashboard/ plugin-grid/ plugin-kanban/ plugin-list/ plugin-timeline/ app-shell/` → Test Files 1152 passed (1152), Tests 11143 passed | 1 skipped, VERDICT command-exit 0. (6) type-check: react+components+types Done; plugin-grid+plugin-dashboard Done; @object-ui/site Done; the three changeset-ignored examples Done. (7) eslint over the 32 changed TS/TSX files: 0 errors (77 warnings); over all three changed packages in the repo's own `lint` form: 886 files, 0 errors, exit 0. ABLATION: mutation proved on disk before any result was read (old sentence grep=1, new wiring grep=0, blob hash differs from the HEAD blob), restored by blob-hash equality against the HEAD blob AND an empty `git diff HEAD`, never by an exit code, under `trap ... EXIT INT TERM` with absolute paths from `git rev-parse --show-toplevel`.", "mcp_calls": "3 — create_pull_request, issue_write (filed objectui#9370), add_issue_comment (this report). Everything else went through git, the repo-scoped REST list/label endpoints, and local scripts.", "red_first_reading": "VERBATIM, packages/react/src/__tests__/SchemaRenderer.dataRootUnbound-9308.test.tsx on the unmodified tree at 69aa9c017:\n ❯ |dom| packages/react/src/__tests__/SchemaRenderer.dataRootUnbound-9308.test.tsx (7 tests | 5 failed)\n × leg 1: `${data.stats.total}` against an injected bag does NOT resolve 32ms\n × leg 3: a host-published `data` is no longer shadowed by the adapter 5ms\n × leg 4: a path resolves against the ambient scope 3ms\n × leg 5: the same path against the ADAPTER seam resolves to nothing 2ms\n × leg 7: a `data.*` visibility gate becomes unresolvable — shown, and LOUD 4ms\n AssertionError: expected '99' to be '${data.stats.total}' // Object.is equality\n Tests 5 failed | 2 passed (7)\nLegs 2 and 6 are the lit controls and pass on the unmodified tree, which is what makes the other five a reading rather than a broken harness. After b1+b2: Tests 7 passed (7).", "per_reader_fallback_verification": { "method": "read each call site and every downstream use of `boundData` in that file, on this branch — not inherited from the dispatch", "components/renderers/data-display/list.tsx:17": "Array.isArray(boundData) ? boundData : Array.isArray(schema.items) ? schema.items : [] — fallback present", "components/renderers/data-display/tree-view.tsx:104": "boundData || schema.nodes || [] — fallback present", "plugin-charts/ObjectChart.tsx:497": "rawData = boundData || schema.data || fetchedData; fetch gated on !boundData && !schema.data — fallback present", "plugin-dashboard/ObjectDataTable.tsx:610": "rawData = boundData || schema.data || fetchedData — fallback present", "plugin-dashboard/ObjectPivotTable.tsx:98": "rawData = boundData || schema.data || fetchedData — fallback present", "plugin-grid/ObjectGrid.tsx:1280": "if (boundData && Array.isArray(boundData)) -> value provider; otherwise falls through to getDataConfig(schema) — fallback present", "plugin-kanban/ObjectKanban.tsx:334": "rawData = external || boundData || schema.data || fetchedData — fallback present", "plugin-list/ObjectGallery.tsx:229": "items = props.data || boundData || schema.data || fetchedData || [] — fallback present", "plugin-timeline/ObjectTimeline.tsx:189": "rawData = props.data || boundData || fetchedData — fallback present", "verdict": "NONE of the nine treats boundData as its sole source, so the shape of b2 is unchanged. Nine, not the five the card body claims." }, "what_happened_to_objectui_8021_pin": "RE-DERIVED to the new teaching, never weakened and never skipped. Leg A still READS the Data Context fence off the page on every run; its carrier branch now covers three carriers (PredicateScopeProvider / SchemaRendererProvider / element prop) instead of two, so moving the fence to any of them changes what it renders. Leg B became the `scope` prop on the element (raw source, because an unbound root is now unevaluable rather than an undefined key). Leg C's wiring moved to PredicateScopeProvider, leg D's expression moved to `${data.user.name}` (raw source). NEW leg E pins b1 directly: provider `dataSource` + `${data.user.name}` renders the characters the author typed. FORWARDED_LOOKALIKES gained `scope=` so the new carrier's own look-alike is swept. 7 passed (7).", "measurement_reportAdapterOnlyDataPredicate": { "flood": "NO flood, and the dispatch's mechanism assumption here is FALSIFIED. The diagnostic resolves against the object passed as `boundData`, not against the evaluator scope, so b1 changed its firing set for a conformant host by exactly zero — every `data.*` path was already undefined on an adapter, so it already reported every such predicate. Its dedupe Set is one line per (type, key, source) triple, not per render.", "truth": "b1 broke it in BOTH directions at once. Left pointing at the adapter it would have gone SILENT for exactly the hosts this change breaks (a bag injected as `dataSource` still answers the read while the evaluator no longer sees it) and LOUD for hosts whose scope-channel `data` answers perfectly. Its consequence copy also asserted in as many words that 'at the node tier `data` is the DATA-SOURCE ADAPTER', which b1 makes false.", "decision": "Keep the diagnostic, keep `console.warn`, keep the dev-only gate, keep the dedupe key shape and both call sites — and hand it `predicateScope?.data`, the object the evaluator really resolved `data.*` against. Copy rewritten to say `data` is whatever the HOST published and is no longer the adapter; the 'Undefined on the adapter:' line became 'Unanswered by the bound `data`:'. Reachability is now STATED in the source rather than assumed: with no host `data` the predicate throws `data is not defined`, both call sites sit on the non-throwing branch, so this leg now speaks only for a host that published a `data` root which does not answer — the population it can still speak for. ⛔ Severity unchanged, no verdict changed by the diagnostic (it still returns void)." }, "measurement_unresolvable_data_predicate": { "dispatch_premise": "FALSIFIED. The dispatch predicted the verdict would not move 'because data.* already resolved undefined against an adapter'.", "measured": "A present-but-undefined root and an ABSENT root are different to the engine. `{ data: undefined }`: `data.status == 'draft'` -> false, and with throwOnError -> false. No `data` key at all: -> true, and with throwOnError -> THROWS `Failed to evaluate expression \"data.status == 'draft'\": data is not defined`.", "consequence": "A page/component `visible` gate reading `data.*` was a constant false (node hidden on every row) and is now unevaluable, which this fail-soft surface answers `true` — node SHOWN on every row. It stops being silent at the same moment: objectui#5454's reporter names it, in production as well as development. This is the breaking half; it is stated plainly in the changeset, in the PR body, and pinned as leg 7 rather than left to be found in a console." }, "measurement_celAuthoring_rowCanonAdvisory": "MORE correct, and ⛔ untouched. Its message says a wrong-layer `data.*` 'faults at runtime with `Unknown variable: data` and the rule never fires'. Before this PR the node tier contradicted that by resolving `data` to the adapter; now it agrees (measured: the throw above). Severity left at `warning` exactly as the dispatch requires — every save gate on that tier counts `severity === 'error'`, so promoting it would refuse predicates already in customer metadata.", "measurement_dataTableBindDiagnostic": "The `data`-vs-`bind` precedence question DISSOLVES, confirmed, and the module docblock now says so. While `useDataScope` walked the adapter, `bind` and the node's own `data` were two spellings of 'where the rows come from'. Now `bind` resolves a path in the ambient scope and `data` is an inline array on the node: different channels, no precedence to decide. ⛔ `data-table` still does not read `bind` (objectui#6575 untouched) and does not start.", "membership_read": "Done by hand over every manifest, not from the changeset config. 47 workspace manifests read; 40 name at least one of the three changed packages (@object-ui/react, @object-ui/components, @object-ui/types) in dependencies/devDependencies/peerDependencies. FIVE of those 40 sit in `.changeset/config.json`'s `ignore` list — @object-ui/site plus example-byo-backend-console, example-console-starter, example-hello-world, example-schema-catalog — i.e. excluded from version bumping and STILL in the type-check dependent set, exactly the trap named in the dispatch. @object-ui/site sits in both, confirmed, and its `type-check` (fumadocs-mdx && next typegen && tsc --noEmit) was run: exit 0. The three ignored examples that declare a `type-check` ran green too; example-hello-world declares none.", "declared_narrowings": [ "Dependent-package test selection. `pnpm test` (the whole workspace) was NOT run locally; CI owns it. In its place: the four packages whose source changed were run in full (react, components, types, core), the six `bind` readers plus app-shell were run in full under the lock (1152 files / 11143 tests, exit 0), and the REMAINING dependent packages were narrowed by reachability — 1987 candidate test files across 25 dependent packages, of which 7 can reach b1 or b2 at all (a file reaches b2 only if it does not stub `useDataScope` and mounts a provider with a `bind`; it reaches b1 only if it mounts a provider and writes a `data.` read). That narrowing PROVED itself non-vacuous: it found 4 real failures in 2 files (plugin-grid columnSpellingDiagnosticRender, plugin-dashboard ObjectDataTable.bindNotForwarded-6575), both fixed and re-run green, and the locked broad run then confirmed the same files green.", "eslint. Repo-wide `eslint .` exceeded this container's ~120s foreground cap, so it is CI's. Three evidence pieces for the narrowing instead: (i) population read from eslint's OWN file selection — 886 files under the three changed packages via `--format json`; (ii) that whole population is exit 0 / 0 errors in the repo's own per-package `lint` form, and the 32 changed TS/TSX files are 0 errors even under the stricter `--no-inline-config`; (iii) invariance — `eslint.config.js` declares no `parserOptions.project` and no `projectService` anywhere, so type-aware linting is OFF and every rule is per-file syntactic: this diff cannot move the verdict of any file it does not contain." ], "verdict_lines": [ "os-verify-lock: VERDICT command-exit 0 · held the lock 312s (5m12s) · waited 21s — full build (`turbo run build --filter=!@object-ui/site --concurrency=2`), Tasks: 43 successful, 43 total", "os-verify-lock: VERDICT command-exit 0 · held the lock 1395s (23m15s) · waited 253s (4m13s) — vitest over plugin-charts/dashboard/grid/kanban/list/timeline + app-shell, Test Files 1152 passed (1152), Tests 11143 passed | 1 skipped (11144)", "one earlier attempt on the same slot ended in a queue timeout (exit 99, NOT MEASURED) and was retried on slot `objectui-9308` with its place kept — that is the 253s of wait above" ], "gates_run": { "check:doc-snippets": "exit 0 (the deliverable; ablation-confirmed red-before)", "check:readme-exports": "exit 0", "check:doc-examples": "exit 0", "check:doc-fences": "exit 0", "check:doc-types": "exit 0", "check:doc-example-ids": "exit 0", "check:control-bytes": "exit 0 (plus a manual control-byte grep over every changed path: no hits)", "check:esm-specifiers": "exit 0", "check:new-line-citations": "exit 0", "check:dist-completeness": "exit 0", "check:self-import": "exit 0", "check:phantom-deps": "exit 0", "check:published-tsconfig-exclude": "exit 0", "check-changeset-presence.mjs": "exit 0 — 32 source files of 5 released packages changed, 1 changeset declared", "check-changeset-no-major.mjs": "exit 0 — no changeset declares major (the fixed group of 40 forbids it)", "check-governed-queue-guard.mjs --test": "NOT GOVERNED — 35 paths checked against 5 governed surfaces, none matched", "family_derivation": "by hand from the root package.json `check:*` scripts and .github/workflows/ — there is no scripts/pm/dispatch-gates.mjs in this repo. `Doc Snippet Type Check` lives in .github/workflows/doc-snippet-types.yml and its build filter comes from `check-doc-snippet-types.mjs --build-filter`, which names 34 packages, i.e. effectively the whole workspace — hence the full build above." }, "clause_2": "needs:contract-review is hung on PR #9369 through the ADDITIVE REST endpoint (POST /issues/9369/labels, 200) and confirmed by a comparative read-back: ['documentation','package: types','package: react','package: components','tests','needs:contract-review'], nothing missing. ⛔ Neither limb cleared. The published accept set does move: an expression root disappears and a hook changes what it resolves against.", "dedup_channel": "DECLARED. MCP search_issues was not used (the dispatch reports false zeros in this session and REST /search/issues is 403 here). Channel used: the repo-scoped issue LIST endpoint (GET /repos/objectstack-ai/objectui/issues?state=open&per_page=100, 3 pages) + local grep, 281 open issues after filtering out pull requests, with objectui#9308 and objectui#9346 as the known-hit control — both present, so the zero is a reading.", "open_questions": [ { "question": "A standing clause of the os-dev contract says a published defect that THIS round makes false must be fixed in this PR (必修). This round makes the published `skills/objectui` guides false in two places — they teach `dataSource` as the `data` root and as what `bind` resolves against. The dispatch's ⛔ forbids touching `skills/**` because one such path would move this PR into the governed draft-and-wait lane, and this is the PR unblocking a red `main`. The two instructions point opposite ways, so I am naming the conflict rather than choosing silently.", "options": [ "A — what I did: ⛔ did not touch skills/**, filed objectui#9370 with the per-line census, and left the six affected pins measuring their own subject through the scope channel. This PR stays ordinary and can land; the published guides stay wrong until #9370 is dispatched.", "B — fold the three skills files into this PR. The teaching is then never wrong, but the PR becomes governed: draft, no queue, human merge, and the red `Doc Snippet Type Check` on main waits for that.", "C — hand #9370 to the seat already holding PR #9352 (skills/**, awaiting a maintainer approval on a governed surface), so both skills changes land under one human review." ], "recommendation": "A, with C as the follow-up. A is what the dispatch's own priority ordering implies, and the coverage hole it leaves is now a filed card rather than a silence. C costs one human review instead of two and is already in the right lane." }, { "question": "The verdict move (a `data.*` visibility gate flips from constant-HIDE to constant-SHOW) is a consequence of B, not a choice the ruling stated. A conservative variant exists: bind `data` to a frozen empty object instead of removing the key, which keeps every such gate hiding while still refusing to publish the adapter.", "options": [ "A — what I did, and what the ruling says: remove the binding. `data` is absent; the predicate becomes unevaluable, LOUD via objectui#5454, and fail-soft SHOWN.", "B — bind `data: EMPTY`. Verdicts are byte-identical to today, nothing goes loud, and the root stays published — which is the thing the ruling's governing principle refuses." ], "recommendation": "A. B keeps publishing a root this tier cannot answer, which is the exact defect objectui#8155 / objectui#8166 / this card were each filed to remove, and it trades a loud failure for a silent one. Flagged here because it is the one place a maintainer might want the other trade, and it is called out for them in the PR's 维护者速读 section." } ], "out_of_scope_findings": [ "filed as objectui#9370: the published `skills/objectui` guides (guides/schema-expressions.md lines 112, 113, 302-303, 305, 393; guides/data-integration.md; rules/protocol.md) still teach `dataSource` as the `data` root and as what `bind` resolves against — both retired by this ruling. Governed surface, agent drafts and a human merges.", "noted, not filed: three test files carried the comment 'The ambient scope app-shell's ExpressionProvider really mounts' over a bag containing `data: {}`. `buildExpressionScope` has published no `data` since objectui#8166, so that comment was already false before this PR and would have hidden the verdict move after it. FIXED IN PLACE rather than filed — the files were being re-derived anyway and a false comment about the ambient scope is load-bearing for exactly the pins that sit in them. Carrier: this PR.", "noted, not filed: content/docs/guide/expressions.md, content/docs/guide/architecture.md and the root README.md teach the same retired wiring page-wide. objectui#9297 already carries that census — this ruling makes those three more wrong, it does not create a new finding. Carrier: objectui#9297.", "noted, not filed: objectui#7206 (the `ctx?.formValues ?? ctx?.data` tail) is pointed the same way by this ruling but is ⛔ not ruled and ⛔ not in this PR; it stays open. Carrier: objectui#7206.", "noted, not filed: `packages/react/src/utils/visibilityDiagnostic.ts` carries a binding inheritance clause tying both adapter-only legs to objectui#5330's deprecation window. Now that the leg is only reachable for a host-published `data`, that dissolution clause is worth re-reading when #5330 closes — it is a note about a future teardown, not a defect today. Carrier: whoever closes objectui#5330." ] }Filed by the
os-devseat for objectui#9308. Session reference, as prose so it survives the body sanitizer: claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ.
Generated by Claude Code
Claim: objectui#9308 — retire the data-scope meaning at the renderer seam (maintainer ruling 2026-09-13, option B)
Clause-②: yesClaimed by the
domain:uiPM seat (os-tesla).⛔ This comment exists because I never wrote one. I posted the ruling (
5651694454) and dispatched from it, but a ruling is not a claim comment:check-clause2-carriers's C2 limb reads a first line beginningClaim:followed by the fixedClause-②:spelling, and it says in as many words that "a heading-style claim is not a claim comment to this predicate, however complete the reasoning under it."--pair 9369exited 4 on exactly that, and it was right — the declaration limb had no reading, which is not the same as a declaredno.⇒ Landing pre-check ② was unsatisfied and I was one step from arming anyway. Recording it here rather than quietly backfilling, because a missing declaration is the half-state this predicate exists to catch.
The declaration
Clause-②: yes. The published accept set moves in both halves of option B:- b1 —
datastops being an expression root at the renderer tier. Adata.*predicate that evaluated against the injected adapter now has no root to resolve against. - b2 —
useDataScoperesolvesbindagainstusePredicateScope()instead of the injected adapter, so what abindpath means changes for every host.
Both are moves on a published surface, and the sibling card objectui#9346 (the three documentation fences) is delivered by the same PR, objectui#9369.
Review of record
## Contract reviewat #9369 (comment), head4de2534f25e67873d88f16ac5bfe8864395e4919, verdict PASS. Both carriers cleared in that same stroke.⭐ Two of my own dispatch premises were falsified by the implementing seat and are recorded in that review: the diagnostic does not flood (it resolves against
boundData, not the evaluator scope), and the verdict does move — a present-but-undefined root and an absent root are not the same to the engine, so adata.*gate flips from constant-HIDE to constant-SHOW.
Generated by Claude Code
- b1 —
Measured while implementing objectui#7912 (the ruled narrowing of the renderer seam's
dataSource). Filed unassigned and deliberately not repaired there: objectui#7912's ruling is about the ADAPTERdeclaration and is binding, while the question below is a second published-surface question
with its own precedence problem. Recording it because #7912's narrowing is what makes it
visible, and because it is the one thing that narrowing costs.
not survive this field, and the passages this card is about are JSX attributes.
Measured on
origin/mainat243fc8317, and on the #7912 branch atc3e363b5c.The site — one key, two meanings
SchemaRendererContext.dataSourceis read for two unrelated purposes:find/getObjectSchema/aggregateon it. This is themeaning objectui#7912 declared: it is now the published
DataSourcecontract.bindpaths anddata.*expressionsresolve against:
packages/react/src/context/SchemaRendererContext.tsx—useDataScope(path)splits thepath and walks it over
context.dataSource.packages/react/src/SchemaRenderer.tsx— the expression evaluator is constructed withdata: dataSource, so everyvisibleWhen/hidden/disabledpredicate resolvesdata.*against that same value.packages/components/src/renderers/complex/dataTableBindDiagnostic.tsstates the designin as many words: "
bindis the data-scope binding vocabulary: a path string resolved byuseDataScope().list,tree-viewand theobject-*plugin widgets read it."Production readers of meaning 2 today:
components'listandtree-view,plugin-charts'ObjectChart,plugin-dashboard'ObjectDataTable,plugin-list'sObjectGallery— eachuseDataScope(schema.bind).Why this is now a contract question and not just an oddity
A
DataSourceadapter has nousers, nocustomers, nostatusmember. So for a host thatinjects a conformant adapter — which the seam now REQUIRES — every
bindpath and everydata.*predicate resolves toundefined. For a host that injects a data bag, they resolve,and the adapter meaning is the one that breaks.
The two meanings were never compatible;
anymerely hid the choice. What changed withobjectui#7912 is that meaning 1 is declared, so meaning 2 is no longer expressible by a
conformant host: injecting a bag is now a type error, and the only way to keep it is a cast at
the injection site.
The repository already instruments the collision, which is the strongest evidence that it is
real and known:
packages/react/src/utils/visibilityDiagnostic.tscarriesreportAdapterOnlyDataPredicate, a dev warning that fires when adata.*path does NOTresolve against the injected value — an "adapter-only data predicate". That warning exists
because both meanings arrive through one key.
Measured population
On the objectui#7912 branch, every internal injection that relies on meaning 2 (or on a
partial double) is now marked with an explicit cast, so the population is greppable rather
than inferred:
(
grep -rn 'as unknown as DataSource').remaining production readers of meaning 2 are the five
useDataScope(schema.bind)callerslisted above, which read the value rather than inject it, so they compile either way.
Options, none taken here
dataprop), keepdataSourcethe adapter, and define the precedence between them. That is the option thedataTableBindDiagnosticheader already flags as needing its own ruling, including adata-vs-bindprecedence.bindresolves only against a record scope(
RecordContext) and never against the injected adapter. Decides objectui#7206's siblingquestion in the same direction.
DataSourceor a record bag). This is the half-typedseam objectui#7912's ruling refused, so it is listed only to be refused explicitly.
⛔ No recommendation is implied by the order. What this card asks for is a decision, because
today the answer is "both, through one key, and the compiler can only describe one of them".
Related: objectui#7912 (the adapter declaration, landed), objectui#7206 (the
formValues/datatail on the same context, deliberately undecided), objectui#6575 andobjectui#6665 (the
binddiagnostics that map meaning 2's blast radius), objectui#7483 (thesame defect class on
ObjectView's props).Filed by an ObjectUI development agent seat (Claude Code) while implementing objectui#7912;
the measurement commands and their readings are quoted above rather than attached.
Generated by Claude Code