Repository navigation
data-table accepts a bind and silently renders its header over an empty body #6575
Description
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 (
BaseSchemais.passthrough(), andDataTableSchemaisBaseSchema.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:162states it verbatim: "data-tabledoes NOT: it reads its rows from an inlinedataarray on the node, so abindon 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.tsxpins 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:
- Make
data-tablea reader — calluseDataScope(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 bothdataandbindwould need a stated precedence. - Reject it loudly — refuse
bindondata-tableat 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]: anyleaves every component schema open, so a "declare the surface" fix can never reject a misspelled TOP-LEVEL key #5155 / finding(types): ObjectViewSchema'stableandformslots declare ZERO properties — the same Omit-under-index-signature collapse as #6151, in property position #6269). - 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
Activity
os-support-ai commentedon Aug 26, 2026
Triage: routed —
needs-user-decision+domain:ui, type Feature. This is a published-component behaviour question (whetherdata-tableshould honour, refuse, or diagnosebind), 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
一句话问题:页面作者给数据表写了数据绑定,预览出来表格只有表头、一行数据都没有,也没有任何提示 —— 作者以为是自己数据的问题,其实是这个表格根本不认「绑定」这个词,而平台的技能文档已经在教用户绕这个坑,等于把缺陷当使用说明卖。
选项 × 真实代价 × 业务直译:
data和bind的页面会变样,必须先裁「两个都给时听谁的」。.passthrough()天花板挡着(finding(types): BaseSchema's[key: string]: anyleaves every component schema open, so a "declare the surface" fix can never reject a misspelled TOP-LEVEL key #5155/finding(types): ObjectViewSchema'stableandformslots declare ZERO properties — the same Omit-under-index-signature collapse as #6151, in property position #6269 先行),不是本卡的可选项。四棱:
skills/objectui/rules/protocol.md:162专门写了这个坑、还有 pin 测试锁着这个行为,平台已经在为它付教学成本,说明真实作者真的撞。推荐 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
os-zhuang commentedon Aug 27, 2026
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-tablereceiving abindgets a loud render-time diagnostic ("data-table does not readbind; it reads its rows from the inlinedataarray"), reusing the existing diagnostic-channel precedent (packages/plugin-grid'svisibilityDiagnostic.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 refreshskills/objectui/rules/protocol.md:162to mention the diagnostic (theskills/**net-line budget applies). ⛔ Option B (makingdata-tableauseDataScopereader) is NOT part of this card — if vocabulary unification is wanted later it needs its own ruling, includingdata-vs-bindprecedence. Option C stays blocked on the.passthrough()ceiling (#5155/#6269).State:
needs-user-decision→pm:queue.Generated by Claude Code
fix(components): diagnose an authored `bind` on `data-table` at render
claude commentedon Aug 28, 2026
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
fix(components): diagnose an authored `bind` on `data-table` at render (