fix(lint): chart series[].name advises instead of blocking — presentation, not a query binding - #15741
Conversation
📓 Docs Drift Check9 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not 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
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 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 |
|
ACCEPT — PR #15741 (head Ruling on the open question ( Per-position tiering, accepted as measured at the current pin PM addendum done: the hand-copied Measured by this seat: NOT MEASURED here: the lint package's vitest run and typecheck (this checkout lacks 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 Flipping ready and enabling auto-merge now. Generated by Claude Code |
#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
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
Fixes #15575
chart-measure-unknownfired aterroron every measure position of the three chart surfacesvalidate-chart-bindings.tscovers, 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's00d3f09c5); the tier follows that read, per surface.1. The per-surface read (renderer lines quoted at the pin)
chart.xAxis/chart.yAxisplugin-report/src/DatasetReportRenderer.tsx:859-867—useDatasetRows(dataset, plan.kind === 'series' && xAxis ? [xAxis] : [], wantsQuery && yAxis ? [yAxis] : [], …), which postsqueryDataset(dataset, { dimensions, measures, … }). Same file,scopeOrderdocblock:326: "the embedded chart queries onlychart.xAxis×chart.yAxis"chart.series[].name:749— "The author's per-chart override for ONE measure's display name — the entry ofchart.series[]whosenameIS that measure";:1034mergeAuthoredSeries(derivedSeries, chart.series), andauthoredSeriesLabel:767-776skips a non-matching entry (if (name !== measure) continue;).@object-ui/coresrc/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 asderived.map(...)at:208-211, so an unmatched entry cannot enter the outputvalues[](the report's measure selection)values[]app-shell/src/views/ObjectView.tsx:2103-2117—const vals = …chart.values, passed asvalues: valsto the dataset-boundobject-chart, and the series list is SYNTHESISED from it (series: vals.map((v) => ({ dataKey: v, label: v })))series[].namepackages/spec/src/ui/view.zod.ts:1256—ListChartConfigSchemais astrictObjectof exactlychartType/dataset/dimensions/values. Noseries, noyAxis; andcheckListChartin this rule never read one. Nothing to re-tierdimensions[]/values[]plugin-charts/src/ObjectChart.tsx:551-556—ds.queryDataset(schema.dataset, { dimensions: …schema.dimensions…, measures: …schema.values… })properties.series[].name:890-892— when the chart is dataset-bound the authored array is REPLACED, not merged:const finalSchema = datasetChart ? { ...schema, data: …, xAxisKey: …, series: datasetChart.series } : …, andbuildChartSeries:681derivesseries: vals.map((v) => ({ dataKey: v, label: labelOf(v) }))— one entry per selected measure. An authored entry reaches the renderer not at allproperties.yAxis[].fieldObjectChart.tsxreadsyAxisnowhere (zero occurrences). It survives the...schemaspread intoChartRenderer, whose:128-132prefers the internal-shapedseriesit was handed;normalizeChartSchema.ts:318-329synthesises series fromyAxis[].fieldonly "No series at all", which on this surface requires an emptyvalues— a chart with no measures to plot either way.chart-presentation.ts:131-141states the same structurally for the sibling surface:fieldis 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):chart.yAxiserrorerror, message verbatimvalues[i], listvalues[i], pagevalues[i]errorerror, message verbatimchart.series[i].nameerrorwarning, rewordedproperties.series[i].nameerrorwarning, rewordedproperties.yAxis[i].fielderrorwarning, 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
suppressWarningsmechanism as the family". That mechanism does not exist here:suppressWarningsis declared on the dashboard WIDGET only (packages/spec/src/ui/dashboard.zod.ts:584), and none of these three surfaces carries the key —ReportChartSchemaandListChartConfigSchemaare 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 onseverity === '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+seriesRefsmerge (was:343-:353) — accident, and splitReading 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
seriesarray before the measure walk was not deliberate — it was an accident of the shape, and it had a consequence: everyyAxis[].fieldtook 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 fromvalues; a series entry is dropped whole), so they are now separate limbs —binding.axesandbinding.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 checkSame shared limb, lower tier, and it took the same one-size consequence: "the query does not return it, so the series plots nothing."
chart.yAxis; page series: the series come fromvalues; page axis: the plotted columns come fromvalues).A second defect this measurement exposed, deliberately NOT fixed here: on the report surface
chart-axis-not-selectedresolves againstreport.values, and the pin shows the chart never queries that set (it queries[chart.yAxis]). So at the reportchart.yAxisposition 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 thanreport.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 uncheckedv as AnyRec[]) is deleted and its 10 call sites re-pointed ontorecordsOf(packages/lint/src/object-graph.ts:181). Behaviour-preserving except that a non-record member is skipped instead of dereferenced — previouslystrName(entry.name)on anullmember threw where the rule should have reported. One test covers it (anullentry in the datasets, dimensions, measures, reports andchart.seriescollections 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 HEADat the run). Every exit code captured BEFORE any pipe (cmd > log 2>&1; EXIT=$?); heavy runs throughscripts/pm/os-verify-lock.shwithOS_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 typecheck—check: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 underVERDICT 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
yAxisstays error; reportseries[].namewarns with the reworded consequence and NOT the refuted one; one chart reporting both tiers at once;chart-axis-not-selectedreworded at the report series position and verbatim at the report yAxis position; listvalues[]stays error plus a pin that the list surface has no presentation position at all; pageseries[].nameand pageyAxis[].fieldwarn with DIFFERENT sentences (the split); pagevalues[]stays error; a battery asserting every presentation finding this rule can raise is belowerror.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, notdist— there is no rebuild leg on either leg, and none was needed.Reverting the tier decision to unconditional
error(severity: position === 'query' ? 'error' : 'warning'becomesseverity: 'error'), proven landed before reading anything:PRE_orig_count=1/PRE_mut_count=0thenPOST_orig_count=0/POST_mut_count=1, hash17f6f5bf…becomesb118bd3b…. 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-objectequals theHEADblob17f6f5bf45c8ebadf030146cbbd6d5dd823cdce9), thenRESTORED_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/objectstackon the final head derived 53 commands from the real change set (3 paths, three-dot against merge base581d8f84c). All 53 run, 50 exit 0. Pluspnpm -s check:pm-dispatch-gates— exit 0,✓ dispatch-gates self-test: 1445 cases pass.— andpnpm check:nul-bytesexit 0, with a direct control-byte scan of the three changed paths (grep -naPover 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.silentfor every card — reported by the tool as outside the runnable total, and read as such rather than as a clearance.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 aftergit fetch origin main: the command list is byte-identical, andpnpm 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 undercontent/docs/releases/. Changeset:@objectstack/lintpatch, 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