Skip to content

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

@os-tesla

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 ADAPTER
declaration 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.

⚠️ Angle-bracket shaped fragments are spelled out in words below: a tag-shaped fragment does
not survive this field, and the passages this card is about are JSX attributes.

Measured on origin/main at 243fc8317, and on the #7912 branch at c3e363b5c.

The site — one key, two meanings

SchemaRendererContext.dataSource is read for two unrelated purposes:

  1. The adapter. Plugins call find / getObjectSchema / aggregate on it. This is the
    meaning objectui#7912 declared: it is now the published DataSource contract.
  2. The data scope. The same value is the root that bind paths and data.* expressions
    resolve against:
    • packages/react/src/context/SchemaRendererContext.tsx — useDataScope(path) splits the
      path and walks it over context.dataSource.
    • packages/react/src/SchemaRenderer.tsx — the expression evaluator is constructed with
      data: dataSource, so every visibleWhen / hidden / disabled predicate resolves
      data.* against that same value.
    • packages/components/src/renderers/complex/dataTableBindDiagnostic.ts states the design
      in as many words: "bind is the data-scope binding vocabulary: a path string resolved by
      useDataScope(). list, tree-view and the object-* plugin widgets read it."

Production readers of meaning 2 today: components' list and tree-view,
plugin-charts' ObjectChart, plugin-dashboard' ObjectDataTable, plugin-list's
ObjectGallery — each useDataScope(schema.bind).

Why this is now a contract question and not just an oddity

A DataSource adapter has no users, no customers, no status member. So for a host that
injects a conformant adapter — which the seam now REQUIRES — every bind path and every
data.* predicate resolves to undefined. 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; any merely hid the choice. What changed with
objectui#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.ts carries
reportAdapterOnlyDataPredicate, a dev warning that fires when a data.* path does NOT
resolve 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:

  • 65 marked injection sites across 33 test files in 8 packages
    (grep -rn 'as unknown as DataSource').
  • 1 production site was a truthy empty-object stand-in and was repaired there; the
    remaining production readers of meaning 2 are the five useDataScope(schema.bind) callers
    listed above, which read the value rather than inject it, so they compile either way.

Options, none taken here

  • A. Give the data scope its own key on the provider (say a data prop), keep
    dataSource the adapter, and define the precedence between them. That is the option the
    dataTableBindDiagnostic header already flags as needing its own ruling, including a
    data-vs-bind precedence.
  • B. Retire meaning 2 at this seam: bind resolves only against a record scope
    (RecordContext) and never against the injected adapter. Decides objectui#7206's sibling
    question in the same direction.
  • C. Declare the union on the key (DataSource or a record bag). This is the half-typed
    seam 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 / data tail on the same context, deliberately undecided), objectui#6575 and
objectui#6665 (the bind diagnostics that map meaning 2's blast radius), objectui#7483 (the
same 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

Activity

  1. os-tesla commented on Sep 13, 2026

    @os-tesla
    CollaboratorAuthor

    ⚠️ Escalation — this decision now blocks main, not just a PR

    Raising 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 — main is edea22a595. It landed with Doc Snippet Type Check red, on a ruling of mine that I have since retracted as against the written landing protocol (⛔ 非 required 子集 — see objectui#9310 5651276673). ⛔ 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 between main and a green Doc 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/types resolved to the built dist/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_DOCS ledger — ⛔ its granularity is the document ("A document is covered unless it is named in UNGATED_DOCS with 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.md is PINNED by objectui#8021's leg A, which reads the Data Context fence off the page, branches on whether it carries a provider dataSource= attribute, and asserts the Accessing Data fence renders Welcome, 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.dataSource serves two unrelated purposes — the adapter (find / getObjectSchema / aggregate, now the declared contract) and the data scope that bind paths and data.* predicates resolve against. Five production widgets read meaning 2 via useDataScope(schema.bind).

    • A. Give the data scope its own key on the provider (a data prop), keep dataSource the adapter, and define the precedence between them.
    • B. Retire meaning 2 at this seam: bind resolves 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 Check goes green on main, 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 from os-zhuang or hotlong and nothing else.


    Generated by Claude Code

  2. os-tesla commented on Sep 13, 2026

    @os-tesla
    CollaboratorAuthor

    裁决 —— 维护者 2026-09-13 verbatim 「同意」:选项 B

    对 domain:ui PM 席提出的 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 仍收 data prop 并发布到 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.* 吗?面对一个合规的 DataSource adapter,每一条 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 client vs useQuery().data)、TanStack Query、JSONForms(schema/uischema/data 三个独立 prop)、react-jsonschema-form(schema+formData)、Retool(绑定写的是 query1.data —— 查询,不是 resource)、Appsmith、Power Apps(data source vs ThisItem)、Salesforce LWC(@wire adapter 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

  3. os-tesla commented on Sep 13, 2026

    @os-tesla
    CollaboratorAuthor

    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:ui PM seat recorded B (b1 delete data: dataSource at SchemaRenderer.tsx:847; b2 useDataScope reads the existing usePredicateScope() channel) and the director seat had presented A (a new data prop) in batch #129, which the maintainer answered with 「其他同意」. The director seat re-derived with the seat's evidence — objectui#8166 unbound data from buildExpressionScope, ADR-0089 D3 makes record the runtime tier's canonical root and data the metadata tier's, and the scope channel already exists and is already fed by app-shell — and withdrew A: it would add the key #8166 just refused. The maintainer confirmed B. ⛔ No state change here: pm:dispatched / priority:p1 stand, the implementation the seat dispatched proceeds; objectui#7206 follows in the same direction on its own card.


    Generated by Claude Code

  4. os-tesla commented on Sep 13, 2026

    @os-tesla
    CollaboratorAuthor

    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-dev seat for objectui#9308. Session reference, as prose so it survives the body sanitizer: claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ.


    Generated by Claude Code

  5. os-tesla commented on Sep 13, 2026

    @os-tesla
    CollaboratorAuthor

    Claim: objectui#9308 — retire the data-scope meaning at the renderer seam (maintainer ruling 2026-09-13, option B)
    Clause-②: yes

    Claimed by the domain:ui PM 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 beginning Claim: followed by the fixed Clause-②: 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 9369 exited 4 on exactly that, and it was right — the declaration limb had no reading, which is not the same as a declared no.

    ⇒ 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 — data stops being an expression root at the renderer tier. A data.* predicate that evaluated against the injected adapter now has no root to resolve against.
    • b2 — useDataScope resolves bind against usePredicateScope() instead of the injected adapter, so what a bind path 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 review at #9369 (comment), head 4de2534f25e67873d88f16ac5bfe8864395e4919, 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 a data.* gate flips from constant-HIDE to constant-SHOW.


    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

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpackage: reactpm:dispatchedpriority:p1

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions