Skip to content

fix(components): diagnose an authored bind on data-table at render - #6663

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6575-data-table-bind-diagnostic
Aug 28, 2026
Merged

fix(components): diagnose an authored bind on data-table at render#6663
os-sales merged 1 commit into
mainfrom
claude/issue-6575-data-table-bind-diagnostic

Conversation

@claude

@claudeclaudeBot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Fixes#6575

Implements the maintainer ruling of 2026-08-27 (verbatim 「同意」, option A): a data-table
carrying a bind now gets a loud render-time diagnostic. No behaviour change to the
published component
— it still does not read bind, and option B (making it a
useDataScope reader) 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 HEAD empty).

Premise check on the merge-base (813bf832)

Confirmed before building, because the card's reproduce block names a path that does not
exist:

  • The card says packages/components/src/renderers/data-display/data-table.tsx. The
    renderer 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.
  • Re-measured on the real file, with a positive control in the same query shape:
    grep -c useDataScope in renderers/complex/data-table.tsx is 0, while the same
    query against renderers/data-display/list.tsx is 2. So the zero is a reading.
  • Rows come from data: rawData = EMPTY_ROWS in the schema destructure, resolved as
    const 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.ts is in packages/react/src/utils/, not plugin-grid. It
    diagnoses unevaluable node-gate predicates, and carries a module-level dedupe Set
    keyed on the predicate text.
  • The column-spelling diagnostic in plugin-grid is
    packages/plugin-grid/src/columnSpellingDiagnostics.ts.

The referent satisfying both halves of the phrase is columnSpellingDiagnostics.ts, and it
is 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:

  • a pure describe… function returning string | null, extracted so the judgement and the
    reporter cannot drift;
  • called from a useEffect keyed on the schema slice;
  • one console.warn, no NODE_ENV branch;
  • a message that names the ADDRESS (which node, what it spells, what to write instead) and
    ends with its issue ref.

Why no dedupe Set here, having read the one in visibilityDiagnostic.ts: that Set
exists because a node gate is evaluated once per row, so one authoring bug would otherwise
print N lines. A data-table is one node rendered once — the effect key already is "one
line per mount per distinct bind", which is the ceiling the Set buys. Adding module
state would also add a __reset export and a test-ordering hazard for no gain.

The wording, and why it agrees with the corpus

[ObjectUI] DataTable bind: data-table (id: 'x', caption: 'Customers') — `bind: 'customers'`
is ignored: data-table does not read `bind`; it reads its rows from the inline `data` array
on the node. This node has no inline rows, so the table renders its header over an empty body.
Put the rows in `data`, or author a component that does read `bind` (`list`, `tree-view`,
or an `object-*` widget — they call `useDataScope`). (objectui#6575)

The core sentence is the ruling's own, and it is the corpus phrasing rather than a third
one. skills/objectui/rules/protocol.md:162 already teaches: "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."
The diagnostic reuses
"does not read bind", "reads its rows from the inline data array 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 data and
bind; that table is not empty, so the "empty body" clause would be false there. The
consequence 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; the bind
contributes nothing"
. This is the discipline columnSpellingDiagnostics.ts records in its
own 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 existing
pin — is extended, not weakened. Everything it asserted still stands: bind is still
not 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:

  • Fires: the retired bind form warns exactly once, and the line contains
    `bind: 'customers'` is ignored, the ruling's sentence, the empty-body consequence,
    and the way out.
  • Does not fire: the guide's taught inline-data node — same renderer, same helper, one
    bind key 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);):

stepobservation
HEAD blob of data-table.tsxd0a32a0a6734078217603228ae2a098766fd281d
injected marker / removed text, by anchored grep -c1 / 0
mutated blob94ded1c06b57e97416a626af5129a286ea588948 (differs, so the edit reached disk)
ablated runexit 1Test Files 1 failed | 1 passed (2), Tests 1 failed | 24 passed (25)
the single failureexpected [] to have a length of 1 on the firing assertion
restored blobd0a32a0a… — back to HEAD
restoration, by observationmarker count 0, warn line count 1, git diff HEAD empty

The 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_PATH from a trap with an absolute repo root, and is proven by
observation, never by an exit code.

packages/components/src/renderers/complex/__tests__/data-table-bind-diagnostic.test.ts
adds 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: ObjectDataTable stops forwarding a consumed bind

Found 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 the object-* widgets that
genuinely reads bind: const boundData = useDataScope(schema.bind) at line 554. It then
delegated with { ...schema, type: 'data-table', data: finalData, … }, spreading the
already-spent key into a component that reads no bind. A correctly authored, published-
guide-taught object-data-table would have tripped the new diagnostic on every render,
over rows that were on screen precisely because its bind had been honoured — the exact
"paint a configuration error over a working grid, which is worse than the silence it
replaces" failure columnSpellingDiagnostics.ts warns about.

Measured, not argued. The new pin was written first and run against the unfixed tree:

FAIL packages/plugin-dashboard/src/__tests__/ObjectDataTable.bindNotForwarded-6575.test.tsx
> leg 1: hands the inner data-table node no `bind` at all
AssertionError: expected true to be false
127| expect('bind' in inner).toBe(false);
Test Files 1 failed | 2 passed (3)

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 DashboardGridLayout already forwards in
that exact shape (const { data: _data, ...restOptions } = options). Three legs pin it,
because each alone admits a wrong fix — (1) no bind on the forwarded node, (2) the rows
still 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: RelatedList and ObjectGrid
build fresh nodes with no spread of the outer schema; DashboardRenderer and
DashboardGridLayout spread a widget options block on their static table branches,
where a bind really 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' on
packages/types/src/base.ts returns 0, and BaseSchema carries [key: string]: any.

So today an authored bind on a data-table is not type-visible — it rides the index
signature on the TS side and .passthrough() on the zod side, which is what makes the
silence 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 touches packages/components/**,
ObjectDataTable.tsx, and its own changeset. If anything, #6574 slightly raises the value of
this diagnostic — a centrally declared bind invites authors to expect "bindable
everywhere", 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 is
the 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 vitest invocation is refused by the
in-repo guard, #3378), serialized through the shared verify lock.

checkverdict line
vitest run over 98 affected filesTest Files 98 passed (98) · Tests 894 passed (894)
pnpm --filter @object-ui/components type-checkexit 0
pnpm --filter @object-ui/plugin-dashboard type-checkexit 0
pnpm --filter '@object-ui/plugin-dashboard^...' buildexit 0
check-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-bytesexit 0 (plus a direct control-byte scan of all seven touched files)
check:vi-mock-specifiersexit 0 — implicated by the new vi.mock in the plugin-dashboard pin
check:self-import, check:phantom-deps, check:esm-specifiers, check:entry-guard, check:skills-pathsexit 0
lint: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 tests
eslint . over the whole repoexit 0 — 3879 files, 0 errors; 0 warnings land inside any line this diff inserted (checked against the post-image hunk ranges)

check:readme-exports is NOT MEASURED, not red, and says so 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 and
public export lists — is untouched here: this PR adds no README and no package export
(describeIgnoredBind stays internal to packages/components, matching
columnSpellingDiagnostics.ts, which plugin-grid also does not export).

The first plugin-dashboard type-check of this branch reported
Cannot find module '@object-ui/components' — a precondition failure, not a red gate. It was
re-run properly after building the closure it named, and is the exit 0 in the table above.


Generated by Claude Code

`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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3238.5 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-BQEjHIR2.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)509.24KB115.61KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.43KB34.48KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.62KB54.56KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)9.30KB3.22KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-salesClaude

Copy link
Copy Markdown
Collaborator

✅ ACCEPT — flipped ready, auto-merge on

Reviewer: domain:ui @ objectui execution seat, PM session 8ca04858-ea8e-5b85-9182-de59aa49e00c. Verified against GitHub and the code tree, not against the report's self-description.

Gate reading: all 29 check runs read and the returned count compared against total_count: 29. 26 success, 3 skipped (dependabot and the two un-expanded coverage matrix templates), 0 failing, 0 still running. Ready flipped first, auto-merge second.

What I re-measured myself

claimmy independent checkverdict
"no behaviour change" — the renderer still does not read binduseDataScope in renderers/complex/data-table.tsx: 0, with list.tsx / tree-view.tsx as positive controls proving the pattern reaches renderers
the new diagnostic module does not become a useDataScope readerits 3 matches are all prose — docblock :17, :38, and the warning text at :143
ObjectDataTable stops forwarding the spent keyconst { bind: _consumedBind, ...schemaWithoutBind } = schema; and the spread moves from ...schema to ...schemaWithoutBind

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 missed

The bounded producer fix is the piece worth naming. ObjectDataTable resolves bind itself through useDataScope(schema.bind) and then delegated with { ...schema, type: 'data-table' } — handing the already-spent key to a component that reads no bind at all. Without that fix, the new diagnostic would fire on every render of a correctly authored, guide-taughtobject-data-table, printing a warning over rows that were on screen precisely because the bind had been honoured.

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 DashboardGridLayout's existing data handling — rather than absorbed as a consumer-side tolerance, and it is pinned red-before-green (expect('bind' in inner).toBe(false) failing with expected true to be false against the unfixed tree), with three legs so no wrong fix passes.

Two judgment calls I am ratifying

  • The precedent correction. The ruling names "packages/plugin-grid's column-spelling diagnostic, visibilityDiagnostic.ts" — which is two different files: visibilityDiagnostic.ts lives in packages/react/src/utils/, and plugin-grid's column-spelling diagnostic is columnSpellingDiagnostics.ts. The dev matched the latter and said so. That satisfies both halves of the phrase and is the nearer defect class; the ruling's intent was "do it like the existing diagnostic", and the ambiguity was in the ruling's own wording, not in the execution.
  • No dedupe Set.visibilityDiagnostic needs one because a node gate runs once per row; a data-table is one node rendered once, so the useEffect key already is the one-line-per-authoring-bug ceiling. Reasoned from the difference rather than copied from the precedent.

Card path drift, recorded rather than papered over

The card's reproduce block names renderers/data-display/data-table.tsx. That file does not exist — the renderer is at renderers/complex/data-table.tsx — so the card's quoted grep returned "no hits" for an absent path: a true conclusion reached by an unsound route. Re-measured on the real file with list.tsx (2 hits) as a positive control.

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 finding filed by the dev and labelled: #6665 — a ${...} expression authored in node-level data is not evaluated and renders an empty body silently. Same family as this card, one key over, so this card's bind-keyed diagnostic does not cover it. It is explicitly quoted from an older ref and names re-measuring as its own first step.


Generated by Claude Code

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@os-sales