Skip to content

feat(lint): report a chart widget that selects no measures — or no dimensions (#15462) - #15507

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-15462-chart-empty-selection
Sep 4, 2026
Merged

feat(lint): report a chart widget that selects no measures — or no dimensions (#15462)#15507
baozhoutao merged 1 commit into
mainfrom
claude/issue-15462-chart-empty-selection

Conversation

@claude

@claude claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #15462

What was unreported

validateWidgetBindings said nothing about two dataset-bound chart shapes that the @object-ui revision this repo PINS visibly degrades. Everything below was read at that pin — .objectui-sha = 00d3f09c500c4a45b5f27aca8af80349412faaf1 — in packages/plugin-dashboard/src/DatasetWidget.tsx, not at objectui's origin/main:

  • :683if (values.length === 0) returns tt('dashboard.pickMeasures', 'Pick measures (values) for this dataset widget.'). It stands above every family branch, so a widget selecting no measures never reaches a chart, a table or a KPI number: nothing is drawn.
  • :423const isMetric = METRIC_TYPES.has(widgetType) || dimensions.length === 0;, with METRIC_TYPES at :343 = metric, kpi, gauge, solid-gauge, bullet; :424 is the tabular test (table/pivot); and the render branches (:702, :798, :854) route isMetric ? KPI : isTable ? table : chart. A dimensionless bar therefore draws as a single KPI number — worse than a blank tile, because the number is REAL and the missing chart reads as a design choice.

The two new ids

Both are new ids in the widget-binding family, both warning tier, both suppressible per widget with suppressWarnings:

  • chart-measures-missing — a chart-family widget selects no measures (values empty or absent). Its message names the placeholder string verbatim.
  • chart-dimensions-missing — a chart-family widget selects at least one measure but no dimensions. Its message quotes the isMetric expression, and its hint offers the honest alternative repair: declare the tile as metric/kpi so the type matches what renders.

Tier. The card left shape 1 open (error "looks right — the renderer draws nothing"); it lands as a warning because an empty selection is a work-in-progress state a build must tolerate, and error would gate the sys_metadata publish path on a half-authored widget. Pinned end-to-end rather than inferred from the constant: an acceptance block asserts both ids arrive on the advisory channel — never as errors — on validate AND build.

Not folded into chart-config-missing. Neither shape is caused by, nor repairable with, chartConfig (chart-field-unknown refuses a yAxis[].field naming anything the widget did not select, so chartConfig can never supply a measure values is missing). PR #15461 left them out for that reason and recorded the omission in that rule's docblock; that paragraph is now a pointer to these two ids instead.

"Chart family" is derived from the renderer, not hand-listed

CHART_FAMILY_WIDGET_TYPES = every declared ChartTypeSchema option that is neither a METRIC_WIDGET_TYPES member nor tabular — i.e. exactly what the renderer's own isMetric ? KPI : isTable ? table : chart routing leaves over. The two exception sets are this file's mirrors of the renderer's own (the same "MUST track objectui" relationship DATE_RANGE_DEFAULT_FIELD already carries one position up), so:

The two ids never both fire on one widget: with no measures the pin returns the placeholder at :683 and the isMetric branch is never reached, so the KPI consequence is not what that widget does. A widget missing both reports chart-measures-missing alone, and its hint names the missing dimension so one fix round closes both.

Tests

packages/lint/src/validate-widget-bindings.test.ts, 14 new cases: a positive case per shape (message pinned to the placeholder string / the isMetric expression), a suppressible case per shape, cross-suppression (an unrelated id suppresses neither), the both-empty ordering case, negatives per family (metric/kpi/gauge/solid-gauge/bullet/table/pivot report neither id, with either selection empty), a sweep over the whole taxonomy (every chart family reports, no other family does), a type outside the taxonomy reports nothing, the two membership checks, and the shipped system_overview KPI tile (a metric with a measure and no dimensions — shape 2's input, correctly silent). #15461's shipped system_overview chart-tile fixture is untouched and still reports zero.

Fixture pass on shipped metadata, with a control leg

validateWidgetBindings over 53 exported @objectstack/platform-objects objects + SystemOverviewDatasets (5) + SystemOverviewDashboard (9 widgets: 2 chart-family, 7 single-value/tabular) — the input #15461's fixture pass used. A zero is only a measurement if the harness can report a non-zero, so the same run adds a control board in the INPUT (no source is mutated):

INPUT objects=53 datasets=5 widgets=9
INPUT chart-family widgets=2 · single-value/tabular=7
FIXTURE-PASS findings=0
FIXTURE-PASS new-id findings=0
CONTROL new-id findings=2
  [chart-dimensions-missing] (warning) dashboards[1].widgets[0]  'bar' widget selects no dimensions ...
  [chart-measures-missing] (warning) dashboards[1].widgets[1]  'pie' widget selects no measures ...

Every in-repo example dashboard was read too (app-showcase chart-gallery / ops / revenue-pulse, app-crm pipeline, app-todo task): every chart-family widget there declares both a dimension and a measure, so none of them moves.

Verification (all at fe2948350; every exit code captured BEFORE any pipe)

  • pnpm --filter @objectstack/lint testTest Files 94 passed (94) · Tests 2927 passed (2927), EXIT=0
  • pnpm --filter @objectstack/lint typecheck — EXIT=0; check:test-typecheck: OK — @objectstack/lint's test layer compiles under packages/lint/tsconfig.test.json
  • Consumers of the rule, after building their dependency closures: pnpm --filter @objectstack/metadata-protocol --filter @objectstack/objectql testTest Files 161 passed | 2 skipped (163) / Tests 2375 passed, and Test Files 271 passed (271) / Tests 4649 passed (4649), EXIT=0
  • Derived gate family, node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack → 45 commands, all run: 43 EXIT=0, 2 EXIT=3 (NOT MEASURED, not a red) — check:dual-build-cjs-loads ("Run pnpm build first. This is NOT a pass: nothing was measured.") and check:type-check-debt, both because this worktree has no whole-repo dist. CI builds first and measures them there.
  • pnpm -s check:pm-dispatch-gates✓ dispatch-gates self-test: 1402 cases pass., EXIT=0
  • pnpm check:nul-bytescheck-nul-bytes: OK (scanned 7496 text file(s) -- 7496 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes)., EXIT=0
  • pnpm check:type-check-coveragecheck-type-check-coverage: OK — 75/79 workspace packages type-checked (plus the root), 4 in the DEBT ledger, EXIT=0
  • Whole-repo pnpm lint (eslint . --no-inline-config) — EXIT=0, no narrowing
  • Ratchet-family gates re-run on the final commit after the last content change, so the green above is a reading of THIS tree.

Scope

  • Changeset: @objectstack/lint minor — the repo's convention for a new rule id (widget-filter-field-unknown / widget-sortby-unselected shipped under Minor Changes in packages/lint/CHANGELOG.md).
  • Docs: content/docs and docs/qa were grepped for the widget-binding rule ids (chart-config-missing, table-count-only, widget-sortby-unselected, widget-measure-unknown, chart-field-unknown, ...). No page enumerates them — the only hits outside the rule file are package CHANGELOG.md release prose and content/docs/releases/, which a code PR never edits. Nothing to add.
  • No edit to packages/platform-objects/** (ADR-0072 D1): the rules are added, the metadata is untouched.
  • chart-field-unknown errors on chartConfig binding keys the pinned renderer REFUSES — its message names a query failure that never happens #15463 (chart-field-unknown tier) is out of scope here and stays open.

🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

…mensions (#15462)

`validateWidgetBindings` said nothing about two dataset-bound chart shapes that the
`@object-ui` revision this repo pins (`.objectui-sha` =
00d3f09c500c4a45b5f27aca8af80349412faaf1) visibly degrades. Both become new
warning-tier ids in the widget-binding family, suppressible per widget:

- `chart-measures-missing` — `DatasetWidget.tsx:683` returns the authoring
  placeholder "Pick measures (values) for this dataset widget." before any query
  runs, above every family branch, so no chart is drawn at all.
- `chart-dimensions-missing` — `DatasetWidget.tsx:423`
  (`const isMetric = METRIC_TYPES.has(widgetType) || dimensions.length === 0;`)
  routes a dimensionless chart to the single-value branch, so it renders a KPI
  number and the declared family is silently ignored.

Warning rather than error for both: an empty selection is a work-in-progress state
a build must tolerate, and erroring would gate the `sys_metadata` publish path on a
half-authored widget. Neither is folded into `chart-config-missing` — neither is
caused by, nor repairable with, `chartConfig` — and that rule's docblock now points
at these two ids instead of recording them as deliberately unreported.

"Chart family" is derived from the renderer's own routing rather than hand-listed:
every declared `ChartTypeSchema` option that is neither a `METRIC_TYPES` member nor
tabular. Membership of both mirrored sets is held to `ChartTypeSchema` by the tests,
as #14436 did for `MARK_MIXING_CHART_TYPES`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@github-actions github-actions Bot added the size/m label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 8 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via validateWidgetBindings (symbol, a top-level function))
  • content/docs/releases/v17.mdx (via validateWidgetBindings (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5023630b1788c497355ea6fa4194c444e35d578fpackageMentionDocs.

Which tree this was computed on

This run read content/docs from bf1ac0b0356b9c8a1e32f9f19fa9ec0118c4130a — the merge of head fe29483503de59cf5c5e8f97abe01e3f3e3d9bb3 into base 5023630b1788c497355ea6fa4194c444e35d578f, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin bf1ac0b0356b9c8a1e32f9f19fa9ec0118c4130a && git checkout bf1ac0b0356b9c8a1e32f9f19fa9ec0118c4130a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5023630b1788c497355ea6fa4194c444e35d578f fe29483503de59cf5c5e8f97abe01e3f3e3d9bb3 && git checkout -B drift-repro 5023630b1788c497355ea6fa4194c444e35d578f && git merge --no-ff fe29483503de59cf5c5e8f97abe01e3f3e3d9bb3

node scripts/docs-audit/affected-docs.mjs --json 5023630b1788c497355ea6fa4194c444e35d578f

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5023630b1788c497355ea6fa4194c444e35d578f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

Reviewed against the three-dot diff origin/main...claude/issue-15462-chart-empty-selection (merge base 40a44b91b, head fe2948350): 4 files, +430/−7, matching the PR file list; delivered under the ruling in claim 5544550223 (two NEW warning-tier, suppressible ids; not folded into chart-config-missing).

Fixes #15462 closes the card on merge. #15508 (the type-independent placeholder for single-value/tabular families) is graded on the card. Flipping ready and arming auto-merge now. Next on the rule file: #15463.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 4, 2026 19:15
@baozhoutao
baozhoutao enabled auto-merge September 4, 2026 19:16
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit 36a16d0 Sep 4, 2026
35 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-15462-chart-empty-selection branch September 4, 2026 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A dashboard chart widget that selects no measures — or no dimensions — degrades visibly in the renderer and no lint rule reports it

2 participants