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

data-table accepts a bind and silently renders its header over an empty body #6575

Description

@os-support-ai

Found while censusing bind producers and consumers for #6357 (PR #6574). Not that card's defect — filed separately per its dispatch.

The fact

bind is the data-scope binding vocabulary: a path string resolved by useDataScope(). Ten production sites read it. data-table is not one of them — it reads its rows from an inline data array on the node and never calls the hook.

So a bind on a data-table node is accepted by every gate and does nothing:

  • the TS side accepts it (BaseSchema's [key: string]: any);
  • the zod side accepts it (BaseSchema is .passthrough(), and DataTableSchema is BaseSchema.extend(…), which inherits that);
  • nothing reads it at render.

The result is a table that draws its header over an empty body — no error, no warning, no diagnostic.

This is already documented and already pinned — it just has no card

  • skills/objectui/rules/protocol.md:162 states it verbatim: "data-table does NOT: it reads its rows from an inline data array on the node, so a bind on it is ignored and the table renders its header over an empty body — no error, no warning."
  • packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx pins the behaviour, including the measured detail that "the bound array never reaches the renderer at all" — the body is one row, the empty state spanning the table.

So the behaviour is known and asserted. What is missing is a decision about whether it should stay.

Why declaring bind centrally did not change this

PR #6574 declares bind on BaseSchema (the #6357 census concluded it belongs there). That does not touch this: the key was accepted on every node before the declaration existed, via the index signature and .passthrough(). The declaration narrows the key's value to a string; it cannot refuse the key on a node that ignores it. Recording that explicitly so this is not read as a regression from that PR.

What needs deciding

Three shapes, not obviously one answer:

  1. Make data-table a reader — call useDataScope(schema.bind) like the other nine. Consistent with the vocabulary the protocol teaches, but it is a behaviour change on a published component: a node carrying both data and bind would need a stated precedence.
  2. Reject it loudly — refuse bind on data-table at parse time. Blocked on the same ceiling as everything else in this class: .passthrough() means a per-node strict shape is needed first (finding(types): BaseSchema's [key: string]: any leaves every component schema open, so a "declare the surface" fix can never reject a misspelled TOP-LEVEL key #5155 / finding(types): ObjectViewSchema's table and form slots declare ZERO properties — the same Omit-under-index-signature collapse as #6151, in property position #6269).
  3. Diagnose it at render — the repo already has a diagnostic channel for exactly this shape of silent failure (packages/plugin-grid's column-spelling diagnostic, visibilityDiagnostic.ts).

Leaning 3 then 1, but this is a published-surface behaviour question rather than a mechanical fix, which is why this is a finding and not a patch.

Reproduce

grep -rn "useDataScope" packages --include='*.tsx' --include='*.ts' | grep -v __tests__
grep -rn "useDataScope" packages/components/src/renderers/data-display/data-table.tsx   # no hits
sed -n '160,166p' skills/objectui/rules/protocol.md

Refs: #6357, PR #6574. Adjacent ceiling: #5155 / #6269.

Activity

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

    @os-support-ai
    CollaboratorAuthor

    Triage: routed — needs-user-decision + domain:ui, type Feature. This is a published-component behaviour question (whether data-table should honour, refuse, or diagnose bind), which sits on the manual floor — the card itself says it is a decision, not a patch. Decision analysis below in Chinese per convention; the facet block marker is written in literal spelling for the sanitizer.

    os-decision-facets

    一句话问题:页面作者给数据表写了数据绑定,预览出来表格只有表头、一行数据都没有,也没有任何提示 —— 作者以为是自己数据的问题,其实是这个表格根本不认「绑定」这个词,而平台的技能文档已经在教用户绕这个坑,等于把缺陷当使用说明卖。

    选项 × 真实代价 × 业务直译:

    四棱:

    • ① 项目长远合理性:A 不扩契约不加特例;B 才是词表真正统一的终态,但它是产品方向,可在 A 落地后单独裁 —— A 不堵 B 的路。
    • ② 实际业务拉动:实测有 —— skills/objectui/rules/protocol.md:162 专门写了这个坑、还有 pin 测试锁着这个行为,平台已经在为它付教学成本,说明真实作者真的撞。
    • ③ 防 AI 犯错:AI 生成页面最容易写出「带 bind 的 data-table」——今天无错无警,空表静默上线;A 让出错时作者与 AI 当场看到具名提示,响亮优于静默。
    • ④ 创业阶段不扩散:A 复用既有诊断通道,近零增量;B 是行为扩张且带优先级新决定,defer 不亏。

    推荐 A(诊断先行),回退 B(若裁词表统一优先,则同笔裁定 data 与 bind 并存时的优先级)。置信缺口:本分析未 census 生产应用里有多少 data-table 节点真带着 bind(拉动强度未量化);也看不见 PR #6574 把 bind 声明进 BaseSchema 之后作者预期是否会向「处处可绑」漂移(那会加重 D 的静默代价)。

    裁后执行:裁 A ⇒ 转 pm:queue,派发词点名 visibilityDiagnostic.ts 先例与 pin 测试同步更新;裁 B ⇒ 需另立 data/bind 优先级裁决后再入队;裁 D ⇒ 关 not planned,重开免费。


    Generated by Claude Code

  3. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    Contributor

    Maintainer ruling recorded — Option A: diagnose at render; the vocabulary question stays open for a later ruling

    Provenance: maintainer, 2026-08-27, PM chat decision-inbox batch 3 (adjudication session session_01DKWDdUJ2XNRESVVWUvcpnh), verbatim: 「同意」 — accepting the triage recommendation A (2026-08-26T14:11Z facet block).

    Ruled: data-table receiving a bind gets a loud render-time diagnostic ("data-table does not read bind; it reads its rows from the inline data array"), reusing the existing diagnostic-channel precedent (packages/plugin-grid's visibilityDiagnostic.ts). No behaviour change to the published component; the trap stops being silent.

    Dispatch notes: update the pin (packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx) to assert the diagnostic alongside the existing behaviour, and refresh skills/objectui/rules/protocol.md:162 to mention the diagnostic (the skills/** net-line budget applies). ⛔ Option B (making data-table a useDataScope reader) is NOT part of this card — if vocabulary unification is wanted later it needs its own ruling, including data-vs-bind precedence. Option C stays blocked on the .passthrough() ceiling (#5155/#6269).

    State: needs-user-decision → pm:queue.


    Generated by Claude Code

  4. self-assigned this
    on Aug 28, 2026
  5. claude commented on Aug 28, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 6575,
      "status": "done",
      "branch": "claude/issue-6575-data-table-bind-diagnostic",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6663",
      "premise_still_valid": true,
      "summary": "Option A implemented: a `data-table` node carrying a `bind` now emits a render-time console.warn naming the node, the path spelled, the key that IS read, and the way out. No behaviour change - `grep -c useDataScope` in the renderer is still 0. New pure module packages/components/src/renderers/complex/dataTableBindDiagnostic.ts, wired from DataTableRenderer. PRECEDENT CORRECTION: the ruling names \"packages/plugin-grid's column-spelling diagnostic, visibilityDiagnostic.ts\" but those are two files - visibilityDiagnostic.ts is in packages/react/src/utils/, and plugin-grid's column-spelling diagnostic is columnSpellingDiagnostics.ts. I matched the latter (pure describe fn returning string|null + useEffect keyed on the schema slice + one console.warn + no NODE_ENV branch), which satisfies both halves of the phrase and is the nearer defect class. No dedupe Set: visibilityDiagnostic needs one because a node gate runs once per row; a data-table is one node rendered once, so the effect key already IS the one-line-per-authoring-bug ceiling. WORDING reuses three phrases verbatim from skills/objectui/rules/protocol.md:162 so console and corpus agree; the consequence clause is measured, so a node with BOTH data and bind is told its rows came from `data` rather than falsely told its body is empty. BOUNDED IN-PLACE FIX, declared in the PR body: ObjectDataTable (plugin-dashboard) reads `bind` itself via useDataScope(schema.bind) and then delegated with `{...schema, type:'data-table'}`, forwarding the spent key - so without this the new diagnostic would fire on every render of a correctly authored, guide-taught object-data-table. Fixed at the producer (mirroring sibling DashboardGridLayout's `const {data:_data,...restOptions}`), not as a consumer tolerance. CARD PATH DRIFT: the card's reproduce block names renderers/data-display/data-table.tsx; the renderer is at renderers/complex/data-table.tsx, so that grep returned 'no hits' for an absent file - a true conclusion by an unsound route. Re-measured on the real file with a positive control (list.tsx = 2 hits). skills/** untouched per the fence; the code is coherent without it because the diagnostic quotes the guide's existing sentences rather than replacing them.",
      "tests": "All runs serialized through scripts/pm/os-verify-lock.sh; vitest from the repo ROOT with path filters (package-dir vitest is refused by the #3378 guard). All readings below are against final commit 35984e79 with a clean tree (`git diff HEAD` empty). GREEN: `pnpm exec vitest run packages/components/src/renderers/complex/__tests__/ packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx packages/components/src/__tests__/skill-guide-provider-envelope.test.tsx packages/plugin-dashboard/src/__tests__/ --maxWorkers=2` -> `Test Files 98 passed (98)` / `Tests 894 passed (894)`. BOTH DIRECTIONS, same renders, in the existing pin (extended, nothing removed): FIRES - the retired bind form warns exactly once, line contains \"`bind: 'customers'` is ignored\", the ruling's sentence, the empty-body consequence and the way out; DOES NOT FIRE - the taught inline-`data` node, same renderer and helper, one bind key apart, yields an empty warning list. ABLATION (cut only `if (message) console.warn(message);`): NO REBUILD LEG APPLIES AND HERE IS WHY - the pin imports the renderer through a RELATIVE path ('../renderers') inside its own package, so the subject resolves from src, not through a dependency's `exports` to dist; the dist-rebuild rule is conditioned on that resolution and does not bind here. Mutation proven on disk, not by an editor exit code: HEAD blob d0a32a0a6734078217603228ae2a098766fd281d vs mutated 94ded1c06b57e97416a626af5129a286ea588948, anchored grep -c on the INJECTED marker = 1 and on the REMOVED text = 0. Ablated run: exit 1, `Test Files 1 failed | 1 passed (2)`, `Tests 1 failed | 24 passed (25)`, the single failure being `expected [] to have a length of 1` on the firing assertion - i.e. the 24 pre-existing behaviour assertions are green against a tree with no diagnostic, exactly the card's warning. Restoration by OBSERVATION: `git checkout HEAD -- ABSOLUTE_PATH` from a trap with an absolute repo root, then blob back to d0a32a0a, marker count 0, warn-line count 1, `git diff HEAD` empty. PRODUCER FIX RED-BEFORE-GREEN: the plugin-dashboard pin was written first and run against the unfixed tree -> `Test Files 1 failed | 2 passed (3)`, failing exactly on leg 1 `expect('bind' in inner).toBe(false)` with `expected true to be false`. Three legs so no wrong fix passes: no bind forwarded / rows still arrive (consumed, not deleted) / an unrelated authored key still survives the spread. GATES (exit code captured before any pipe; quoting each gate's own verdict line): components type-check exit 0; plugin-dashboard type-check exit 0; `pnpm --filter '@object-ui/plugin-dashboard^...' build` exit 0; check-changeset-presence `6 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)`; check-changeset-fixed/-no-major/-overwrite all green; check:control-bytes exit 0 plus a direct control-byte scan of all 7 touched files; check:vi-mock-specifiers exit 0 (implicated by the new vi.mock); check:self-import, check:phantom-deps, check:esm-specifiers, check:entry-guard, check:skills-paths all exit 0; lint:coverage `46/46 packages linted, 0 with outstanding errors (0 total)`; type-check:coverage `45/46 via type-check, 0 known-broken` and `41/41 packages compile their tests`. LINT: no narrowing argument needed - I ran the FULL population, `eslint .` over the whole repo, exit 0, 3879 files, 0 errors; and 0 warnings land inside any line this diff inserted (checked per-message line numbers against the post-image hunk ranges; the pre-existing warnings in the two edited files are at L23/L55/L778/L1743 and L648, all outside my ranges, and my new useEffect produced no exhaustive-deps warning). NOT MEASURED, not red - two of them, both re-run or explained rather than reported as failures: (1) the first plugin-dashboard type-check said `Cannot find module '@object-ui/components'` - a missing-dist precondition, re-run properly after building the closure it named, then exit 0; (2) check:readme-exports still says of itself `the population COLLAPSED -- this run proves nothing ... packagesRead: found 12, floor is 25 ... 25 unbuilt` - it needs a full repo build, which is CI's run, and its judged surface (package READMEs, public export lists) is untouched here since describeIgnoredBind stays internal, matching plugin-grid which also does not export columnSpellingDiagnostics. SCRIPT-NAME MISSES, not gate failures: `pnpm run check:changeset-presence` and `check:lint-coverage` exit 1 with ERR_PNPM_NO_SCRIPT (the real names are the direct `node scripts/check-changeset-*.mjs` invocations and `lint:coverage`); re-run correctly and green, as recorded above. Gate list derived from my ACTUAL diff against this repo's own package.json and .github/workflows/ - scripts/pm/dispatch-gates.mjs lives only in objectstack and would have answered about the wrong tree.",
      "mcp_calls": "2 - one issue_read for the card, plus one targeted search_issues for the dedupe check. Everything else (comments, PR 6574 status and file list, open-PR claim scan, PR creation, body read-back, finding creation) went through repo-scoped REST at zero MCP cost. CHANNEL SWITCH DECLARED: repo-scoped REST reads work from this seat (HTTP 200), but /search/issues is 403 ('sessions are bound to their configured repositories'), so the dedupe search had to switch to MCP. It returned 6 hits, so the non-zero result is its own proof the method worked and no separate control term was needed.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6665: a `${...}` expression authored in node-level `data` on a data-table is not evaluated (rawData is a string, Array.isArray fails, rows fall back to EMPTY_ROWS) and renders an empty body silently - the same family as #6575 but one key over, so the new bind-keyed diagnostic does not cover it. Unassigned, no labels, left for PM triage. Quoted from protocol.md's own measured table (f1c27f037) and explicitly NOT re-measured on 813bf832; the issue says so and names re-measuring as the first step.",
        "not filed, reported here as a documentation inaccuracy in the card and the ruling rather than a code defect: (a) the card's reproduce block names packages/components/src/renderers/data-display/data-table.tsx, which does not exist - the renderer is at renderers/complex/data-table.tsx, so the quoted grep returned 'no hits' for an absent file; (b) the ruling and triage both cite \"packages/plugin-grid's ... visibilityDiagnostic.ts\", conflating packages/react/src/utils/visibilityDiagnostic.ts with packages/plugin-grid/src/columnSpellingDiagnostics.ts. Both are recorded in the PR body."
      ]
    }

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

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

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions