Skip to content

fix(lint): report an empty measure selection on every widget family, not only charts - #15668

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-15508-widget-measures-missing-all-families
Sep 5, 2026
Merged

fix(lint): report an empty measure selection on every widget family, not only charts#15668
baozhoutao merged 1 commit into
mainfrom
claude/issue-15508-widget-measures-missing-all-families

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15508

The ruling, and what landed

chart-measures-missing (#15462) reported the authoring placeholder for the chart family
only. The return that produces it is type-independent. Read at the revision this repo pins
(.objectui-sha = a472b07167a39e55491109e864bb5a54027dcfbd), in
packages/plugin-dashboard/src/DatasetWidget.tsx:

  • :683if (values.length === 0) returns
    tt('dashboard.pickMeasures', 'Pick measures (values) for this dataset widget.');
  • :343const METRIC_TYPES = new Set(['metric', 'kpi', 'gauge', 'solid-gauge', 'bullet']);
  • :423const isMetric = METRIC_TYPES.has(widgetType) || dimensions.length === 0;
  • :424const isTable = widgetType === 'table' || widgetType === 'pivot';

:683 stands ABOVE :423/:424 and above the chart branch, and tests nothing but
values.length === 0. Premise confirmed at the pinned sha (the card quoted an older
.objectui-sha; the four lines read identically at a472b0716). So a metric, kpi,
gauge, solid-gauge, bullet, table or pivot widget selecting no measures renders
the same placeholder — the KPI number or the table is not drawn at all — and nothing
reported it (table-count-only needs values.length > 0 before it looks; the rules that
iterate dimensions[]/values[] are silent on an empty array by construction).

The measures-missing POPULATION is now every declared ChartTypeSchema widget type. Same
warning tier, same per-widget suppressWarnings suppression, same advises-never-gates
property. chart-dimensions-missing stays CHART-FAMILY ONLY, unwidened: a dimensionless
metric/table is what those families are for.

The id decision: kept chart-measures-missing, added widget-measures-missing

Measured first, as the ruling required. "chart" does not name the condition once the
population is every family — a metric tile is not a chart — so the new families needed a
family-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 skills
on main (7087f99c1) returns four hits, two of them outside validate-widget-bindings.*:

.changeset/chart-empty-selection-rules.md:11:- `chart-measures-missing` — a chart-family widget selects no measures (`values` empty or
packages/lint/src/index.ts:25:  CHART_MEASURES_MISSING,
packages/lint/src/validate-widget-bindings.ts:92: * - `chart-measures-missing` — a chart-family widget selects NO measures
packages/lint/src/validate-widget-bindings.test.ts:1524:describe('chart-measures-missing / chart-dimensions-missing (#15462)', () => {

packages/lint/src/index.ts is the package's published barrel — and
rule-id-barrel-exports.test.ts makes that reachability a public-surface contract, not a
convenience. .changeset/chart-empty-selection-rules.md is the release-note record of the
PR 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-missing is untouched — same id, same chart-family population, same
    message ("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-missing is new, for the non-chart declared families, with the same
    message 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/pivot get "no
    table 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_TYPES is
ChartTypeSchema.options minus CHART_FAMILY_WIDGET_TYPES, so the three sets stay a
partition 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 type outside the
taxonomy 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-only still continues on values.length === 0. Both non-overlaps are pinned
by "exactly one finding" tests.

Files

File Why
packages/lint/src/validate-widget-bindings.ts new id + derived population + emission arm; docblock rule list and the "two empty-selection shapes" section updated with a (#15508) paragraph
packages/lint/src/validate-widget-bindings.test.ts new #15508 block, the acceptance battery, and the three #15462 population pins re-stated for the widened shape
packages/lint/src/index.ts one added barrel line. Not in the dispatch's hot-file fence, but not optional either: rule-id-barrel-exports.test.ts fails 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 API

Population 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 finding now asserts the family-neutral id fires and the chart id does not;
every chart family reports, and no other family does became every declared family reports the MEASURES shape; only the chart family reports the dimensions one, sweeping the
whole 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 and
still 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 serialised
through OS_VERIFY_LOCK_SLOT=issue-15508 scripts/pm/os-verify-lock.sh. Head at every run
below: 4712be5d8.

Build (dependency closure — the tests import @objectstack/spec/ui through its exports):

pnpm --filter '@objectstack/lint...' build
os-verify-lock: VERDICT command-exit 0 · held the lock 170s (2m50s) · waited 0s
packages/lint build: check-dts-emitted: @objectstack/lint - 4/4 declared declaration file(s) present.

Tests:

pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-widget-bindings.test.ts src/rule-id-barrel-exports.test.ts
 Test Files  2 passed (2)
      Tests  153 passed (153)
os-verify-lock: VERDICT command-exit 0 · held the lock 9s · waited 0s

pnpm --filter @objectstack/lint test
 Test Files  96 passed (96)
      Tests  3023 passed (3023)
os-verify-lock: VERDICT command-exit 0 · held the lock 45s · waited 0s

pnpm --filter @objectstack/lint typecheck
check:test-typecheck: OK — @objectstack/lint's test layer compiles under packages/lint/tsconfig.test.json
os-verify-lock: VERDICT command-exit 0 · held the lock 16s · waited 0s

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/, and
packages/lint/vitest.config.ts sets one unrelated option (disableConsoleIntercept) and
no alias — so the mutation is the code under measurement. Mutation: the emission's
} else if (isNonChartDatasetWidgetType(w.type)) { replaced by a false arm, i.e. the
chart-family-only population restored.

Mutation proven on disk by grep counts in BOTH directions plus the blob:

HEAD_BLOB=5ef6b84a45f25606332b73b7b19ee0ec8a48390f
BEFORE: original-predicate-lines=1 injected-lines=0
AFTER : original-predicate-lines=0 injected-lines=1
MUTATED_BLOB=2b39230da47fc9daa88cfbd6bfcfc0f51970db4c (differs from HEAD: yes)

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):

 Test Files  1 failed (1)
      Tests  11 failed | 138 passed (149)
os-verify-lock: VERDICT command-exit 1 · held the lock 9s · waited 0s

 FAIL … widget-measures-missing (#15508) > warns when a `metric` tile selects no measures, naming the KPI consequence
AssertionError: expected [] to have a length of 1 but got +0
 FAIL … widget-measures-missing (#15508) > warns when a `table` selects no measures, naming the TABLE consequence
 FAIL … widget-measures-missing (#15508) > a `metric` with NO measures and NO dimensions reports exactly one finding
 FAIL … widget-measures-missing (#15508) > a `table` with NO measures and NO dimensions reports once — not also table-count-only
 FAIL … [#15508] a single-value or tabular family with no measures reports the FAMILY-NEUTRAL id
 (11 failures total, every one naming a metric or tabular family)

Restore proven by state, not by exit code:

diff-empty=yes                    (git diff HEAD -- PATH)
worktree_blob=5ef6b84a45f25606332b73b7b19ee0ec8a48390f
head_blob=5ef6b84a45f25606332b73b7b19ee0ec8a48390f
injected-marker-count=0   original-predicate-lines=1

Green again on the restored tree:

 Test Files  1 passed (1)
      Tests  149 passed (149)
os-verify-lock: VERDICT command-exit 0 · held the lock 10s · waited 0s

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 base
7087f99c1) — 52 commands. All 52 run; 50 exit 0, 2 exit 3 (NOT MEASURED, below). Verdict
lines from the two the dispatch named:

check-nul-bytes: OK (scanned 7577 text file(s) -- 7577 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes).
✓ dispatch-gates self-test: 1445 cases pass.

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, and check-changeset-fixed (run explicitly — the derivation
marks 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 sibling check:type-check-coverage
    ran and is green.)
  • The 7 families whose argv takes a value from the workflow ($RUNNER_TEMP,
    matrix.shard) — the derivation prints them as ⊘ NOT MEASURED itself; they have no
    value outside a CI run.
  • The always-runs tail (workflows with no path filter) is CI's, not derived here.

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 of
validateWidgetBindings (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.ts filters to severity === 'error'
so a new warning cannot move it; packages/lint/src/validate-filter-tokens.test.ts and
packages/sdui-parser/** do not run this rule. Every shipped board
(system_overview, the showcase/CRM/todo examples) selects its measures, so first-party
metadata stays clean — pinned by the existing system_overview case and by a new one that
reports 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

…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
@github-actions github-actions Bot added the size/m label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 4 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 fa125f3bfe434a615c846c0ed0e1ab01de386a17packageMentionDocs.

Which tree this was computed on

This run read content/docs from 682b7e563a9413b3ecfcdbce8ba7857014ec3309 — the merge of head 4712be5d85f7d6fae7284051c8e8a7b3a5f69226 into base fa125f3bfe434a615c846c0ed0e1ab01de386a17, 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 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

⚠️ 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 fa125f3bfe434a615c846c0ed0e1ab01de386a17 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

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

Reviewed against the three-dot diff at 4712be5d8 (4 files, +348/−16), not the report. Closing keyword checked: first line Fixes #15508 and no other card number adjacent to a keyword.

What the diff does, checked line by line

  • packages/lint/src/validate-widget-bindings.ts: the (d1) arm is re-ordered so values.length === 0 is the OUTER test (the pin's DatasetWidget.tsx:683 order, cited at a472b0716), then family: chart family → chart-measures-missing unchanged (id, message, hint, suppression); non-chart declared types → new widget-measures-missing, message stating the family's own consequence (KPI number not drawn / no table rendered), same warning tier and per-widget suppressWarnings. chart-dimensions-missing stays chart-family only. NON_CHART_DATASET_WIDGET_TYPES is DERIVED as ChartTypeSchema.options − CHART_FAMILY_WIDGET_TYPES (a partition by construction, pinned by a test), and METRIC_WIDGET_TYPES/TABULAR_WIDGET_TYPES (:519/:529) are the existing renderer-set copies. Docblock updated with a "(A dashboard widget of ANY non-chart family that selects no measures renders the authoring placeholder, and no lint rule reports it #15508)" paragraph; the id decision is argued there: the old id is reachable from the published barrel and written into an already-merged changeset, so it was kept and the population split, not renamed — I verified the two references on origin/main (packages/lint/src/index.ts, .changeset/chart-empty-selection-rules.md).
  • packages/lint/src/index.ts: ONE added barrel line — outside the dispatch fence, required by rule-id-barrel-exports.test.ts (every exported rule id must be re-exported), declared in the PR body. Accepted on the four conditions (same defect class, mechanical, no other claim, same gate family).
  • Tests (+190): family-neutral id per family, the bar control unchanged, absent values key ≡ empty array, suppression independence between the two ids, exactly-one-finding for a measureless+dimensionless metric AND table (never also table-count-only), partition pin, a measureless clone of a shipped system_overview tile, and the "advises, never gates" battery extended to the new id.
  • .changeset/widget-measures-missing-every-family.md: @objectstack/lint: minor (new exported id = additive API), body names both ids and the pin lines.

Measured / arbiter: packages/lint imports typescript (absent in the PM checkout) — the suite is NOT MEASURED here; the dev's readings at this head: 149/149 in the rule file, 3023/3023 package-wide, typecheck OK, ablation red in the predicted direction (11 cases naming metric/tabular families, chart cases green), restore blob-proven. CI (Lint & Repo Gates, Type Check) is the arbiter — in progress at review time; flip waits for both required jobs.

Fences held: content/docs/releases/ untouched; governed-surface test check-governed-merges.mjs --test on the four paths → 0 of 4 path(s) hit the register; no other open PR holds these files; git merge-tree origin/main clean. #15575 (the chart-measure-unknown message card) is next on validate-widget-bindings.ts and waits for this to land.

Flipping ready + enabling auto-merge once the two required jobs read success. Fixes #15508 closes on merge.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 5, 2026 02:56
@baozhoutao
baozhoutao enabled auto-merge September 5, 2026 02:56
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit cd55558 Sep 5, 2026
35 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-15508-widget-measures-missing-all-families branch September 5, 2026 03:36
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 widget of ANY non-chart family that selects no measures renders the authoring placeholder, and no lint rule reports it

2 participants