Skip to content

chart-measure-unknown errors on report/list/page chart series[].name with a message that names an empty series — at the pin it is a display-name override that lands on nothing #15575

Description

@claude

Found while implementing #15463 (PR #15571). Out of scope there — that card names one rule id (chart-field-unknown, dashboard chartConfig), and this is a different id on different surfaces — but it is the same measurement, applied one rule over.

The claim

packages/lint/src/validate-chart-bindings.ts runs measureRef over series[].name for all three surfaces it covers (:231, fed by :258 for report charts and :346-:353 for list charts and dataset-bound page chart components). At error tier its message reads:

"NAME" is not a measure declared by dataset "DS". Post-ADR-0021 result rows are keyed by MEASURE NAME (e.g. "sum_amount"), not the base field (e.g. "amount"), so this series comes back empty.

Read at the @object-ui revision this repo pins (.objectui-sha = 00d3f09c500c4a45b5f27aca8af80349412faaf1), that consequence looks wrong for the series[].name position specifically, because the renderer does not build the series from it:

  • packages/plugin-report/src/DatasetReportRenderer.tsx imports mergeAuthoredSeries and says why, in its own words: "mergeAuthoredSeries — not mergeAuthoredPresentation — because a report's chart.xAxis/chart.yAxis are bare dimension/measure NAME strings, i.e. pure data on this surface". So xAxis/yAxis really are the query binding on a report, and error is right for those two positions.
  • chart.series[] is not. The same file describes it as "The author's per-chart override for ONE measure's display name — the entry of chart.series[] whose name IS that measure", matched by name against the derived series. An entry matching none is ignored, exactly as @object-ui/core src/utils/chart-presentation.ts states for the dashboard's copy: "an authored entry naming a measure that is NOT in the dataset selection is ignored — membership belongs to the dataset".

So a series[].name naming an undeclared measure does not empty a series; it is a display-name override that lands on nothing. That is the ignored-key class, not a broken query — which is precisely what #15463 measured for chart-field-unknown and what the maintainer ruling there resolved by dropping those positions to warning and rewording the message.

Why this is a card and not a patch

The tier is a product judgement, the same one #15463 put to the PM rather than deciding in a PR, and the answer may differ per surface. Three things need measuring per surface before anything moves:

  1. Report charts (report.chart, report.blocks[].chart) — the evidence above is direct, and xAxis/yAxis should clearly keep error.
  2. List-view charts (ListChartConfigSchema) — not yet read at the pin; the binding may genuinely be the query there.
  3. Dataset-bound page chart components — note :346-:353 merges axisRefs and seriesRefs into one series array before :231 walks it, so axis positions on that surface currently take the series limb's message. Whether that is deliberate or an accident of the shape is worth settling in the same pass.

Also worth deciding: whether chart-axis-not-selected (warning, :203, "the query does not return it, so the series plots nothing") needs the same wording pass at the series[].name position.

What to decide

  1. Leave all three surfaces at error and correct only the consequence sentence for the series[].name position.
  2. Follow chart-field-unknown errors on chartConfig binding keys the pinned renderer REFUSES — its message names a query failure that never happens #15463: drop series[].name to warning on whichever surfaces the pin shows the key is presentation-only, reword to the true consequence, and leave xAxis/yAxis at error.
  3. Something else the per-surface measurement suggests.

No recommendation offered — surfaces 2 and 3 have not been read at the pin, and #15463's lesson is that the tier follows the measurement rather than the other way round.

Filed unassigned and unlabeled for triage — not started, no branch. packages/lint/src/validate-widget-bindings.ts (the #15463 file) is NOT involved.


Generated by Claude Code

Activity

  1. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    分诊 · domain:devx / priority:p2 / pm:queue

    Anchor read, not guessed. packages/lint/src/validate-chart-bindings.ts ⇒ domain:devx (the lint lane). Confirmed on origin/main f1d7872 (2026-09-05T00:21:28Z):

    validate-chart-bindings.ts:41    export const CHART_MEASURE_UNKNOWN = 'chart-measure-unknown';
    validate-chart-bindings.ts:181   const measureRef = (name, path, selected?) => {
    validate-chart-bindings.ts:230   if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
    validate-chart-bindings.ts:231   for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
    

    ⇒ yAxis and series[].name really do go through one measureRef, so they get one message. That is the mechanism, and it is why the consequence sentence cannot be true for both positions at once.

    ⭐ And the file already knows the risk: :33 — "…convention, which would make chart-measure-unknown's message a lie." The rule's own header worries about exactly this failure mode one case over.

    Grade — p2

    • Not p3. This fires at error tier, so it reds os validate / os build. If the key is presentation-only, an author is blocked from building over a display-name override that lands on nothing — and told the reason is "this series comes back empty", which sends them to debug a query that is fine. ⇒ Wrong tier and wrong explanation, on the loud channel.
    • Not p1: the author's remedy (remove or correct the series[].name entry) is obvious once found, nothing ships wrong, and no data moves.

    ⛔ But the grade is against the report charts case only — the card is explicit and I am not over-reading it

    The evidence at the pin (.objectui-sha = 00d3f09c500c…) is direct for report charts and quoted from objectui's own words: DatasetReportRenderer.tsx uses mergeAuthoredSeries "because a report's chart.xAxis/chart.yAxis are bare dimension/measure NAME strings, i.e. pure data on this surface" — so xAxis/yAxis are genuinely the query binding and error is right for those two positions — while chart.series[] is described as "the author's per-chart override for ONE measure's display name", and an entry matching nothing is ignored (@object-ui/core chart-presentation.ts: "membership belongs to the dataset").

    ⛔ Surfaces 2 and 3 have not been read at the pin:

    • List-view charts (ListChartConfigSchema) — the binding may genuinely be the query there.
    • Dataset-bound page chart components — ⚠️ and this one carries its own question: :346-:353 merges axisRefs and seriesRefs into one series array before :231 walks it, so axis positions on that surface currently take the series limb's message. ⇒ Whether that is deliberate or an accident of the shape must be settled in the same pass; if it is an accident, surface 3's axis positions are getting the wrong message today regardless of how the tier question resolves.

    ⇒ Deliverable 1 is the per-surface read at the pin, ⛔ not the tier change. Three surfaces, three answers possible.

    Boundary test — a tier change is a gate move, above the trivial line

    Dropping error → warning weakens a gate for the affected positions, which is on the manual-floor list. ⭐ But there is precedent that makes this cheap rather than open-ended: #15463 put exactly this question (for chart-field-unknown, dashboard chartConfig) to the PM rather than deciding it in a PR, and the maintainer ruling there dropped the presentation-only positions to warning and reworded the message. ⇒ Same question, one rule over; the ruler has already answered the general form.

    ⚠️ ⛔ That precedent does not decide this card — the card's own closing sentence is the rule to follow: "#15463's lesson is that the tier follows the measurement, rather than the other way round." ⇒ Measure surfaces 2 and 3, then apply the precedent per surface.

    Also in scope, so it is not a fourth card

    Whether chart-axis-not-selected (warning, :203, "the query does not return it, so the series plots nothing") needs the same wording pass at the series[].name position. ⚠️ Same shared-limb problem, lower tier. ⇒ Settle it in the same pass rather than leaving a second message with the same defect at warning.

    Scope fences — endorsed

    ⛔ Not #15463 (different rule id, different surfaces) and ⛔ packages/lint/src/validate-widget-bindings.ts (that card's file) is not involved. ⭐ Filing rather than folding in was right: same measurement, different rule, different tier decision.

    ⛔ Not a claim, not a dispatch — routing only.


    Generated by Claude Code

  2. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    Claim: PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk, 2026-09-05T03:54Z — dispatching to an os-dev subagent on branch claude/issue-15575-chart-measure-unknown-series-name (lock depth read before dispatch per #14944: lock free, queue empty → admitted; the assignee/label write landed a minute before this comment because the first attempt's comment step failed — same claim, same seat). Ruling: MEASUREMENT FIRST, per surface, at the CURRENT pin (.objectui-sha = a472b0716…, not the card's 00d3f09c5): for report charts, list-view charts and dataset-bound page chart components, read whether series[].name is the query binding or a presentation-only override at the pinned renderer; THEN apply the #15463 maintainer precedent per surface — presentation-only positions drop to warning with the true consequence ("a display-name override that lands on nothing"), query-binding positions (xAxis/yAxis on reports, and any surface where the read says the binding is the query) stay error. Settle in the same pass whether surface 3's axisRefs+seriesRefs merge (:346-:353) is deliberate, and give chart-axis-not-selected the same wording check at the series[].name position. A surface whose read is inconclusive keeps error and is reported. Hot files packages/lint/src/validate-chart-bindings.ts (+ its test) FREE; validate-widget-bindings.ts NOT involved. pm:queue → pm:dispatched, assignee set.


    Generated by Claude Code

  3. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 15575,
      "status": "done",
      "branch": "claude/issue-15575-chart-measure-unknown-series-name",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15741",
      "premise_still_valid": true,
      "summary": "Measurement first, at the CURRENT pin a472b0716 (not the card's 00d3f09c5), and all three surfaces came back conclusive. Report charts: `chart.xAxis`/`chart.yAxis` ARE the query (DatasetReportRenderer.tsx:859-867 `useDatasetRows(dataset, [xAxis], [yAxis], ...)`, and :326 'the embedded chart queries only chart.xAxis x chart.yAxis'), while `chart.series[]` is a display-name override merged by `mergeAuthoredSeries` (:1034), whose unmatched entry is 'ignored - membership belongs to the dataset' (core chart-presentation.ts:178-180, implemented as `derived.map(...)` at :208-211 so an unmatched entry cannot enter the output). List-view charts: `ListChartConfigSchema` (spec view.zod.ts:1256) is a strictObject of chartType/dataset/dimensions/values - there is NO series or yAxis position on that surface at all, and `values[]` is handed to the chart as the dataset measures (ObjectView.tsx:2103-2117), so it stays error with nothing to re-tier. Page chart components: ObjectChart queries `{dimensions: schema.dimensions, measures: schema.values}` (:551-556) and then REPLACES the authored series wholesale (`series: datasetChart.series`, :890-892, one entry per selected measure via buildChartSeries :681), and reads `yAxis` nowhere - so both `properties.series[].name` and `properties.yAxis[].field` are presentation. Applied the #15463 precedent per position: the three presentation positions drop error->warning with reworded consequences; every query position (report chart.yAxis, and values[] on all three surfaces) keeps error and its existing message verbatim. Merge question (:343-353) answered: reading both shapes on the page surface is deliberate, folding them into ONE `series` limb was an accident - it gave every yAxis[].field the SERIES message - so they are now separate limbs (`binding.axes` / `binding.series`) with different sentences, walk order and paths unchanged. chart-axis-not-selected: wording is now per position - verbatim at a query position, and at each presentation position it names the derivation instead of a query that does return the measure. PM addendum done: the hand-copied `asArray` is deleted and its 10 call sites re-pointed onto `recordsOf` (object-graph.ts:181), with a test that a null member in every collection this file reads is skipped rather than dereferenced. Changeset @objectstack/lint patch, tier drop stated as the user-visible behaviour change.",
      "tests": "All numbers from the final commit ef5f28aad (git rev-parse --short HEAD at the run). Every exit code captured BEFORE any pipe (cmd > log 2>&1; EXIT=$?); heavy runs through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-15575. (1) pnpm --filter '@objectstack/lint^...' build -> VERDICT command-exit 0, held 177s, waited 0s - closure built BEFORE any judgement. (2) pnpm --filter @objectstack/lint exec vitest run src/validate-chart-bindings.test.ts -> 'Test Files 1 passed (1)' / 'Tests 31 passed (31)'; VERDICT command-exit 0, held 16s, waited 487s. (3) pnpm --filter @objectstack/lint test -> 'Test Files 96 passed (96)' / 'Tests 3041 passed (3041)'; pnpm --filter @objectstack/lint typecheck -> 'check:test-typecheck: OK - @objectstack/lint's test layer compiles under packages/lint/tsconfig.test.json' (so the edited test file is MEASURED, not excluded); both under VERDICT command-exit 0, held 61s, waited 222s. (4) pnpm lint -> exit 0, the FULL repo scan (eslint . --no-inline-config), not narrowed. (5) ABLATION (trap-guarded, absolute paths): the test imports './validate-chart-bindings.js' relatively, so vitest resolves SOURCE not dist - NO rebuild leg on either leg. Reverting the tier to unconditional error, proven on disk before reading anything (PRE_orig_count=1/PRE_mut_count=0 -> POST_orig_count=0/POST_mut_count=1, hash 17f6f5bf -> b118bd3b): MUTATED_EXIT=1, 'Test Files 1 failed (1)' / 'Tests 8 failed | 23 passed (31)' - red in the expected direction. Restore proven by observation, not by an exit code: RESTORE_orig_count=1/RESTORE_mut_count=0, RESTORE_DIFF_HEAD=empty, RESTORE_HASH_MATCHES_HEAD_BLOB=yes (git hash-object == HEAD blob 17f6f5bf45c8ebadf030146cbbd6d5dd823cdce9), then RESTORED_EXIT=0, 'Tests 31 passed (31)'. (6) DERIVED FAMILY: node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack on the final head -> 53 commands from the real change set (3 paths, three-dot vs merge base 581d8f84c); ALL 53 RUN, 50 exit 0. (7) pnpm -s check:pm-dispatch-gates -> exit 0, 'dispatch-gates self-test: 1445 cases pass.'; pnpm check:nul-bytes -> exit 0, plus a direct grep -naP C0/DEL scan of the three changed paths -> no matches. NOT MEASURED, by name: check:dual-build-cjs-loads (exit 3, 'PREREQUISITE NOT MET - this gate reads built output, and some package has no dist/', 84 packages - needs a whole-repo build, CI's); check:type-check-debt (exit 3, 'PREREQUISITE NOT MET ... 30 workspace dependenc(ies) ... have no built type entry point on disk' - CI's, and no ledger number raised on it); check:docs-transcript-drift was exit 3 first (packages/lint/dist missing) and was RE-RUN after building @objectstack/lint -> exit 0, '4 declared transcript value(s) across 405 page(s) ... equal what the registry derives today' (measured); plus the 7 CI-variable families and the 36 artifact rosters the derivation itself scores outside the runnable total. The derivation printed a STALE TREE warning (branch behind origin/main; scripts/pm/check-half-states.mjs and scripts/pm/ensure-pm-labels.sh moved in that range) - re-derived after git fetch origin main, command list byte-identical, and check:pm-half-states (the family those two files define) ran exit 0.",
      "mcp_calls": "1 - one targeted search_issues dedup before filing #15734. Everything else went through repo-scoped REST (the probe passed; note GitHub's /search/issues is 403 in this session, 'sessions are bound to their configured repositories', which is why the dedup used the one permitted MCP call).",
      "open_questions": [
        {
          "question": "The brief asked for 'the same suppressWarnings mechanism as the family', but that mechanism does not exist on these three surfaces: suppressWarnings is declared on the dashboard WIDGET only (packages/spec/src/ui/dashboard.zod.ts:584). ReportChartSchema and ListChartConfigSchema are strict and do not carry it; the page props bag is untyped. Should the key be declared on them so the new advisories are individually silenceable?",
          "options": [
            "A - Ship as landed: the three positions are warning (non-gating, since consumers split on severity === 'error') but not individually suppressible; the hint says so rather than advertising a key that does not exist.",
            "B - Declare suppressWarnings on ReportChartSchema / ListChartConfigSchema / the object-chart props entry in a follow-up spec card, then wire the read here.",
            "C - Leave the surfaces alone and drop the finding entirely at presentation positions."
          ],
          "recommendation": "A now, B as a separate card if the PM wants parity with the dashboard surface. Declaring the key is a schema change, which is a stop condition on this card, and C loses a finding that is genuinely worth reporting - the author wrote a key and believes it is in force."
        }
      ],
      "out_of_scope_findings": [
        "filed as #15734: chart-axis-not-selected resolves a report chart against report.values, but at the pin the chart queries only [chart.yAxis] - so at the report chart.yAxis position it warns about a measure the chart DOES query, and at the report series position the deciding set is the singleton {chart.yAxis}. Wording corrected in this PR; changing WHICH findings fire is the decision that card carries.",
        "not filed, reported here: the page-component surface accepts an authored properties.series[] that ObjectChart discards wholesale even when it MATCHES a selected measure (:890-892 replaces the array rather than merging it, unlike the report surface's mergeAuthoredSeries). So a correct series[] entry on a dataset-bound page chart is inert too - a renderer-side gap, in objectui, not a lint rule this card could report."
      ]
    }

    Generated by Claude Code

  4. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    LANDED — PR #15741 merged as a51eb86b7. Card closed by Fixes.

    Probe on re-fetched origin/main (a9b9c7dc9), PM seat: packages/lint/src/validate-chart-bindings.ts imports recordsOf (:131), declares type MeasurePosition (:220) and UNKNOWN_CONSEQUENCE (:223); the only remaining asArray mention is the docblock at :125 describing the deleted copy; the test file carries 31 it( cases; changeset chart-measure-unknown-presentation-positions.md present. Lint vitest NOT MEASURED here (no deps in this checkout); both required jobs were success on ef5f28aad.

    Stripping pm:dispatched and the assignee in the same action. Frees validate-chart-bindings.ts for PR #15751's ledger fix (#15636).


    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

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions