Uh oh!
There was an error while loading. Please reload this page.
fix(components): diagnose an authored bind on data-table at render - #6663
Conversation
`bind` is the data-scope vocabulary resolved by `useDataScope()`. `data-table`
does not read it — it takes its rows from an inline `data` array on the node —
yet the key was accepted by every gate (`BaseSchema`'s index signature on the TS
side, `.passthrough()` on the zod side). The author got a header drawn over the
"No results found" empty state with no error, no warning and no diagnostic.
Adds `dataTableBindDiagnostic.ts` and wires it from `DataTableRenderer` through
the channel `plugin-grid`'s `columnSpellingDiagnostics.ts` already uses for this
exact shape of failure: a pure describe function returning `string | null`, a
`useEffect` keyed on the schema slice, one `console.warn`, no NODE_ENV branch.
No behaviour change: `data-table` still does not read `bind` (maintainer ruling
2026-08-27, option A). The message never overstates itself — a node carrying
both `data` and `bind` is told its rows came from `data`, not that its body is
empty.
`ObjectDataTable` stops forwarding a `bind` it has already consumed. It resolves
the binding itself and then delegated with `{ ...schema, type: 'data-table' }`,
handing the spent key to a component that cannot read one; without this a
correctly authored, guide-taught `object-data-table` would trip the new
diagnostic on every render.
Part of #6575
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-sales
commented
Aug 28, 2026
✅ ACCEPT — flipped ready, auto-merge onReviewer: Gate reading: all 29 check runs read and the returned count compared against What I re-measured myself
The diff is 7 files, 554 insertions against 3 deletions, and nothing in it reaches outside the card. The part of this I would have missedThe bounded producer fix is the piece worth naming. A new diagnostic whose first act is to accuse correct usage is worse than no diagnostic, and this one would have shipped that way. It was found and fixed at the producer — mirroring Two judgment calls I am ratifying
Card path drift, recorded rather than papered overThe card's reproduce block names That is the third mis-aimed probe this rotation, and the pattern is now unmistakable: a zero on a path nobody proved exists is not a measurement. I hit the same class twice myself while reviewing #6662 today. Establish the path, then read the zero. Follow-up Generated by Claude Code |
Fixes#6575
Implements the maintainer ruling of 2026-08-27 (verbatim 「同意」, option A): a
data-tablecarrying a
bindnow gets a loud render-time diagnostic. No behaviour change to thepublished component — it still does not read
bind, and option B (making it auseDataScopereader) is explicitly not part of this card. Option C stays blocked on the.passthrough()ceiling (#5155 / #6269).All verification below ran against
35984e79, the head of this branch, with a clean tree(
git diff HEADempty).Premise check on the merge-base (
813bf832)Confirmed before building, because the card's reproduce block names a path that does not
exist:
packages/components/src/renderers/data-display/data-table.tsx. Therenderer actually lives at
packages/components/src/renderers/complex/data-table.tsx.The reproduce command therefore returned "no hits" for a file that was not there — a
true conclusion reached by an unsound route.
grep -c useDataScopeinrenderers/complex/data-table.tsxis 0, while the samequery against
renderers/data-display/list.tsxis 2. So the zero is a reading.data: rawData = EMPTY_ROWSin the schema destructure, resolved asconst data = Array.isArray(rawData) ? rawData : EMPTY_ROWS. The premise holds.The precedent, and a correction to the reference
The ruling names "
packages/plugin-grid's column-spelling diagnostic,visibilityDiagnostic.ts". Those are two different files:visibilityDiagnostic.tsis inpackages/react/src/utils/, not plugin-grid. Itdiagnoses unevaluable node-gate predicates, and carries a module-level dedupe
Setkeyed on the predicate text.
packages/plugin-grid/src/columnSpellingDiagnostics.ts.The referent satisfying both halves of the phrase is
columnSpellingDiagnostics.ts, and itis also the nearer match by defect class: "you declared something and the renderer dropped
it", which is exactly this card. Its channel is what this PR copies:
describe…function returningstring | null, extracted so the judgement and thereporter cannot drift;
useEffectkeyed on the schema slice;console.warn, no NODE_ENV branch;ends with its issue ref.
Why no dedupe
Sethere, having read the one invisibilityDiagnostic.ts: thatSetexists because a node gate is evaluated once per row, so one authoring bug would otherwise
print N lines. A
data-tableis one node rendered once — the effect key already is "oneline per mount per distinct
bind", which is the ceiling theSetbuys. Adding modulestate would also add a
__resetexport and a test-ordering hazard for no gain.The wording, and why it agrees with the corpus
The core sentence is the ruling's own, and it is the corpus phrasing rather than a third
one.
skills/objectui/rules/protocol.md:162already teaches: "data-tabledoes NOT: itreads its rows from an inline
dataarray on the node, so abindon it is ignored and thetable renders its header over an empty body — no error, no warning." The diagnostic reuses
"does not read
bind", "reads its rows from the inlinedataarray on the node" and"renders its header over an empty body" verbatim, so an author who hits the console and an
author who reads the guide meet the same three phrases.
The message never asserts something it did not check. A node can carry both
dataandbind; that table is not empty, so the "empty body" clause would be false there. Theconsequence is measured against the rows the renderer actually resolved, and the two cases
get two clauses — the second reads "The 2 rows on screen come from
data; thebindcontributes nothing". This is the discipline
columnSpellingDiagnostics.tsrecords in itsown docstring after its reverse-verification found the same class of overstatement.
Both-directions evidence
packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx— the existingpin — is extended, not weakened. Everything it asserted still stands:
bindis stillnot read, the bound array still never reaches the renderer, the body is still one row of
empty state. The new assertions read the diagnostic off the same renders, so behaviour
and diagnostic cannot drift into two trees:
bindform warns exactly once, and the line contains`bind: 'customers'` is ignored, the ruling's sentence, the empty-body consequence,and the way out.
datanode — same renderer, same helper, onebindkey apart — produces an empty warning list.The new assertions are what discriminate, proven by ablation rather than asserted.
Cutting only the diagnostic's output line (
if (message) console.warn(message);):data-table.tsxd0a32a0a6734078217603228ae2a098766fd281dgrep -c1/094ded1c06b57e97416a626af5129a286ea588948(differs, so the edit reached disk)Test Files 1 failed | 1 passed (2),Tests 1 failed | 24 passed (25)expected [] to have a length of 1on the firing assertiond0a32a0a…— back to HEAD0, warn line count1,git diff HEADemptyThe 24 that still pass in the ablated tree are the point the card made: every pre-existing
behaviour assertion is green against a tree with no diagnostic in it. Restoration used
git checkout HEAD -- ABS_PATHfrom a trap with an absolute repo root, and is proven byobservation, never by an exit code.
packages/components/src/renderers/complex/__tests__/data-table-bind-diagnostic.test.tsadds the pure-message half. Every zero in it is paired with a positive control in the same
call shape.
A bounded in-place fix, declared:
ObjectDataTablestops forwarding a consumedbindFound while building, and the diagnostic is wrong without it — so this is named here
rather than filed away.
ObjectDataTable(@object-ui/plugin-dashboard) is one of theobject-*widgets thatgenuinely reads
bind:const boundData = useDataScope(schema.bind)at line 554. It thendelegated with
{ ...schema, type: 'data-table', data: finalData, … }, spreading thealready-spent key into a component that reads no
bind. A correctly authored, published-guide-taught
object-data-tablewould have tripped the new diagnostic on every render,over rows that were on screen precisely because its
bindhad been honoured — the exact"paint a configuration error over a working grid, which is worse than the silence it
replaces" failure
columnSpellingDiagnostics.tswarns about.Measured, not argued. The new pin was written first and run against the unfixed tree:
The fix is on the producer, not a tolerance carve-out in the consumer: a key this widget has
consumed is this widget's to stop. Its own sibling
DashboardGridLayoutalready forwards inthat exact shape (
const { data: _data, ...restOptions } = options). Three legs pin it,because each alone admits a wrong fix — (1) no
bindon the forwarded node, (2) the rowsstill arrive so the binding was consumed rather than deleted, (3) an unrelated authored key
still survives the spread.
The other three internal producers were checked and are clean:
RelatedListandObjectGridbuild fresh nodes with no spread of the outer schema;
DashboardRendererandDashboardGridLayoutspread a widgetoptionsblock on their static table branches,where a
bindreally would be inert — a diagnostic there is correct, not a false positive.PR #6574 finding
Still open and still a draft; not merged (
state: open,draft: true,merged: false).Confirmed on the merge-base rather than taken from the PR's status:
grep -c '\bbind\b'onpackages/types/src/base.tsreturns 0, andBaseSchemacarries[key: string]: any.So today an authored
bindon adata-tableis not type-visible — it rides the indexsignature on the TS side and
.passthrough()on the zod side, which is what makes thesilence total. When #6574 lands it narrows the key's value to a string on every node; it
cannot refuse the key on a node that ignores it, so it neither creates nor fixes this trap,
and this PR is independent of it. No file overlap: #6574 touches
packages/types/**,its changeset, and
ObjectPivotTable.tsx; this PR touchespackages/components/**,ObjectDataTable.tsx, and its own changeset. If anything, #6574 slightly raises the value ofthis diagnostic — a centrally declared
bindinvites authors to expect "bindableeverywhere", which is exactly the drift the triage facet block flagged as an unquantified
confidence gap.
Deliberately NOT in this PR
skills/objectui/rules/protocol.md— the other half of the ruling's dispatch notes. It isthe skills lane's surface and a human-merge-only governed path, being routed separately.
The code change is coherent without it: the diagnostic quotes the guide's existing
sentences rather than replacing them, so the console and the corpus already agree today. The
skills-side edit is an enhancement (telling authors the warning now exists), not a
prerequisite. Nothing here contradicts what that file currently teaches, and the pin test
still lifts its blocks out of the real file at run time, so the two cannot silently diverge.
Verification
Run from the repo root with path filters (a package-dir
vitestinvocation is refused by thein-repo guard, #3378), serialized through the shared verify lock.
vitest runover 98 affected filesTest Files 98 passed (98)·Tests 894 passed (894)pnpm --filter @object-ui/components type-check0pnpm --filter @object-ui/plugin-dashboard type-check0pnpm --filter '@object-ui/plugin-dashboard^...' build0check-changeset-presence6 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)check-changeset-fixed/-no-major/-overwriteAll workspace packages are in the changeset fixed group.·No changeset declares a major bump.·No pre-existing changeset was modified or deleted.check:control-bytes0(plus a direct control-byte scan of all seven touched files)check:vi-mock-specifiers0— implicated by the newvi.mockin the plugin-dashboard pincheck:self-import,check:phantom-deps,check:esm-specifiers,check:entry-guard,check:skills-paths0lint:coverage46/46 packages linted, 0 with outstanding errors (0 total)type-check:coverage45/46 via type-check, 0 known-broken·41/41 packages compile their testseslint .over the whole repo0— 3879 files, 0 errors; 0 warnings land inside any line this diff inserted (checked against the post-image hunk ranges)check:readme-exportsis NOT MEASURED, not red, and says so itself: "the populationCOLLAPSED -- 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 and
public export lists — is untouched here: this PR adds no README and no package export
(
describeIgnoredBindstays internal topackages/components, matchingcolumnSpellingDiagnostics.ts, which plugin-grid also does not export).The first plugin-dashboard
type-checkof this branch reportedCannot find module '@object-ui/components'— a precondition failure, not a red gate. It wasre-run properly after building the closure it named, and is the exit
0in the table above.Generated by Claude Code