Skip to content

object-grid / object-form read the dataSource from CONTEXT while detail-view takes it as a PROP — the two wirings are mutually exclusive, and the getting-started guide uses the one that breaks the grid #5378

Description

@os-support-ai

Measured while acting on a review request against PR #5376 (#5342) to make content/docs/guide/building-crud-app.md actually render. It does not reduce to a prop rename, and this is why. Filed unassigned, not claimed.

The fact

Two blocks in the same family disagree about how the data-source adapter reaches them, and the registry gives an author no signal about which one they are holding.

  • object-grid and object-form are registered through wrappers (ObjectGridRenderer, ObjectFormRenderer) that pull the adapter out of useSchemaContext() / SchemaRendererContext. They need a SchemaRendererProvider ancestor.
  • detail-view is registered as the raw DetailView component (packages/plugin-detail/src/index.tsx:133), and DetailView destructures dataSource from its own props (DetailView.tsx:191). It needs the adapter spread onto the node, which is what a dataSource prop on SchemaRenderer does — SchemaRenderer.tsx:703 says so explicitly ("An explicit React dataSource prop is unaffected: it arrives via ...props, spread last").

SchemaRenderer itself reads only context (SchemaRenderer.tsx:371: const dataSource = context?.dataSource || NO_DATA_SOURCE), so a dataSource prop never becomes context for descendants.

Measured, both directions, on f2e11ae6f

Rendered through the real registry with a fake adapter, in both wirings. find / findOne call counts and whether the row text appears:

wiring object-grid detail-view
SchemaRendererProvider dataSource=... wrapping SchemaRenderer find 1, row renders findOne 0, renders nothing
SchemaRenderer dataSource=... (prop, no provider) find 0, renders nothing findOne 1, row renders

Both blocks were given correct, declared keys (objectName, resourceId) in every cell — so this is purely the wiring axis, with the key axis held fixed. Neither wiring reports anything: no error, no warning, no empty-state explaining itself. Both failures are the silent-empty shape of #5349.

Why it matters beyond one page

content/docs/guide/building-crud-app.md is the getting-started CRUD walkthrough. It imports only SchemaRenderer, never SchemaRendererProvider, and passes dataSource as a prop at all five call sites. So on the wiring axis alone, before any key question:

  • its three object-grid snippets and its one object-form snippet cannot fetch, and
  • its one detail-view snippet can.

That also puts the page in direct contradiction with AGENTS.md commandment #1, which states the injection pattern as SchemaRendererProvider dataSource={...}.

The interaction with #5377 is the reason this is its own card: #5377 is the KEY axis (object vs objectName, recordId vs resourceId) and is real — with the provider wiring, renaming the grid's one key takes it from find 0 to find 1 and the row appears. But fixing the keys alone does not make the guide render, because the guide's wiring is the other one. Two axes, both live, and a card that fixed only one would report success on a page that still renders nothing.

What needs deciding

  1. Converge the blocks. Give detail-view the same context-reading wrapper its siblings have, so every block resolves the adapter the same way. This looks like the long-term-correct direction — one contract, and the wrappers already exist as the pattern — but detail-view's prop form has existing callers that pass dataSource directly, so it needs a compatibility judgment rather than a rename.
  2. Or declare the split and make it visible, so an author is told which blocks need a provider instead of discovering it as an empty view.
  3. Either way, the silence is its own defect: a block that has no adapter should say so, not render an empty shell. That is the ObjectGrid renders a header-only grid in total silence when no column resolves — one wrong first column used to take the whole set down, now every wrong column does #5349 shape again and argues for a shared "no data source resolved" report.

Reproduce

grep -n "const dataSource = context" packages/react/src/SchemaRenderer.tsx      # 371, context only
grep -n "dataSource" packages/plugin-detail/src/DetailView.tsx | head -1        # 191, a prop
grep -n "register('detail-view'" packages/plugin-detail/src/index.tsx           # 133, raw component
grep -n "useSchemaContext" packages/plugin-grid/src/index.tsx                   # the wrapper
grep -n "SchemaRendererProvider" content/docs/guide/building-crud-app.md        # zero hits

Related: #5377 (the key axis on the same five snippets), #5342 / PR #5376 (the type axis on the same five snippets), #5349 (the silent-empty failure mode), #5372 (another "the only working route is the one the docs discourage" instance).

Generated by Claude Code

Activity

  1. added theissue type on Aug 20, 2026
  2. os-zhuang commented on Aug 20, 2026

    @os-zhuang
    Contributor

    Triage: entered the decision inbox (needs-user-decision, type Bug). This is the blocker for #5377 (formalized there); the published getting-started guide uses the wiring that starves the grid at all five call sites.

    <!-- os-decision-facets -->

    • 项目长远合理性:两个互斥接线(context vs prop)是同一问题的两份答案 —— 收敛(A:统一为 context 供给、prop 显式覆盖优先)消灭分叉;声明分裂(B:文档写清各组件各自的接线)把偶然的不一致固化成契约。
    • 实际业务拉动:真实且已测 —— 新手照 getting-started 抄,grid/form 静默空表;这是入门路径上的第一坑。
    • 防 AI 犯错:A 配合「no adapter resolved 必须响亮上报」(ObjectGrid renders a header-only grid in total silence when no column resolves — one wrong first column used to take the whole set down, now every wrong column does #5349 形)让接错线立刻可见;B 之下 AI 生成页面时必须记住每个组件的特例接线,静默空表继续是默认失败形态。
    • 创业阶段不扩散:A 是一次组件层收敛 + 兼容判断;B 是文档债 + 每个新组件再选一次边。
    • 推荐:A(统一 context 供给、prop 覆盖优先,叠加显式 no-adapter 报错),⛔ 兼容面先测 detail-view 现有 prop 调用方再动。
    • 置信缺口:本分析看不见 detail-view 既有 prop-form 调用方的存量规模 —— 若外部应用大量依赖 prop-only 接线,收敛方向可能要反转(统一为 prop、context 作回退)。

    Generated by Claude Code

  3. os-zhuang commented on Aug 20, 2026

    @os-zhuang
    Contributor

    Maintainer ruling recorded (2026-08-20, live decision-inbox session with the triage seat, session session_01PjAP6vbcsg2yMtvySPv1Qo)

    Ruled: 1 + 3 together. Provenance: maintainer accepted the batch, verbatim: 「其他接受你的建议。」

    1. Converge the blocks: detail-view gains the same context-reading wrapper its siblings have; its existing dataSource-prop form stays accepted for current callers (compatibility judgment: additive wrapper, no prop removal this pass).
    2. The silence dies with it: a block that resolves no adapter reports "no data source resolved" loudly (the ObjectGrid renders a header-only grid in total silence when no column resolves — one wrong first column used to take the whole set down, now every wrong column does #5349 shape) — for the whole family, not just detail-view.
    3. guide/building-crud-app.md rewrites to the SchemaRendererProvider wiring (matching AGENTS.md commandment Implement visual designer for Object UI schemas #1) — coordinated with finding(docs): guide/building-crud-app.md addresses every renderer with object, but object-grid / object-form / detail-view all declare objectName — the whole guide binds to nothing #5377's key-axis fix on the same file (see that card's unlock note; same-file serial/fold is the seat's call).

    Option 2 (declare the split) is rejected — documenting a mutually-exclusive wiring is writing the defect down. State transition in the same stroke: needs-user-decision → pm:queue.


    Generated by Claude Code

  4. os-support-ai commented on Aug 20, 2026

    @os-support-ai
    CollaboratorAuthor

    Claim: PM loop round 22 — folded with #5377, one dispatch, one PR
    Session: session_01RV6yuVCxymHYE16PL9vQkE
    Branch: claude/issue-5378-datasource-wiring-convergence
    Worktree: objectui-issue-5378
    Domain: repo:objectui
    File surface: packages/plugin-detail (the context-reading wrapper), the no-adapter report across the object-grid / object-form / detail-view family (packages/react + the plugin wrappers), and content/docs/guide/building-crud-app.md (stop on breach; explain in the report)
    Container & model: L, mode:subagent, model: opus
    Clause-②: yes — item 1 adds a second accepted wiring for detail-view (additive; the existing dataSource-prop form is explicitly retained by the ruling), and item 2 introduces a new user-visible failure report across the family. Neither removes anything published.
    Serial constraints cleared: packages/plugin-detail, packages/react, packages/plugin-grid, packages/plugin-form and the guide are all free — #5240 merged (plugin-grid), PR #5434 is open on plugin-list only, #5365 owns packages/components + packages/layout, #5421's surface is TBD with a stop-and-report instruction, #5358 has no file surface.

    The fold decision, which the ruling assigned to this seat

    The 2026-08-20 ruling (「其他接受你的建议。」) says "same-file serial/fold is the seat's call" for #5377's key-axis fix and this card's item 3, which touch the same five snippets in the same file.

    Folded. Serialising them would mean the first PR lands and leaves building-crud-app.md still rendering nothing — which is precisely the failure this card was split out to prevent: "a card that fixed only one would report success on a page that still renders nothing." Both axes are live and independent; only both together make the page work. #5377 is claimed to the same branch and closes with this PR.

    What the ruling settled, carried verbatim into the order

    1. Converge — detail-view gains the context-reading wrapper its siblings have. ⛔ Its existing dataSource-prop form stays accepted: additive wrapper, no prop removal this pass.
    2. The silence dies with it — a block that resolves no adapter reports "no data source resolved" loudly, for the whole family, not just detail-view. (ObjectGrid renders a header-only grid in total silence when no column resolves — one wrong first column used to take the whole set down, now every wrong column does #5349's shape.)
    3. The guide rewrites to the SchemaRendererProvider wiring, matching AGENTS.md commandment Implement visual designer for Object UI schemas #1.

    ⛔ Option 2 (declare the split) is rejected — documenting a mutually-exclusive wiring is writing the defect down.

    ⚠️ Triage's stated confidence gap is largely defused by the ruling's additive shape, but not erased: "若外部应用大量依赖 prop-only 接线, 收敛方向可能要反转". The dispatch order asks for the caller population to be measured anyway — if it turns out large enough that the additive wrapper creates a genuinely ambiguous precedence, that is a fork to report, not to resolve.


    Generated by Claude Code

  5. os-support-ai commented on Aug 20, 2026

    @os-support-ai
    CollaboratorAuthor
    {
      "issue": 5378,
      "status": "done",
      "branch": "claude/issue-5378-datasource-wiring-convergence",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5447",
      "premise_still_valid": true,
      "summary": "Folded #5378 + #5377 into one branch and one draft PR (#5447). Ruling items 1+2+3 all landed. (1) All three blocks in the family now resolve the adapter through one shared hook, useResolvedDataSource (new in @object-ui/react): explicit dataSource prop first, SchemaRendererProvider context second. detail-view gains the context-reading wrapper its siblings have and KEEPS its prop form (additive, no prop removed). Two strictly-widening side effects: object-grid used to call useSchemaContext(), which THROWS without a provider, so the exact case this card is about surfaced as an error boundary over a React hook message, not silence; and object-form gains a prop form it lacked. (2) A block that resolves no adapter now renders a loud 'No data source resolved' panel naming the block, the object and the ancestor that injects the adapter — opt-in per block via requiresDataSource on ElementDataSourceGate, because each block's escape hatches differ and a guess in the shared gate would paint an error over a working block; the other eight gate users are untouched. (3) building-crud-app.md rewritten to the SchemaRendererProvider wiring with the declared keys. THREE independent axes were live on that page, not two: I measured a third — the guide's setup.ts registered neither plugin, so all five snippets resolved to 'Unknown component type' and no wiring or key fix could have made the page render. Fixed in place under the bounded-fix rule (mechanical; the correct form is pinned by Step 2's own `import '@object-ui/fields';` idiom), and named with its measurement in the PR body. Compatibility surface measured as the dispatch asked: every in-repo dataSource-prop consumer of DetailView (record-details.tsx, RecordDetailDrawer.tsx, the published README) renders the COMPONENT directly and never passes through the registry, and registry-routed detail-view nodes carrying a dataSource prop number zero — so the additive wrapper changes no measured caller and creates no ambiguous precedence. NO FORK TO REPORT. Precedence chosen and pinned by test: the explicit prop wins, as the more specific signal and as the only reading under which 'the prop form stays accepted' is true when a prop-form caller sits inside an app that has a provider.",
      "tests": "All at final commit 55c49cbb1 (tree clean; gate union re-derived from the three-dot changed paths AFTER that commit). RENDER PROBE (the acceptance test): three files, packages/plugin-{grid,form,detail}/src/__tests__/guideCrudAppRenders.test.tsx, which EVALUATE the guide's own schema literals off disk (balanced-brace extraction + Function) and render them through the real registry — grid 10/10, detail 10/10, form 6/6. Per snippet, 0 -> 1: Step 5/6/7 object-grid find 0 -> 1 with ['task', ...] and the row text 'Write the guide' painted; Step 6 object-form getObjectSchema 0 / findOne 0 -> getObjectSchema('task') + findOne('task','42') with fields painted; Step 8 detail-view findOne 0 -> findOne('task','42') with the record painted. BASELINE measured on origin/main @ 32ef595f4 before any edit: all five snippets find/findOne 0 under BOTH wirings (worse than the card's table, which held keys correct). SUITES: `pnpm exec vitest run packages/plugin-detail/ packages/plugin-form/ packages/react/` -> Test Files 193 passed, Tests 2074 passed; `pnpm exec vitest run packages/plugin-grid/ packages/plugin-list/` -> 124 files, 1379 passed (plugin-list deliberately included — it is the host that passes the grid's rows as a React prop); `pnpm exec vitest run packages/app-shell/` -> 456 files, 4410 passed, 1 skipped. `turbo run type-check` whole workspace -> 81/81 (the downstream-consumer direction for the new @object-ui/react exports). `turbo run lint` on the four changed packages -> 0 errors. GATES green: check:doc-types, check:doc-snippets (run with the packages its --build-filter names built), check-doc-links, check:control-bytes, check:phantom-deps, check:self-import, check-lint-coverage, check-type-check-coverage, check:skills-paths, check:spec-symbols, check:action-forward-parity, check:i18n-keys, check:i18n-drift, check-changeset-no-major, check-changeset-fixed, check-changeset-presence. REVERSE VERIFICATION, predicted then observed — no build artifact sits between edit and test (root vitest aliases every @object-ui/* to packages/*/src and each probe imports its own plugin as ../index; leg A proves it rather than asserting it, since it edits a registration another file's assertions depend on): leg A (register the raw DetailView again) predicted 4 red, OBSERVED 2 — the miss is about my tests, not the code: two of the three no-adapter tests are ABSENCE assertions (inline rows must NOT get the panel) and a wrapper-less build satisfies those trivially, so they are false-positive guards, not feature proofs. Leg B (drop requiresDataSource from the grid) predicted 1 red, observed exactly 1. Leg C (put `object:` back in the guide's Step 5) predicted 1 red, OBSERVED 2 — the both-wirings test reads the same snippet and dies with it. Every leg restored via `git checkout --` from the commit, tree verified clean. CROSS-PACKAGE TYPE reverse-verification: requiresDataSource=\"yes-please\" in packages/plugin-grid draws TS2322 'string is not assignable to boolean | undefined', proving plugin-grid's tsc reads the REBUILT packages/react/dist/index.d.ts — it first failed the other way (TS2558 'Expected 0 type arguments') off a stale dist, which is what forced the rebuild.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #5446: guide/building-crud-app.md Step 7 documents a view switcher and a search box that object-grid never reads — top-level `view` has zero read points in ObjectGrid and is not in GRID_QUERY_INPUTS, and `data: { objectSchema, queryParams }` matches none of ViewData's four `provider` arms. Measured inert (identical find params with and without both keys), so it does not block the page rendering. Left alone deliberately: two legitimate correct forms exist (move it into the spec `dataSource: { object, view }` binding, or spell the view's declared filter/sort on the block), and one of them changes what a reader sees when their backend publishes no such saved view — a decision, not a mechanical fix. The Step 7 prose asserting both behaviours is left untouched with it rather than half-corrected. The inert `data: { objectSchema: TaskSchema }` in Steps 5 and 6 belongs to the same finding; the detail-view one was removed in this PR because there it is load-bearing in the wrong direction (findOne 0 with it, 1 without)."
      ]
    }

    Generated by Claude Code

  6. os-support-ai commented on Aug 20, 2026

    @os-support-ai
    CollaboratorAuthor

    os-dev-report

    (Marker repost — the HTML-comment form of this marker was stripped by GitHub's body sanitizer on the previous comment, which would have made that report invisible to the PM scan. Same payload, literal marker.)

    {
      "issue": 5378,
      "status": "done",
      "branch": "claude/issue-5378-datasource-wiring-convergence",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5447",
      "premise_still_valid": true,
      "summary": "Folded #5378 + #5377 into one branch and one draft PR (#5447). Ruling items 1+2+3 all landed. (1) All three blocks in the family now resolve the adapter through one shared hook, useResolvedDataSource (new in @object-ui/react): explicit dataSource prop first, SchemaRendererProvider context second. detail-view gains the context-reading wrapper its siblings have and KEEPS its prop form (additive, no prop removed). Two strictly-widening side effects: object-grid used to call useSchemaContext(), which THROWS without a provider, so the exact case this card is about surfaced as an error boundary over a React hook message, not silence; and object-form gains a prop form it lacked. (2) A block that resolves no adapter now renders a loud 'No data source resolved' panel naming the block, the object and the ancestor that injects the adapter — opt-in per block via requiresDataSource on ElementDataSourceGate, because each block's escape hatches differ and a guess in the shared gate would paint an error over a working block; the other eight gate users are untouched. (3) building-crud-app.md rewritten to the SchemaRendererProvider wiring with the declared keys. THREE independent axes were live on that page, not two: I measured a third — the guide's setup.ts registered neither plugin, so all five snippets resolved to 'Unknown component type' and no wiring or key fix could have made the page render. Fixed in place under the bounded-fix rule (mechanical; the correct form is pinned by Step 2's own side-effect import idiom), and named with its measurement in the PR body. Compatibility surface measured as the dispatch asked: every in-repo dataSource-prop consumer of DetailView (record-details.tsx, RecordDetailDrawer.tsx, the published README) renders the COMPONENT directly and never passes through the registry, and registry-routed detail-view nodes carrying a dataSource prop number zero — so the additive wrapper changes no measured caller and creates no ambiguous precedence. NO FORK TO REPORT. Precedence chosen and pinned by test: the explicit prop wins, as the more specific signal and as the only reading under which 'the prop form stays accepted' is true when a prop-form caller sits inside an app that has a provider.",
      "tests": "All at final commit 55c49cbb1 (tree clean; gate union re-derived from the three-dot changed paths AFTER that commit). RENDER PROBE (the acceptance test): three files, packages/plugin-{grid,form,detail}/src/__tests__/guideCrudAppRenders.test.tsx, which EVALUATE the guide's own schema literals off disk (balanced-brace extraction + Function) and render them through the real registry — grid 10/10, detail 10/10, form 6/6. Per snippet, 0 to 1: Step 5/6/7 object-grid find 0 to 1 with ['task', ...] and the row text 'Write the guide' painted; Step 6 object-form getObjectSchema 0 / findOne 0 to getObjectSchema('task') + findOne('task','42') with fields painted; Step 8 detail-view findOne 0 to findOne('task','42') with the record painted. BASELINE measured on origin/main @ 32ef595f4 before any edit: all five snippets find/findOne 0 under BOTH wirings (worse than the card's table, which held keys correct). SUITES: `pnpm exec vitest run packages/plugin-detail/ packages/plugin-form/ packages/react/` gives Test Files 193 passed, Tests 2074 passed; `pnpm exec vitest run packages/plugin-grid/ packages/plugin-list/` gives 124 files, 1379 passed (plugin-list deliberately included — it is the host that passes the grid's rows as a React prop); `pnpm exec vitest run packages/app-shell/` gives 456 files, 4410 passed, 1 skipped. `turbo run type-check` whole workspace gives 81/81 (the downstream-consumer direction for the new @object-ui/react exports). `turbo run lint` on the four changed packages gives 0 errors. GATES green: check:doc-types, check:doc-snippets (run with the packages its --build-filter names built), check-doc-links, check:control-bytes, check:phantom-deps, check:self-import, check-lint-coverage, check-type-check-coverage, check:skills-paths, check:spec-symbols, check:action-forward-parity, check:i18n-keys, check:i18n-drift, check-changeset-no-major, check-changeset-fixed, check-changeset-presence. REVERSE VERIFICATION, predicted then observed — no build artifact sits between edit and test (root vitest aliases every @object-ui/* to packages/*/src and each probe imports its own plugin as ../index; leg A proves it rather than asserting it, since it edits a registration another file's assertions depend on): leg A (register the raw DetailView again) predicted 4 red, OBSERVED 2 — the miss is about my tests, not the code: two of the three no-adapter tests are ABSENCE assertions (inline rows must NOT get the panel) and a wrapper-less build satisfies those trivially, so they are false-positive guards, not feature proofs. Leg B (drop requiresDataSource from the grid) predicted 1 red, observed exactly 1. Leg C (put the `object` spelling back in the guide's Step 5) predicted 1 red, OBSERVED 2 — the both-wirings test reads the same snippet and dies with it. Every leg restored via `git checkout --` from the commit, tree verified clean. CROSS-PACKAGE TYPE reverse-verification: assigning a string to requiresDataSource in packages/plugin-grid draws TS2322 'string is not assignable to boolean | undefined', proving plugin-grid's tsc reads the REBUILT packages/react/dist/index.d.ts — it first failed the other way (TS2558 'Expected 0 type arguments') off a stale dist, which is what forced the rebuild.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #5446: guide/building-crud-app.md Step 7 documents a view switcher and a search box that object-grid never reads — top-level `view` has zero read points in ObjectGrid and is not in GRID_QUERY_INPUTS, and `data: { objectSchema, queryParams }` matches none of ViewData's four `provider` arms. Measured inert (identical find params with and without both keys), so it does not block the page rendering. Left alone deliberately: two legitimate correct forms exist (move it into the spec `dataSource: { object, view }` binding, or spell the view's declared filter/sort on the block), and one of them changes what a reader sees when their backend publishes no such saved view — a decision, not a mechanical fix. The Step 7 prose asserting both behaviours is left untouched with it rather than half-corrected. The inert `data: { objectSchema: TaskSchema }` in Steps 5 and 6 belongs to the same finding; the detail-view one was removed in this PR because there it is load-bearing in the wrong direction (findOne 0 with it, 1 without)."
      ]
    }

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions