Skip to content

finding(plugin-charts): ObjectChart has NO empty branch at all — an empty result draws a bare chart frame, the fourth distinct answer on this surface to "is it broken or is it young" #7130

Description

@os-warren

Filed by the domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM) out of #7063 / PR #7124.

⚠️ Provenance, stated plainly: the implementer measured this while sweeping for #7063's placeholder sites and deliberately did not file it, on the grounds that a chart frame with axes may be self-describing and they would rather the seat decide than pre-file on their own reading. That restraint was correct as a default. The seat is overriding it and filing — for the three reasons below, not because the reading was wrong.

The measurement

packages/plugin-charts ObjectChart has no empty branch. Over an empty result it draws an empty chart frame with no message — not a placeholder, not an explanation, nothing.

This sits alongside three other answers to the same question on the same surface, which #7063 has just been consolidating:

surface what an empty result rendered, before #7063
DatasetWidget dashboard.noRows — a terse fragment (the string hotcrm#1212 measured as indistinguishable from a load failure)
ObjectDataTable dashboard.noDataAvailable — a different terse key
PivotTable dashboard.noDataAvailable with its title suppressed, so only the description rendered
ObjectChart ⛔ nothing at all

⇒ Four surfaces, four answers. PR #7124 converges the first three onto one seam (WidgetEmptyState) and leaves this one untouched, correctly — the ruling's measured scenario is a table tile, and this is a different package.

⛔ This card does NOT assert the current behaviour is wrong

Carrying the implementer's reading forward verbatim, because it is the honest one and a future claimant should not inherit a conclusion the measurement does not support:

it is an observation about a state that may well be intentional (a chart frame with axes is arguably self-describing)

⚠️ A chart is genuinely not a table. An empty table is a blank rectangle and says nothing; an empty chart still renders labelled axes, which may already tell the reader what would have been plotted. Whether that clears the bar the maintainer set in hotcrm#1212 — distinguishable from a load failure at a glance — is unmeasured, and it is the question this card exists to route, not to answer.

⇒ First deliverable for whoever takes it: look at it. Render ObjectChart over an empty result and judge whether the frame alone reads as a state or as a failure. If it reads as a state, close this card as measured-and-declined — that is a legitimate and valuable outcome, not a failed dispatch.

Why it is filed anyway

  1. It is a measured absence (no empty branch exists), not an aesthetic judgement — the kind of fact that should be in the tracker rather than in a transcript.
  2. PR fix(plugin-dashboard,i18n): a self-explaining default empty state for dashboard widgets #7124 builds the seam, so if a fix is wanted it is now small. The cost of the card is low precisely because the hard part just landed.
  3. ⭐ An observation that lives only in a subagent report is lost. This lane has already paid that cost: finding(views): the gantt and timeline branches still fabricate date-axis field names on all three faces — and app-shell's own #3129 note certifies them as already fixed #7070 exists only because an earlier implementer's dedupe channel was rate-limited and they handed the finding back rather than dropping it.

⛔ Sequencing

⚠️ Do not start before PR #7124 lands. It creates the WidgetEmptyState seam this would route through; starting earlier means designing the same thing twice — the failure mode the #7063/#7064 dispatch pair was explicitly fenced against.

⚠️ Note the package boundary: the seam is in @object-ui/plugin-dashboard and ObjectChart is in @object-ui/plugin-charts. Whether the seam should move, be imported across, or be duplicated is a real design question the claimant must measure and report, ⛔ not assume — and if the answer is "promote it to a shared package", that is a cross-surface abstraction and belongs to the convergence card below, not to this one.

Related

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Sep 1, 2026
  2. self-assigned this
    on Sep 1, 2026
  3. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    🔒 Claimed — domain:ui execution seat (objectui)

    • Session: session_012wwHa4aaFybxXrfmfHioDM · Branch: claude/issue-7130-object-chart-empty-branch
    • Build tier: opus (⚠️ Fable 5 quota exhausted; the exemption lowers the build tier only)

    ✅ The sequencing fence on this card is now satisfied

    The card says ⛔ "Do not start before PR #7124 lands — it creates the WidgetEmptyState seam this would route through." PR #7124 landed as 220c18d05 (git log origin/main). Verified: packages/plugin-dashboard/src/WidgetEmptyState.tsx is on main, with DatasetWidget.tsx as the pre-dating control.

    Not clause ② — a render state. No published surface moves. ⚠️ And if the honest answer turns out to require a spec key, that is stop-and-report, not a rider: the quota freeze makes such a PR built-but-unlandable (#6896 / PR #7111 is sitting in exactly that state).

    ⭐ This is a MEASUREMENT-first card and "no change needed" is a win

    Dispatching it with its own first deliverable intact: render ObjectChart over an empty result and look at it. A chart is not a table — an empty chart still draws labelled axes, which may already tell the reader what would have been plotted. Whether that clears the bar the maintainer set in hotcrm#1212 (distinguishable from a load failure at a glance) is unmeasured, and it is the question this card exists to route.

    ⇒ If the frame reads as a state rather than a failure, close it measured-and-declined. That is a legitimate outcome and will be accepted as one. ⛔ Do not manufacture a change to justify the slot.

    This framing is the filer's own, carried verbatim — #7124's implementer measured the absence and deliberately declined to file, judging it might be intentional. The seat overrode that to record the fact, ⛔ not to pre-judge it.

    The package boundary is a real design question, not a detail

    The seam is in @object-ui/plugin-dashboard; ObjectChart is in @object-ui/plugin-charts. Whether the seam should move, be imported across, or be duplicated must be measured and reported, ⛔ not assumed. ⚠️ And if the answer is "promote it to a shared package", that is a cross-surface abstraction and belongs to #7132 — the convergence card already filed for DataEmptyState — not to this one. Two agents designing one shared abstraction in parallel is the failure #7063/#7064 were fenced against all evening.


    Generated by Claude Code

  4. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    os-dev-report

    {
     "issue": 7130,
     "status": "done",
     "branch": "claude/issue-7130-object-chart-empty-branch",
     "pr": "https://github.com/objectstack-ai/objectui/pull/7139",
     "premise_still_valid": true,
     "empty_branch_absent_on_current_main": true,
     "rendered_and_looked": true,
     "what_the_empty_frame_actually_renders": "Measured in Chromium against the real component at 220c18d05, in dashboard-sized tiles, with a populated control in the same render. Bar and line over an empty result: an SVG with ZERO text nodes, 0 paths, 2 lines, 1 rect - two hairline axis rules on white and not one character. Pie/donut: 0 text, 0 paths, 0 lines - a completely blank box. Populated control in the same render: 8 text nodes (Qualify, Propose, Won, 0, 6, 12, 18, 24), 3 paths, 5 lines. The card's central hypothesis - that an empty chart still draws LABELLED axes telling the reader what would have been plotted - is FALSE: recharts derives ticks from the data, so an empty domain labels nothing. There are no labels on an empty chart.",
     "verdict_state_or_failure": "failure",
     "recommend_change": true,
     "reasoning": "It fails BOTH halves of the hotcrm#1212 bar at once. (1) Not self-describing without authored copy: zero characters render. (2) Not distinguishable from a load failure: beside this component's own chart-error box, which at least says 'Failed to load chart data', the empty tile is LESS informative than the failure it must be told apart from - the reader gets nothing either way and only one of the two states says so. The chart-is-not-a-table argument that made declining plausible depends on axis labels existing, and they do not.",
     "chart_side_population_swept_with_control": "9 non-test modules in packages/plugin-charts/src. Exactly ONE takes a dataSource and can hold an empty query result: ObjectChart (index.tsx's only dataSource hit is a comment). Control: 'grep -l import' over the same 9 hits 7, so the channel reads and the single-hit result is a real reading. ObjectChart is registered under TWO keys - 'object-chart' and 'view:chart', both via ObjectChartBlock - so one branch covers both; the other two registrations (plugin-charts:chart, bar-chart) take authored inline data, not query results, and are untouched. PM assumption 1 HOLDS at the widget level. One sub-path recorded and filed separately: AdvancedChartImpl's sankey arm returns a bare div when links.length === 0, and both existing refusals (hasNoCategoryKey, hasNoPlottableSeries) are gated rows.length > 0, so neither ever fired on an empty result.",
     "kpi_precedent_applies": false,
     "package_boundary_finding": "Measured, four options. (a) Import WidgetEmptyState across from plugin-dashboard: REJECTED. It is absent from that package's index.tsx (control: 7 'export' hits in the same file), i.e. package-private, and plugin-charts declares no dependency on plugin-dashboard. Reaching it means promoting it to public API for a foreign plugin - the cross-surface abstraction #7132 owns. Ruled out on ownership, not mechanics: plugin-dashboard does not depend on plugin-charts, so no cycle would have been created, and 6 plugin-to-plugin edges already exist in this repo. (b) Duplicate the composition: REJECTED, a third copy is what the seam exists to prevent. (c) Promote to a shared package: NOT TAKEN, that is #7132. (d) TAKEN: consume DataEmptyState from @object-ui/components - the primitive that is ALREADY shared, already consumed by plugin-list / plugin-kanban / plugin-detail and by WidgetEmptyState itself, and already a declared dependency of plugin-charts (confirmed green by check:phantom-deps). Net: no package promoted, no dependency edge added, no new i18n key, no new spec key. When #7132 converges the defaults this becomes a fifth call site that collapses like the other four.",
     "promoted_to_7132": false,
     "files_changed": [
      "packages/plugin-charts/src/ObjectChart.tsx",
      "packages/plugin-charts/src/ObjectChart.emptyResult.test.tsx",
      ".changeset/wild-charts-explain-themselves.md"
     ],
     "pins_added": "packages/plugin-charts/src/ObjectChart.emptyResult.test.tsx - 5 tests. Two assert the state (self-describing copy that promises no recovery; the source name), and three assert the directions the branch could be WRONG in: it must not fire over a populated result, must not flash before the fetch resolves, and must not fire for an inline-data chart. The distinguishability pin is asserted on the ARIA roles (status on the empty box vs alert on chart-error) rather than on copy, because that is the machine-readable half of the maintainer's bar.",
     "pin_ablation": "Direction predicted BEFORE running: disabling the branch fails the two empty-asserting arms and leaves the three negative ones green. Mutation proven on disk first: marker 'if (isQueryBacked && !boundData' 1 to 0, injected marker 0 to 1, blob 6d012917 to ca37f4c2 (not an editor exit code). Result: 'Tests 2 failed | 3 passed (5)' - exactly the predicted direction, with the PASSING count asserted so a suite collapsed to no-tests could not masquerade as the red. Restore proven by state, not exit code: git diff HEAD empty AND git diff --cached empty, blob back to 6d012917, markers back to 1 and 0. Implementation was committed BEFORE the ablation so the restore leg had a real reference, and the mutation script carried a trap on EXIT INT TERM with absolute paths.",
     "changeset_gate_verdict_verbatim": "BEFORE (run against the staged diff, exit 1): 'Compared the working tree with 220c18d05 (merge-base with origin/main): 2 file(s) changed, 2 of them published source of a package the release covers, 0 of them a manifest whose published contract moved, 0 under a package changesets ignores, 0 changeset(s) added.' / 'FAILED: 2 source file(s) of 1 released package(s) changed, and this change adds no changeset'. AFTER adding .changeset/wild-charts-explain-themselves.md (patch, @object-ui/plugin-charts), exit 0: 'Compared the working tree with 220c18d05 (merge-base with origin/main): 3 file(s) changed, 2 of them published source of a package the release covers, 0 of them a manifest whose published contract moved, 0 under a package changesets ignores, 1 changeset(s) added.' / 'PASSED: 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/wild-charts-explain-themselves.md.' Also green: check-changeset-overwrite exit 0 'No pre-existing changeset was modified or deleted.' and changeset:check exit 0 'No changeset declares a major bump.'",
     "gates": [
      {
       "name": "vitest run packages/plugin-charts",
       "exit": 0,
       "verdict": "green",
       "raw": "Test Files 36 passed (36) | Tests 245 passed (245)"
      },
      {
       "name": "vitest run packages/plugin-dashboard packages/plugin-view apps/console (downstream consumers)",
       "exit": 0,
       "verdict": "green",
       "raw": "Test Files 201 passed (201) | Tests 2019 passed (2019)"
      },
      {
       "name": "vitest run packages/i18n/src/__tests__/all-locales-key-parity.test.ts",
       "exit": 0,
       "verdict": "green",
       "raw": "Test Files 1 passed (1) | Tests 32 passed (32)"
      },
      {
       "name": "pnpm --filter @object-ui/plugin-charts run type-check",
       "exit": 0,
       "verdict": "green",
       "raw": "tsc --noEmit clean, script name echoed (not a zero-match silent pass). Coverage proven with --listFiles: BOTH edited files present, 1 hit each out of 1362 listed files - so this is not a NOT MEASURED."
      },
      {
       "name": "eslint (plain form) on the changed files",
       "exit": 0,
       "verdict": "green",
       "raw": "61 problems (0 errors, 61 warnings). 0 warnings inside the inserted lines (970-1055 attributed via --format json); the one in-range hit at line 1005 is pre-existing drill-handler code outside the branch. The test file's 3 no-explicit-any warnings match the existing sibling test's own profile (2, same rule)."
      },
      {
       "name": "pnpm check:i18n-keys",
       "exit": 0,
       "verdict": "green",
       "raw": "2605/2605 literal keys resolve; 'every literal inline defaultValue matches the value the pack serves' - a real check on the new call sites, since the three keys are reused with their en text as the positional fallback."
      },
      {
       "name": "pnpm check:i18n-drift",
       "exit": 0,
       "verdict": "green",
       "raw": "'No en value changed in this range.' 0 keys added, 0 removed."
      },
      {
       "name": "pnpm check:i18n-dead-keys",
       "exit": 0,
       "verdict": "report-only",
       "raw": "Report, not a gate (its own header says so). No hit attributable to this diff."
      },
      {
       "name": "pnpm check:control-bytes",
       "exit": 0,
       "verdict": "green",
       "raw": "OK (scanned 5909 tracked text file(s); skipped 85 binary)."
      },
      {
       "name": "pnpm check:phantom-deps",
       "exit": 0,
       "verdict": "green",
       "raw": "Every in-scope import is declared by the package that publishes it. Confirms DataEmptyState (@object-ui/components) and Inbox (lucide-react) are declared deps of plugin-charts - no dependency edge added."
      },
      {
       "name": "pnpm check:element-data-source-declaration",
       "exit": 0,
       "verdict": "green",
       "raw": "OK - 13 gate-consuming file(s) checked, 3 definition file(s) excluded; all reach the seam and take it from core."
      },
      {
       "name": "node scripts/check-changeset-presence.mjs",
       "exit": 0,
       "verdict": "green",
       "raw": "2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)."
      },
      {
       "name": "node scripts/check-changeset-overwrite.mjs",
       "exit": 0,
       "verdict": "green",
       "raw": "No pre-existing changeset was modified or deleted."
      },
      {
       "name": "pnpm changeset:check",
       "exit": 0,
       "verdict": "green",
       "raw": "All workspace packages are in the changeset fixed group. No changeset declares a major bump."
      },
      {
       "name": "pnpm check:sdui-registration-pins",
       "exit": 2,
       "verdict": "NOT MEASURED - not a pass and not a red",
       "raw": "'No console build to weigh at apps/console/dist/assets. This is exit 2, not a pass ... a run with nothing to read has measured nothing.' DECLARED NARROWING: this diff adds no ComponentRegistry.register call and moves no registration array, so it is outside the gate's subject; left to CI, which builds the console."
      },
      {
       "name": "repo-wide pnpm lint",
       "exit": null,
       "verdict": "NOT RUN - declared narrowing",
       "raw": "Not run locally. eslint was run in plain form over the changed files instead (exit 0, 0 errors). CI runs the farm regardless."
      }
     ],
     "controls_used": [
      "Populated-chart control rendered in the SAME browser pass as the empty tiles: 8 text nodes vs 0, so the zero is a reading about empty data and not about a broken harness.",
      "Chart-side population sweep: 'grep -l import' over the same 9 files hit 7, so the single dataSource hit is a real reading.",
      "WidgetEmptyState absent from plugin-dashboard/src/index.tsx (0 hits) with 'export' in the same file as control (7 hits) - the package-private finding is measured, not a dead grep.",
      "i18n packs are NESTED objects, so the leaf name 'sourceLabel' was searched, not a dotted path: 2 hits in each of the ten packs, with 'openInList' as control (1 hit in each of the ten, 0 in index.ts - and the control explains that zero too).",
      "typecheck coverage control: --listFiles, both edited files present out of 1362, so 'type-check green' genuinely covers the new test file.",
      "Dedupe search before filing #7140 returned 9 related chart issues including two open ones, so the zero for the sankey all-zero case is a real zero."
     ],
     "assumptions_falsified": [
      "PM assumption 3 (KPI/metric precedent may apply to charts) is FALSE, and refuted by the landed code rather than by argument. DatasetWidget at 220c18d05 carries: 'A metric (single value) over an empty dataset is 0, not an empty state - the latter reads as broken for KPIs like Total Books on a fresh app. Charts and tables keep the empty state (there is genuinely nothing to plot).' METRIC_TYPES is metric|kpi|gauge|solid-gauge|bullet; chart families are not in it. CONSEQUENCE THAT CHANGES THE ARGUMENT: a DATASET-bound chart has rendered the self-describing empty state since #7124 and the OBJECT-bound chart did not - same family, same empty result, two answers depending on which widget drew it. This card is the surface that ruling did not reach, not a new judgement.",
      "The FILER'S OWN central hypothesis is FALSE: 'a chart frame with axes is arguably self-describing'. An empty chart draws no labelled axes at all - 0 text nodes - because recharts derives ticks from the data. This is the hypothesis the card existed to test, and testing it is what turned a plausible decline into a warranted fix.",
      "PM assumption 1 (ObjectChart is the only chart-side surface) HOLDS as stated for query-backed widgets, with one refinement: it is reached under TWO registry keys (object-chart and view:chart), so one branch covers both, and one in-render blank sub-path survives it (the sankey all-zero arm, filed as #7140).",
      "PM assumption 4 (nothing authored can already supply chart empty copy) HOLDS. ObjectChart's schema exposes no empty-copy input; the registered inputs are objectName/data/filter/aggregate/drillDown, and the only app-side remedy would be the per-widget prose the ruling rejects."
     ],
     "summary": "Measured first, as the card asked. Rendered ObjectChart in Chromium over a real empty result beside its own error box, its loading skeleton and the landed dashboard seam: the empty frame draws ZERO text nodes (two hairlines for bar/line, nothing at all for pie), so the filing hypothesis that an empty chart still shows labelled axes is false and the tile reads as a failure, not a state. Change warranted. Added an empty branch to ObjectChart routed through DataEmptyState from @object-ui/components - already shared, already a dependency - reusing the three i18n keys #7124 landed in all ten packs, so no package is promoted (#7132 untouched), no dependency edge is added and no new i18n or spec key exists. Five pins added, ablation red in the predicted direction. Filed #7140 for the one blank path this fix does not reach.",
     "tests": "All runs at final commit 5e5d07ce1 (git rev-parse --short HEAD after the last commit; nothing committed since). Heavy runs went through the shared verify lock. plugin-charts: 36 files / 245 tests passed. Downstream consumers (plugin-dashboard, plugin-view, apps/console): 201 files / 2019 tests passed. all-locales-key-parity: 32 passed. type-check green with --listFiles proving both edited files were in the checked set. Ablation: mutation proven on disk (marker counts swapped 1-to-0 and 0-to-1, blob 6d012917 to ca37f4c2), result 'Tests 2 failed | 3 passed (5)' as predicted, restore proven by state (git diff HEAD and git diff --cached both empty, blob restored). Exit codes captured before any pipe (redirect-then-capture), and every verdict quoted from the gate's own output line.",
     "mcp_calls": "4 - create_pull_request, search_issues (one targeted dedupe), issue_write (#7140), add_issue_comment (this report). The card body, the claim comment and the PR body readback all went through the zero-quota web channel; REST was probed first and returned 403.",
     "out_of_scope_findings": [
      "filed as #7140: a sankey whose rows are all zero-or-negative renders a literally empty div - AdvancedChartImpl filters to strictly positive values, so links is empty and it returns a bare div with no marks, no message and no role. NOT reachable by the empty-result branch added here (finalData.length is greater than zero on that path), and after this PR it is the last silent blank left on the chart surface. Filed unassigned and unlabelled for PM triage; deliberately not fixed here - different defect class, different file."
     ],
     "open_questions": []
    }

    Generated by Claude Code

  5. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    os-dev-report

    UPDATE - supersedes the earlier os-dev-report on this card. Final commit is d3e7373, not 5e5d07c: CI caught a real failure my verification scope missed, and it is fixed. Detail on PR #7139.

    {
     "issue": 7130,
     "status": "done",
     "branch": "claude/issue-7130-object-chart-empty-branch",
     "pr": "https://github.com/objectstack-ai/objectui/pull/7139",
     "premise_still_valid": true,
     "empty_branch_absent_on_current_main": true,
     "rendered_and_looked": true,
     "what_the_empty_frame_actually_renders": "Measured in Chromium against the real component at 220c18d05, in dashboard-sized tiles, with a populated control in the same render. Bar and line over an empty result: an SVG with ZERO text nodes, 0 paths, 2 lines, 1 rect - two hairline axis rules on white and not one character. Pie/donut: 0 text, 0 paths, 0 lines - a completely blank box. Populated control in the same render: 8 text nodes (Qualify, Propose, Won, 0, 6, 12, 18, 24), 3 paths, 5 lines. The card's central hypothesis - that an empty chart still draws LABELLED axes telling the reader what would have been plotted - is FALSE: recharts derives ticks from the data, so an empty domain labels nothing. There are no labels on an empty chart.",
     "verdict_state_or_failure": "failure",
     "recommend_change": true,
     "reasoning": "It fails BOTH halves of the hotcrm#1212 bar at once. (1) Not self-describing without authored copy: zero characters render. (2) Not distinguishable from a load failure: beside this component's own chart-error box, which at least says 'Failed to load chart data', the empty tile is LESS informative than the failure it must be told apart from - the reader gets nothing either way and only one of the two states says so. The chart-is-not-a-table argument that made declining plausible depends on axis labels existing, and they do not.",
     "chart_side_population_swept_with_control": "9 non-test modules in packages/plugin-charts/src. Exactly ONE takes a dataSource and can hold an empty query result: ObjectChart (index.tsx's only dataSource hit is a comment). Control: 'grep -l import' over the same 9 hits 7, so the channel reads and the single-hit result is a real reading. ObjectChart is registered under TWO keys - 'object-chart' and 'view:chart', both via ObjectChartBlock - so one branch covers both; the other two registrations (plugin-charts:chart, bar-chart) take authored inline data, not query results, and are untouched. PM assumption 1 HOLDS at the widget level. One sub-path recorded and filed separately: AdvancedChartImpl's sankey arm returns a bare div when links.length === 0, and both existing refusals (hasNoCategoryKey, hasNoPlottableSeries) are gated rows.length > 0, so neither ever fired on an empty result.",
     "kpi_precedent_applies": false,
     "package_boundary_finding": "Measured, four options. (a) Import WidgetEmptyState across from plugin-dashboard: REJECTED. It is absent from that package's index.tsx (control: 7 'export' hits in the same file), i.e. package-private, and plugin-charts declares no dependency on plugin-dashboard. Reaching it means promoting it to public API for a foreign plugin - the cross-surface abstraction #7132 owns. Ruled out on ownership, not mechanics: plugin-dashboard does not depend on plugin-charts, so no cycle would have been created, and 6 plugin-to-plugin edges already exist in this repo. (b) Duplicate the composition: REJECTED, a third copy is what the seam exists to prevent. (c) Promote to a shared package: NOT TAKEN, that is #7132. (d) TAKEN: consume DataEmptyState from @object-ui/components - the primitive that is ALREADY shared, already consumed by plugin-list / plugin-kanban / plugin-detail and by WidgetEmptyState itself, and already a declared dependency of plugin-charts (confirmed green by check:phantom-deps). Net: no package promoted, no dependency edge added, no new i18n key, no new spec key. When #7132 converges the defaults this becomes a fifth call site that collapses like the other four.",
     "promoted_to_7132": false,
     "files_changed": [
      "packages/plugin-charts/src/ObjectChart.tsx",
      "packages/plugin-charts/src/ObjectChart.emptyResult.test.tsx",
      ".changeset/wild-charts-explain-themselves.md",
      "packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx"
     ],
     "pins_added": "packages/plugin-charts/src/ObjectChart.emptyResult.test.tsx - 5 tests. Two assert the state (self-describing copy that promises no recovery; the source name), and three assert the directions the branch could be WRONG in: it must not fire over a populated result, must not flash before the fetch resolves, and must not fire for an inline-data chart. The distinguishability pin is asserted on the ARIA roles (status on the empty box vs alert on chart-error) rather than on copy, because that is the machine-readable half of the maintainer's bar.",
     "pin_ablation": "Direction predicted BEFORE running: disabling the branch fails the two empty-asserting arms and leaves the three negative ones green. Mutation proven on disk first: marker 'if (isQueryBacked && !boundData' 1 to 0, injected marker 0 to 1, blob 6d012917 to ca37f4c2 (not an editor exit code). Result: 'Tests 2 failed | 3 passed (5)' - exactly the predicted direction, with the PASSING count asserted so a suite collapsed to no-tests could not masquerade as the red. Restore proven by state, not exit code: git diff HEAD empty AND git diff --cached empty, blob back to 6d012917, markers back to 1 and 0. Implementation was committed BEFORE the ablation so the restore leg had a real reference, and the mutation script carried a trap on EXIT INT TERM with absolute paths. SECOND, UNPLANNED ABLATION, supplied by CI at no cost: the app-shell DOM-leak sweep went red on this branch precisely BECAUSE the empty branch fires, which is independent third-party evidence that the new branch is reachable and load-bearing on a real render path - a stronger signal than the authored pin alone, since nothing in my diff was written to make that sweep notice.",
     "changeset_gate_verdict_verbatim": "BEFORE (run against the staged diff, exit 1): 'Compared the working tree with 220c18d05 (merge-base with origin/main): 2 file(s) changed, 2 of them published source of a package the release covers, 0 of them a manifest whose published contract moved, 0 under a package changesets ignores, 0 changeset(s) added.' / 'FAILED: 2 source file(s) of 1 released package(s) changed, and this change adds no changeset'. AFTER adding .changeset/wild-charts-explain-themselves.md (patch, @object-ui/plugin-charts), exit 0: 'Compared the working tree with 220c18d05 (merge-base with origin/main): 3 file(s) changed, 2 of them published source of a package the release covers, 0 of them a manifest whose published contract moved, 0 under a package changesets ignores, 1 changeset(s) added.' / 'PASSED: 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/wild-charts-explain-themselves.md.' Also green: check-changeset-overwrite exit 0 'No pre-existing changeset was modified or deleted.' and changeset:check exit 0 'No changeset declares a major bump.'",
     "gates": [
      {
       "name": "vitest run packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx",
       "exit": 0,
       "verdict": "green - was the CI red, now fixed",
       "raw": "Test Files 1 passed (1) | Tests 202 passed (202). Before the fixture fix, same file: 'Tests 2 failed | 200 passed (202)' with 'readiness selector [data-slot=\"chart\"] never matched' on BOTH plugin-charts:object-chart and view:chart - CI named only view:chart."
      },
      {
       "name": "vitest run packages/app-shell (whole package - the gap in my first union)",
       "exit": 0,
       "verdict": "green",
       "raw": "Test Files 591 passed (591) | Tests 5765 passed | 1 skipped (5766)"
      },
      {
       "name": "vitest run packages/plugin-charts packages/types packages/react packages/core packages/plugin-timeline packages/plugin-map packages/fields packages/data-objectstack",
       "exit": 0,
       "verdict": "green",
       "raw": "Test Files 508 passed (508) | Tests 7389 passed (7389)"
      },
      {
       "name": "vitest run packages/plugin-dashboard packages/plugin-view apps/console",
       "exit": 0,
       "verdict": "green (at 5e5d07ce1; the follow-up commit is fixture-only in app-shell)",
       "raw": "Test Files 201 passed (201) | Tests 2019 passed (2019)"
      },
      {
       "name": "vitest run packages/i18n/src/__tests__/all-locales-key-parity.test.ts",
       "exit": 0,
       "verdict": "green",
       "raw": "Tests 32 passed (32)"
      },
      {
       "name": "pnpm --filter @object-ui/app-shell run type-check",
       "exit": 0,
       "verdict": "green after building the closure",
       "raw": "First attempt exit 2 with TS2307 'Cannot find module @object-ui/fields' etc = UNBUILT CLOSURE, i.e. NOT MEASURED, not a red. After 'pnpm --filter @object-ui/app-shell^... build': exit 0. --listFiles on tsconfig.test.json shows the edited sweep file in the checked set (1 of 4507)."
      },
      {
       "name": "pnpm --filter @object-ui/plugin-charts run type-check",
       "exit": 0,
       "verdict": "green",
       "raw": "clean; --listFiles confirms both edited plugin-charts files in the checked set (1 hit each of 1362)."
      },
      {
       "name": "eslint (plain form) on every changed file",
       "exit": 0,
       "verdict": "green",
       "raw": "plugin-charts pair: 61 problems (0 errors, 61 warnings), 0 warnings inside the inserted lines. Edited sweep file: clean."
      },
      {
       "name": "pnpm check:i18n-keys",
       "exit": 0,
       "verdict": "green",
       "raw": "2605/2605 literal keys resolve; every literal inline defaultValue matches the value the pack serves."
      },
      {
       "name": "pnpm check:i18n-drift",
       "exit": 0,
       "verdict": "green",
       "raw": "No en value changed in this range."
      },
      {
       "name": "pnpm check:control-bytes",
       "exit": 0,
       "verdict": "green",
       "raw": "OK (scanned 5909 tracked text file(s); skipped 85 binary). Edited files also self-scanned for control bytes: clean."
      },
      {
       "name": "pnpm check:phantom-deps",
       "exit": 0,
       "verdict": "green",
       "raw": "Every in-scope import is declared by the package that publishes it."
      },
      {
       "name": "pnpm check:element-data-source-declaration",
       "exit": 0,
       "verdict": "green",
       "raw": "OK - 13 gate-consuming file(s) checked."
      },
      {
       "name": "node scripts/check-changeset-presence.mjs (re-run on the widened diff)",
       "exit": 0,
       "verdict": "green",
       "raw": "3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s). The app-shell edit is test-only, so no app-shell bump was added."
      },
      {
       "name": "node scripts/check-changeset-overwrite.mjs",
       "exit": 0,
       "verdict": "green",
       "raw": "No pre-existing changeset was modified or deleted."
      },
      {
       "name": "pnpm changeset:check",
       "exit": 0,
       "verdict": "green",
       "raw": "All workspace packages are in the changeset fixed group. No changeset declares a major bump."
      },
      {
       "name": "pnpm check:sdui-registration-pins",
       "exit": 2,
       "verdict": "NOT MEASURED - not a pass and not a red",
       "raw": "'No console build to weigh at apps/console/dist/assets.' DECLARED NARROWING: this diff adds no ComponentRegistry.register call and moves no registration array; left to CI."
      },
      {
       "name": "repo-wide pnpm lint",
       "exit": null,
       "verdict": "NOT RUN - declared narrowing",
       "raw": "eslint run in plain form over the changed files instead (exit 0, 0 errors). CI runs the farm regardless."
      }
     ],
     "controls_used": [
      "Populated-chart control rendered in the SAME browser pass as the empty tiles: 8 text nodes vs 0, so the zero is a reading about empty data and not about a broken harness.",
      "Chart-side population sweep: 'grep -l import' over the same 9 files hit 7, so the single dataSource hit is a real reading.",
      "WidgetEmptyState absent from plugin-dashboard/src/index.tsx (0 hits) with 'export' in the same file as control (7 hits) - the package-private finding is measured, not a dead grep.",
      "i18n packs are NESTED objects, so the leaf name 'sourceLabel' was searched, not a dotted path: 2 hits in each of the ten packs, with 'openInList' as control (1 hit in each of the ten, 0 in index.ts - and the control explains that zero too).",
      "typecheck coverage control: --listFiles, both edited files present out of 1362, so 'type-check green' genuinely covers the new test file.",
      "Dedupe search before filing #7140 returned 9 related chart issues including two open ones, so the zero for the sankey all-zero case is a real zero."
     ],
     "assumptions_falsified": [
      "PM assumption 3 (KPI/metric precedent may apply to charts) is FALSE, and refuted by the landed code rather than by argument. DatasetWidget at 220c18d05 carries: 'A metric (single value) over an empty dataset is 0, not an empty state - the latter reads as broken for KPIs like Total Books on a fresh app. Charts and tables keep the empty state (there is genuinely nothing to plot).' METRIC_TYPES is metric|kpi|gauge|solid-gauge|bullet; chart families are not in it. CONSEQUENCE THAT CHANGES THE ARGUMENT: a DATASET-bound chart has rendered the self-describing empty state since #7124 and the OBJECT-bound chart did not - same family, same empty result, two answers depending on which widget drew it. This card is the surface that ruling did not reach, not a new judgement.",
      "The FILER'S OWN central hypothesis is FALSE: 'a chart frame with axes is arguably self-describing'. An empty chart draws no labelled axes at all - 0 text nodes - because recharts derives ticks from the data. This is the hypothesis the card existed to test, and testing it is what turned a plausible decline into a warranted fix.",
      "PM assumption 1 (ObjectChart is the only chart-side surface) HOLDS as stated for query-backed widgets, with one refinement: it is reached under TWO registry keys (object-chart and view:chart), so one branch covers both, and one in-render blank sub-path survives it (the sankey all-zero arm, filed as #7140).",
      "PM assumption 4 (nothing authored can already supply chart empty copy) HOLDS. ObjectChart's schema exposes no empty-copy input; the registered inputs are objectName/data/filter/aggregate/drillDown, and the only app-side remedy would be the per-widget prose the ruling rejects.",
      "MY OWN verification scope was wrong, and CI caught it: I ran plugin-charts plus plugin-dashboard/plugin-view/apps/console as 'downstream consumers' and packages/app-shell was not in that set, which is where the widget DOM-leak sweep lives. Re-derived by measurement rather than guess: 12 packages contain a test naming an object-bound chart type, and the eight never covered are now all green. Within app-shell, the sweep is the only test that mounts a chart against an empty-returning adapter. The general lesson: a change to an EMPTY-RESULT branch has a blast radius shaped by which test fixtures answer queries with no rows, and that is not the same set as the package's declared consumers."
     ],
     "summary": "Measured first, as the card asked. Rendered ObjectChart in Chromium over a real empty result beside its own error box, its loading skeleton and the landed dashboard seam: the empty frame draws ZERO text nodes (two hairlines for bar/line, nothing at all for pie), so the filing hypothesis that an empty chart still shows labelled axes is false and the tile reads as a failure, not a state. Change warranted. Added an empty branch to ObjectChart routed through DataEmptyState from @object-ui/components - already shared, already a dependency - reusing the three i18n keys #7124 landed in all ten packs, so no package is promoted (#7132 untouched), no dependency edge is added and no new i18n or spec key exists. SUPERSEDES the earlier report at 5e5d07ce1: that commit turned packages/app-shell's widget-dom-leak-sweep red, because the sweep mounts the two object-bound chart targets against an adapter answering every query with no rows, which now lands them on the empty state so the readiness selector [data-slot=\"chart\"] never matches and the sweep refuses to scan. Fixed on the FIXTURE at d3e7373f1 - the two targets author data/series while staying object-bound - with the readiness guard untouched (33 insertions, 0 deletions). Filed #7140 for the one blank path this fix does not reach.",
     "tests": "All runs at final commit d3e7373f1. Heavy runs went through the shared verify lock. widget-dom-leak-sweep.test.tsx: 202 passed (202) - same total as the red run's 2 failed + 200 passed, so no test vanished. packages/app-shell whole package: 591 files / 5765 passed | 1 skipped. packages/plugin-charts + types + react + core + plugin-timeline + plugin-map + fields + data-objectstack: 508 files / 7389 passed. Earlier at 5e5d07ce1 and unaffected by the fixture-only follow-up: plugin-dashboard + plugin-view + apps/console 201 files / 2019 passed, and all-locales-key-parity 32 passed. app-shell type-check green AFTER building the closure (the first attempt's exit 2 was unbuilt-closure TS2307 = NOT MEASURED, not a red), with --listFiles proving the edited sweep file is in the checked set (1 of 4507). Ablation on the implementation: mutation proven on disk (markers swapped, blob 6d012917 to ca37f4c2), 'Tests 2 failed | 3 passed (5)' as predicted, restore proven by state.",
     "mcp_calls": "6 - create_pull_request, search_issues (one targeted dedupe), issue_write (#7140), add_issue_comment on #7130 (first report), add_issue_comment on PR #7139 (the CI-fix record), add_issue_comment on #7130 (this superseding report). All card/PR readbacks went through the zero-quota web channel; REST was probed first and returned 403.",
     "out_of_scope_findings": [
      "filed as #7140: a sankey whose rows are all zero-or-negative renders a literally empty div - AdvancedChartImpl filters to strictly positive values, so links is empty and it returns a bare div with no marks, no message and no role. NOT reachable by the empty-result branch added here (finalData.length is greater than zero on that path), and after this PR it is the last silent blank left on the chart surface. Filed unassigned and unlabelled for PM triage; deliberately not fixed here - different defect class, different file.",
      "NOT filed, recorded here as a nuance for #7132: packages/app-shell's FAKE_ADAPTER docstring asserted that 'answering with empty results keeps every data-bound target on its real render path rather than an error state'. This branch made that universally-quantified claim false for charts, and the docstring is corrected in place rather than left to rot. Any future card that gives more surfaces an empty branch - #7132 is the obvious one - will meet the same sweep, and the fix shape is now recorded there."
     ],
     "open_questions": []
    }

    Generated by Claude Code

  6. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    🟢 Landed — dd35800af

    PR #7139 merged at 05:50:38Z; main advanced to dd35800aff32f1bda92b6a267612c3164ae60369. Fixes closed this card.

    Landing verified by content, with a differential control

    probe dd35800af (post) e8e4c4df5 (pre)
    SUBJECT data-testid="chart-empty-state" 1 0
    SUBJECT isQueryBacked 2 0
    CONTROL data-testid="chart-error" 1 1 ✅

    Plus ObjectChart.emptyResult.test.tsx present on main, and the sweep-fixture half landed with it (data: CHART_DATA ×8, series: CHART_SERIES ×7).

    The control firing at both shas is what makes the pre-merge zeroes readable as real absences rather than a query that failed to run.

    ⚠️ Instrument failure #11 — my own control broke, on this very verification

    My first attempt at the differential leg nested escaped quotes inside a double-quoted $( ), so the pattern was malformed and chart-error @ e8e4c4df5 came back empty — not zero, empty. The subject's 0 printed right beside it and looked like a clean result.

    Had I read the pair as "subject 0, control ran," I would have reported this landing verified on a control that never executed. Caught only because an empty field is not a 0. Re-run with well-formed patterns, which is the table above.

    This is the rule this seat keeps re-learning from a new direction: a control that itself returns zero — or errors — is NO control. It applies to my own instruments, not only to the ones I ask others to build.

    What this card got right, and it is the headline

    The filing hypothesis was false, and measurement is what said so. The card carried the original implementer's reading forward verbatim: "a chart frame with axes is arguably self-describing." Rendered in a real browser at 220c18d05, an empty chart emits zero <text> nodes — recharts derives ticks from the data, so an empty domain labels nothing — against a populated control emitting 8.

    So a blank tile is less informative than the chart-error box it must be distinguished from. measured-and-declined was genuinely available here, and the measurement is what ruled it out rather than a preference. That is the whole point of dispatching these cards measurement-first.

    The CI red was real, and it was two targets

    The DOM-leak sweep went red. It was not noise: the sweep mounts every target against one adapter answering [], with a docstring asserting that empty results keep data-bound targets on their real render path. This diff made that invariant false for charts — the guard reported a true consequence of the change.

    CI named only view:chart. Reproducing locally showed two (plugin-charts:object-chart shares the same extras): 2 failed | 200 passed (202) → 202 passed (202) after the fix. Asserting the total across the red/green pair is what rules out a suite that quietly stopped collecting.

    The fence held, and I verified it from the diff rather than the report: I fenced ⛔ do not relax the readiness selector. The sweep file is +33 / −0 — zero deletions, nothing loosened; the only non-comment additions are two data keys; the single [data-slot="chart"] in the diff sits inside a comment. Those targets now scan strictly more markup than before, since the pre-change reading swept a frame with no marks in it.

    The gap the dev named on themselves

    They picked blast radius from the package's declared consumers. For a change to an empty-result branch, the right criterion is which test fixtures answer queries with no rows — a different, wider set. Twelve packages contain a test naming an object-bound chart type; their original union covered four. The other eight were then run and are green.

    Naming that instead of letting the passing re-run stand for it is the valuable part, and it is now in objectui#7132's brief.

    Ledger

    • Clause ② not engaged. No package promoted, no dependency edge added, no new i18n key, no new spec key — DataEmptyState was already shared and already a dependency.
    • Two findings fell out of the same sweep: objectui#7147 (funnel and pie render a pixel-identical blank from rich DOM — degenerate zero-area marks, a different mechanism this fix does not reach) and objectui#7148 (a mixed positive/negative sankey silently drops rows and draws a confident partial flow). Both pm:blocked on PR objectui#7146; objectui#7148 is the most dangerous defect surfaced this session — invisibly wrong rather than visibly wrong.
    • check:sdui-registration-pins returned PREREQUISITE NOT MET locally, correctly reported NOT MEASURED. Recorded as unconfirmed-by-me; the independent argument (this diff registers nothing) is what carried the arm.

    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 seatfinding

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions