Skip to content

fix(lint): chart series[].name advises instead of blocking — presentation, not a query binding - #15741

Merged
baozhoutao merged 3 commits into
mainfrom
claude/issue-15575-chart-measure-unknown-series-name
Sep 5, 2026
Merged

fix(lint): chart series[].name advises instead of blocking — presentation, not a query binding#15741
baozhoutao merged 3 commits into
mainfrom
claude/issue-15575-chart-measure-unknown-series-name

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15575

chart-measure-unknown fired at error on every measure position of the three chart surfaces validate-chart-bindings.ts covers, with one consequence sentence — "result rows are keyed by MEASURE NAME … so this series comes back empty". Deliverable 1 was the per-surface read at the CURRENT pin (.objectui-sha = a472b07167a39e55491109e864bb5a54027dcfbd, not the card's 00d3f09c5); the tier follows that read, per surface.

1. The per-surface read (renderer lines quoted at the pin)

Surface Position Query binding, or presentation? The line at the pin that decides it
Report charts chart.xAxis / chart.yAxis QUERY plugin-report/src/DatasetReportRenderer.tsx:859-867useDatasetRows(dataset, plan.kind === 'series' && xAxis ? [xAxis] : [], wantsQuery && yAxis ? [yAxis] : [], …), which posts queryDataset(dataset, { dimensions, measures, … }). Same file, scopeOrder docblock :326: "the embedded chart queries only chart.xAxis × chart.yAxis"
Report charts chart.series[].name PRESENTATION Same file :749"The author's per-chart override for ONE measure's display name — the entry of chart.series[] whose name IS that measure"; :1034 mergeAuthoredSeries(derivedSeries, chart.series), and authoredSeriesLabel :767-776 skips a non-matching entry (if (name !== measure) continue;). @object-ui/core src/utils/chart-presentation.ts:178-180: "an authored entry naming a measure that is NOT in the dataset selection is ignored — membership belongs to the dataset", implemented as derived.map(...) at :208-211, so an unmatched entry cannot enter the output
Report charts values[] (the report's measure selection) QUERY The table's own selection; the rule already reports it as such
List-view charts values[] QUERY app-shell/src/views/ObjectView.tsx:2103-2117const vals = …chart.values, passed as values: vals to the dataset-bound object-chart, and the series list is SYNTHESISED from it (series: vals.map((v) => ({ dataKey: v, label: v })))
List-view charts series[].name DOES NOT EXIST packages/spec/src/ui/view.zod.ts:1256ListChartConfigSchema is a strictObject of exactly chartType / dataset / dimensions / values. No series, no yAxis; and checkListChart in this rule never read one. Nothing to re-tier
Page chart components dimensions[] / values[] QUERY plugin-charts/src/ObjectChart.tsx:551-556ds.queryDataset(schema.dataset, { dimensions: …schema.dimensions…, measures: …schema.values… })
Page chart components properties.series[].name PRESENTATION Same file :890-892 — when the chart is dataset-bound the authored array is REPLACED, not merged: const finalSchema = datasetChart ? { ...schema, data: …, xAxisKey: …, series: datasetChart.series } : …, and buildChartSeries :681 derives series: vals.map((v) => ({ dataKey: v, label: labelOf(v) })) — one entry per selected measure. An authored entry reaches the renderer not at all
Page chart components properties.yAxis[].field PRESENTATION ObjectChart.tsx reads yAxis nowhere (zero occurrences). It survives the ...schema spread into ChartRenderer, whose :128-132 prefers the internal-shaped series it was handed; normalizeChartSchema.ts:318-329 synthesises series from yAxis[].field only "No series at all", which on this surface requires an empty values — a chart with no measures to plot either way. chart-presentation.ts:131-141 states the same structurally for the sibling surface: field is dropped so that a forwarded one cannot be "a live membership channel on an empty selection"

No surface came back inconclusive.

2. The tier decision, per position

Applying the #15463 maintainer precedent per surface (presentation → warning + the true consequence; query binding → error, message untouched):

Position Before After
report chart.yAxis error error, message verbatim
report values[i], list values[i], page values[i] error error, message verbatim
report chart.series[i].name error warning, reworded
page properties.series[i].name error warning, reworded
page properties.yAxis[i].field error warning, reworded (and its own sentence — see 3)

The finding is KEPT at every position: the metadata really is wrong — the author wrote a key and believes it is in force — which is exactly the distinction the #15463 ruling drew.

One deviation from the brief, reported rather than worked around. The brief asked for "the same suppressWarnings mechanism as the family". That mechanism does not exist here: suppressWarnings is declared on the dashboard WIDGET only (packages/spec/src/ui/dashboard.zod.ts:584), and none of these three surfaces carries the key — ReportChartSchema and ListChartConfigSchema are strict, and the page bag is untyped. Declaring it on them would be a schema change, which is a stop condition on this card, so the new advisories are non-gating (consumers split on severity === 'error') but not individually silenceable. The hint says so instead of advertising a key that does not exist, and the docblock records it. Whether those surfaces should get the key is a decision for the PM.

3. The axisRefs + seriesRefs merge (was :343-:353) — accident, and split

Reading BOTH shapes on the page surface is deliberate: the props bag genuinely mixes the list-chart selection with ChartConfig-style axes, and the rule's own comment says so. Folding them into one series array before the measure walk was not deliberate — it was an accident of the shape, and it had a consequence: every yAxis[].field took the SERIES message ("this series comes back empty"). The pin refuses the two for different reasons (an axis entry keeps its slot and its scale/chrome while the plotted columns come from values; a series entry is dropped whole), so they are now separate limbs — binding.axes and binding.series — with different sentences. The walk order is unchanged (axes, then series), so paths and finding order are byte-stable.

4. chart-axis-not-selected — the wording check

Same shared limb, lower tier, and it took the same one-size consequence: "the query does not return it, so the series plots nothing."

  • At a query position that is true, and it is kept verbatim.
  • At a presentation position the read refutes it: no series is derived for the name at all, so nothing "plots nothing" — the override lands on nothing. Each of the three presentation positions now states its own derivation (report: the ONE series the chart derives from its own chart.yAxis; page series: the series come from values; page axis: the plotted columns come from values).

A second defect this measurement exposed, deliberately NOT fixed here: on the report surface chart-axis-not-selected resolves against report.values, and the pin shows the chart never queries that set (it queries [chart.yAxis]). So at the report chart.yAxis position the warning fires for a measure the chart DOES query, and at the report series position the deciding set is the singleton { chart.yAxis } rather than report.values. Correcting the SENTENCE is this card; changing WHICH findings fire is a behaviour decision the card did not put to the PM. Filed as #15734 with both directions measured.

5. The #15636 seam this file carries

Per the PM addendum: this file's hand-copied asArray (array branch spelled as an unchecked v as AnyRec[]) is deleted and its 10 call sites re-pointed onto recordsOf (packages/lint/src/object-graph.ts:181). Behaviour-preserving except that a non-record member is skipped instead of dereferenced — previously strName(entry.name) on a null member threw where the rule should have reported. One test covers it (a null entry in the datasets, dimensions, measures, reports and chart.series collections this file reads: no throw, the real declarations around it still resolve, the one genuine defect still reported), and it also pins the honest price — positions index into the FILTERED collection, so a dropped junk member shifts them.

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, verdict lines quoted from the run output.

  • pnpm --filter '@objectstack/lint^...' build — dependency closure built BEFORE any judgement. os-verify-lock: VERDICT command-exit 0 · held the lock 177s (2m57s) · waited 0s.
  • 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 the lock 16s · waited 487s (8m07s).
  • pnpm --filter @objectstack/lint test Test Files 96 passed (96) / Tests 3041 passed (3041).
  • pnpm --filter @objectstack/lint typecheckcheck:test-typecheck: OK — @objectstack/lint's test layer compiles under packages/lint/tsconfig.test.json, so the edited test file is MEASURED rather than excluded. Both under VERDICT command-exit 0 · held the lock 61s (1m01s) · waited 222s (3m42s).
  • pnpm lint — exit 0, the FULL repo scan (eslint . --no-inline-config), not a narrowed one.

New coverage, per surface and per position class, each title naming the pinned renderer line that decides it (as the #15463 tests do): report yAxis stays error; report series[].name warns with the reworded consequence and NOT the refuted one; one chart reporting both tiers at once; chart-axis-not-selected reworded at the report series position and verbatim at the report yAxis position; list values[] stays error plus a pin that the list surface has no presentation position at all; page series[].name and page yAxis[].field warn with DIFFERENT sentences (the split); page values[] stays error; a battery asserting every presentation finding this rule can raise is below error.

Ablation (trap-guarded, absolute paths, mutation proven on disk in BOTH directions)

The test imports the module by relative path (./validate-chart-bindings.js), so vitest resolves SOURCE, not dist — there is no rebuild leg on either leg, and none was needed.

Reverting the tier decision to unconditional error (severity: position === 'query' ? 'error' : 'warning' becomes severity: 'error'), proven landed before reading anything: PRE_orig_count=1 / PRE_mut_count=0 then POST_orig_count=0 / POST_mut_count=1, hash 17f6f5bf… becomes b118bd3b…. Result: MUTATED_EXIT=1, Test Files 1 failed (1) / Tests 8 failed | 23 passed (31) — the warning-tier pins go red, in the expected direction.

Restore leg proven the same way, 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 equals the HEAD blob 17f6f5bf45c8ebadf030146cbbd6d5dd823cdce9), then RESTORED_EXIT=0, Test Files 1 passed (1) / Tests 31 passed (31).

Derived gate family

node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack on the final head derived 53 commands from the real change set (3 paths, three-dot against merge base 581d8f84c). All 53 run, 50 exit 0. Plus pnpm -s check:pm-dispatch-gates — exit 0, ✓ dispatch-gates self-test: 1445 cases pass. — and pnpm check:nul-bytes exit 0, with a direct control-byte scan of the three changed paths (grep -naP over the C0/DEL class) returning no matches.

NOT MEASURED, by name

  • pnpm 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.
  • pnpm check:type-check-debt — exit 3, PREREQUISITE NOT MET … 30 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk. Needs the whole-packages closure; CI's. Not a finding in either direction, and no ledger number is raised on it.
  • pnpm check:docs-transcript-drift — exit 3 on first run (packages/lint/dist/index.js does not exist), then RE-RUN after building @objectstack/lint: exit 0, ✓ check-docs-transcript-drift: 4 declared transcript value(s) across 405 page(s) under content/docs/ equal what the registry derives today. Measured.
  • The 7 families whose argv carries a CI-only variable, and the 36 artifact rosters the derivation scores silent for every card — reported by the tool as outside the runnable total, and read as such rather than as a clearance.
  • The derivation printed a STALE TREE warning (this branch is behind origin/main, and 2 files the families derive from moved in that range: scripts/pm/check-half-states.mjs, scripts/pm/ensure-pm-labels.sh). Re-derived after git fetch origin main: the command list is byte-identical, and pnpm check:pm-half-states — the family those two files define — ran and exited 0.

Scope

packages/lint/src/validate-widget-bindings.ts (the #15463 file) is not touched. No new rule id, no barrel change, no schema change, no governed surface, nothing under content/docs/releases/. Changeset: @objectstack/lint patch, with the tier drop stated as the behaviour change users see.

🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

9 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • 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 c2a336ca26223343f397cf1931916b3c769915d4packageMentionDocs.

Which tree this was computed on

This run read content/docs from bac177bdd31be167e7274ee6681a5d4b2c95744d — the merge of head ef5f28aad237dfab24bd3370fc8733730ee54c70 into base c2a336ca26223343f397cf1931916b3c769915d4, 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 bac177bdd31be167e7274ee6681a5d4b2c95744d && git checkout bac177bdd31be167e7274ee6681a5d4b2c95744d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c2a336ca26223343f397cf1931916b3c769915d4 ef5f28aad237dfab24bd3370fc8733730ee54c70 && git checkout -B drift-repro c2a336ca26223343f397cf1931916b3c769915d4 && git merge --no-ff ef5f28aad237dfab24bd3370fc8733730ee54c70

node scripts/docs-audit/affected-docs.mjs --json c2a336ca26223343f397cf1931916b3c769915d4

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 5, 2026
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — PR #15741 (head ef5f28aad) reviewed against the GitHub three-dot diff (3 files, +526/−39), not the report.

Ruling on the open question (suppressWarnings on the three surfaces): A — ship as landed. The three presentation positions are warning (non-gating) and the hint says plainly that the surface carries no suppressWarnings key. Declaring the key on ReportChartSchema / ListChartConfigSchema / the object-chart props entry is a spec change and this card's stop condition; it is recorded as a follow-up candidate in the seat post rather than folded in here. C is refused: the author wrote a key and believes it is in force, which is worth reporting.

Per-position tiering, accepted as measured at the current pin a472b0716: report chart.yAxis = query → error (verbatim message kept); report chart.series[].name = display-name override → warning; list values[] = query → error, and list charts have no presentation position at all; page values[] = query → error; page series[].name and yAxis[].field = presentation → warning, now on separate limbs (binding.axes / binding.series) with distinct sentences, walk order and paths unchanged. The #15463 precedent is applied per position, not per surface — correct.

PM addendum done: the hand-copied asArray is deleted and its call sites re-pointed to recordsOf (packages/lint/src/object-graph.ts:181, confirmed on origin/main), with a null-member test.

Measured by this seat: git merge-tree --write-tree origin/main <branch> → EXIT=0 (clean against eca504724); no live PR of this lane touches packages/lint/src/validate-chart-bindings.ts; changeset present (@objectstack/lint patch, tier drop stated as the behaviour change); 13 new it( cases read from the diff. Required jobs on ef5f28aad: Lint & Repo Gates = success, TypeScript Type Check = success.

NOT MEASURED here: the lint package's vitest run and typecheck (this checkout lacks typescript; the report's locked runs Tests 31 passed (31) / Tests 3041 passed (3041) / typecheck exit 0 and the trap-guarded ablation 8 failed | 23 passed → restored 31 passed are the record).

Out-of-scope: #15734 filed by the dev (chart-axis-not-selected deciding set on the report surface) — graded by the seat separately; the objectui-side finding (page series[] replaced wholesale even when it matches) is noted for the objectui lane, not filed here.

Flipping ready and enabling auto-merge now.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 5, 2026 05:43
@baozhoutao
baozhoutao enabled auto-merge September 5, 2026 05:43
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit a51eb86 Sep 5, 2026
35 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-15575-chart-measure-unknown-series-name branch September 5, 2026 06:12
baozhoutao pushed a commit that referenced this pull request Sep 5, 2026
#15741 re-pointed `validate-chart-bindings.ts` onto `recordsOf`, so both rows
naming it here — the `COPY_LEDGER` entry and the `UNGUARDED_ALLOWANCE` entry —
stopped describing the package. Both lists are exact in both directions, so the
gate went red on `names no copy the ledger has outlived` and `allows no
unchecked branch the allowance has outlived`, each naming the file. That is the
mechanism working: the allowance was dated to the change that removes it, and it
came out because the test failed rather than because anyone remembered.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
baozhoutao pushed a commit that referenced this pull request Sep 5, 2026
Same self-expiry, one layer down from the gate. `RESIDUAL_THROWS` recorded that
`validateReferenceIntegrity` still threw on a non-record `stack.datasets` member
through `indexDatasets`; #15741 re-pointed that reader, so the sweep went red
demanding a throw that no longer happens. The rows come out because the
assertion failed, which is what "exact in both directions" is for — a residual
list that only ever grew would rot into a list of excuses.

`objects[].fields` / `validateStackExpressions` (#15742) stays: measured, still
throwing on this tree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
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/l tests tooling

Projects

None yet

2 participants