fix(lint): report an empty measure selection on every widget family, not only charts - #15668
Conversation
…not only charts The pinned `@object-ui` renderer's `values.length === 0` return (`packages/plugin-dashboard/src/DatasetWidget.tsx:683` at `.objectui-sha` = `a472b07167a39e55491109e864bb5a54027dcfbd`) is type-independent and stands above `isMetric` (`:423`, `METRIC_TYPES` at `:343`), `isTable` (`:424`) and the chart branch alike. A `metric`, `kpi`, `gauge`, `solid-gauge`, `bullet`, `table` or `pivot` widget that selects no measures therefore renders the same authoring placeholder — the KPI number or the table is not drawn at all — and nothing reported it: `chart-measures-missing` was chart-family only, `table-count-only` requires `values.length > 0`, and the rules that iterate `dimensions[]`/`values[]` are silent on an empty array by construction. Adds `widget-measures-missing` for the non-chart declared families, with the same warning tier, the same per-widget `suppressWarnings` suppression and a message that states the consequence its family actually has. `chart-measures-missing` is left exactly as ruled — same id, same population, same wording — because it is reachable from the package barrel (a public-surface contract) and may already be written into a board's `suppressWarnings`. The dimensions arm stays chart-family only: a dimensionless `metric`/`table` is what those families are for. The population is derived, never hand-listed: `NON_CHART_DATASET_WIDGET_TYPES` is the `ChartTypeSchema` taxonomy minus `CHART_FAMILY_WIDGET_TYPES`, so the three sets stay a partition by construction. Mutual exclusion is the pin's own order — the measures check runs before the dimensions one, and rule (e) already skips an empty selection — pinned by tests that assert exactly one finding per widget. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 2 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 682b7e563a9413b3ecfcdbce8ba7857014ec3309 && git checkout 682b7e563a9413b3ecfcdbce8ba7857014ec3309
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fa125f3bfe434a615c846c0ed0e1ab01de386a17 4712be5d85f7d6fae7284051c8e8a7b3a5f69226 && git checkout -B drift-repro fa125f3bfe434a615c846c0ed0e1ab01de386a17 && git merge --no-ff 4712be5d85f7d6fae7284051c8e8a7b3a5f69226
node scripts/docs-audit/affected-docs.mjs --json fa125f3bfe434a615c846c0ed0e1ab01de386a17
|
|
ACCEPT — PM seat Reviewed against the three-dot diff at What the diff does, checked line by line
Measured / arbiter: Fences held: Flipping ready + enabling auto-merge once the two required jobs read Generated by Claude Code |
Fixes #15508
The ruling, and what landed
chart-measures-missing(#15462) reported the authoring placeholder for the chart familyonly. The return that produces it is type-independent. Read at the revision this repo pins
(
.objectui-sha=a472b07167a39e55491109e864bb5a54027dcfbd), inpackages/plugin-dashboard/src/DatasetWidget.tsx::683—if (values.length === 0)returnstt('dashboard.pickMeasures', 'Pick measures (values) for this dataset widget.');:343—const METRIC_TYPES = new Set(['metric', 'kpi', 'gauge', 'solid-gauge', 'bullet']);:423—const isMetric = METRIC_TYPES.has(widgetType) || dimensions.length === 0;:424—const isTable = widgetType === 'table' || widgetType === 'pivot';:683stands ABOVE:423/:424and above the chart branch, and tests nothing butvalues.length === 0. Premise confirmed at the pinned sha (the card quoted an older.objectui-sha; the four lines read identically ata472b0716). So ametric,kpi,gauge,solid-gauge,bullet,tableorpivotwidget selecting no measures rendersthe same placeholder — the KPI number or the table is not drawn at all — and nothing
reported it (
table-count-onlyneedsvalues.length > 0before it looks; the rules thatiterate
dimensions[]/values[]are silent on an empty array by construction).The measures-missing POPULATION is now every declared
ChartTypeSchemawidget type. Samewarningtier, same per-widgetsuppressWarningssuppression, same advises-never-gatesproperty.
chart-dimensions-missingstays CHART-FAMILY ONLY, unwidened: a dimensionlessmetric/tableis what those families are for.The id decision: kept
chart-measures-missing, addedwidget-measures-missingMeasured first, as the ruling required. "chart" does not name the condition once the
population is every family — a
metrictile is not a chart — so the new families needed afamily-neutral id. Whether the old id could be RETIRED turned on whether anything outside
the rule file depends on the string.
git grep -n "chart-measures-missing" -- packages content docs skillson
main(7087f99c1) returns four hits, two of them outsidevalidate-widget-bindings.*:packages/lint/src/index.tsis the package's published barrel — andrule-id-barrel-exports.test.tsmakes that reachability a public-surface contract, not aconvenience.
.changeset/chart-empty-selection-rules.mdis the release-note record of thePR that added the id. Renaming would have retired a reachable id and falsified an
already-merged PR's changeset; so the ruling's other branch applies:
chart-measures-missingis untouched — same id, same chart-family population, samemessage ("no chart is drawn at all"), same suppression. A board that already wrote
suppressWarnings: ['chart-measures-missing']keeps working, pinned by a test.widget-measures-missingis new, for the non-chart declared families, with the samemessage shape and the consequence each family actually has: the single-value families get
"the single KPI number this tile is for is not drawn at all",
table/pivotget "notable is rendered at all". Hint keeps "select at least one measure BY NAME (declared
measures: …)" plus the suppression tail; it does not steer toward a dimension (that
clause is chart-only and would be false advice on a
metric).One id could only have produced the per-family messages by branching internally anyway,
so the split costs nothing in message quality and keeps every reference true.
Population is derived, never hand-listed:
NON_CHART_DATASET_WIDGET_TYPESisChartTypeSchema.optionsminusCHART_FAMILY_WIDGET_TYPES, so the three sets stay apartition of the taxonomy by construction and a family added to the taxonomy or to either
exception set lands on exactly one side without a second edit. A widget
typeoutside thetaxonomy is still judged by none of the three ids.
Mutual exclusion and ordering are the pin's own, and unchanged: the measures check is
evaluated BEFORE
chart-dimensions-missing(now as the control flow itself —if (values.length === 0) { … } else if (dims.length === 0) { … }), and rule (e)table-count-onlystillcontinues onvalues.length === 0. Both non-overlaps are pinnedby "exactly one finding" tests.
Files
packages/lint/src/validate-widget-bindings.ts(#15508)paragraphpackages/lint/src/validate-widget-bindings.test.ts#15508block, the acceptance battery, and the three #15462 population pins re-stated for the widened shapepackages/lint/src/index.tsrule-id-barrel-exports.test.tsfails any exported rule id that no published barrel re-exports. Additive — no existing export renamed or removed..changeset/widget-measures-missing-every-family.md@objectstack/lint: minor— a new exported id constant is additive APIPopulation pins re-stated (they asserted the OLD, chart-family-only population, and are
where the widening is visible):
a single-value or tabular family with no measures is NOT the measures findingnow asserts the family-neutral id fires and the chart id does not;every chart family reports, and no other family doesbecameevery declared family reports the MEASURES shape; only the chart family reports the dimensions one, sweeping thewhole taxonomy and asserting each type reports exactly one of the two measures ids. The
CHART_FAMILY_WIDGET_TYPES ⊆ ChartTypeSchema/ exception-set-mirror pins are untouched andstill green; the new block adds the partition assertion for the other side.
Verification
Exit codes captured BEFORE any pipe (
cmd > file 2>&1; EXIT=$?). Heavy runs serialisedthrough
OS_VERIFY_LOCK_SLOT=issue-15508 scripts/pm/os-verify-lock.sh. Head at every runbelow:
4712be5d8.Build (dependency closure — the tests import
@objectstack/spec/uithrough itsexports):Tests:
Ablation — the population widening, reverted
Trap-guarded (
trap 'git -C "$REPO_ROOT" checkout HEAD -- "$TARGET"' EXIT INT TERM,absolute paths, run from a COMMITTED implementation so the restore leg has a real
reference point). No dist rebuild is involved and none is owed: the test imports the
rule through the relative specifier
./validate-widget-bindings.js, i.e.src/, andpackages/lint/vitest.config.tssets one unrelated option (disableConsoleIntercept) andno alias — so the mutation is the code under measurement. Mutation: the emission's
} else if (isNonChartDatasetWidgetType(w.type)) {replaced by afalsearm, i.e. thechart-family-only population restored.
Mutation proven on disk by grep counts in BOTH directions plus the blob:
Result — RED, in the predicted direction (the new families' cases fail; the chart-family
cases stay green, which is the arm the mutation does not touch):
Restore proven by state, not by exit code:
Green again on the restored tree:
Derived gate family
node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack(exit 0) at
4712be5d8, over its own change set (4 paths, three-dot vs merge base7087f99c1) — 52 commands. All 52 run; 50 exit 0, 2 exit 3 (NOT MEASURED, below). Verdictlines from the two the dispatch named:
Changeset family, all exit 0:
check-changeset-no-major --base origin/main,check-empty-changeset --base origin/main,check:changeset-gate-self-tests,check:objectui-changeset, andcheck-changeset-fixed(run explicitly — the derivationmarks its roster ⛔ because it lives under
.changeset, one of my paths). The other three⛔-marked roster gates ran too:
check:authz-resolver,check:error-code-casing,check:filter-alias-parity— exit 0.NOT MEASURED (named, counted as neither green nor red):
pnpm check:dual-build-cjs-loads— exit 3,PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/(74+ packages). Needs a whole-repo build;CI does it.
pnpm check:type-check-debt— exit 3,check-type-check-coverage: PREREQUISITE NOT MET … --re-measure cannot run: 29 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk. Same cause. (Its siblingcheck:type-check-coverageran and is green.)
$RUNNER_TEMP,matrix.shard) — the derivation prints them as⊘ NOT MEASUREDitself; they have novalue outside a CI run.
Fixture radius
Swept every fixture that could feed the widened rule: repo-wide scan for a dataset-bound
widget of a non-chart family with no
values, plus every caller ofvalidateWidgetBindings(metadata-protocol publish-gate and drafts-closure boards,objectql protocol-meta, cli, and the lint suites). Three candidates, none needing a change:
packages/cli/test/authoring-rule-command-parity.test.tsfilters toseverity === 'error'so a new warning cannot move it;
packages/lint/src/validate-filter-tokens.test.tsandpackages/sdui-parser/**do not run this rule. Every shipped board(
system_overview, the showcase/CRM/todo examples) selects its measures, so first-partymetadata stays clean — pinned by the existing
system_overviewcase and by a new one thatreports a measureless CLONE of that tile.
Not flipped to ready, no auto-merge, not merged — the PM does that.
🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Generated by Claude Code