You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
finding(plugin-charts): scatter is NOT MEASURED for the degenerate-magnitude class — its all-positive CONTROL drew zero marks, so the sweep's zeros say nothing #7171
Filed by the domain:ui PM seat from a measurement in objectui#7147 (PR #7169). The implementer reported it rather than filing, per the report-don't-duplicate fence on that surface — but unlike objectui#7148's finding, this one has no existing card to attach to, so it gets its own.
What is NOT known
objectui#7147 swept 8 chart families × 9 datasets = 74 tiles in real Chromium, MD5'd and pixel-diffed against a literally empty div, to find where a degenerate magnitude (zero, negative, null, unparseable) produces a chart that misrepresents its data.
It reached verdicts for seven families:
family
verdict
pie / donut / funnel / treemap
defective — fixed in PR objectui#7169
bar / radar
measured and clean — deliberately untouched
scatter
⛔ NOT MEASURED
Scatter is not clean. It is unmeasured, and the distinction is the whole point of this card.
Why the sweep could not read it
Every scatter tile drew zero marks — including the all-positive CONTROL (markN 0).
A control that returns zero is no control. With the control dead, scatter's zeros on the degenerate datasets carry no information: they are indistinguishable from "the fixture never rendered a scatter at all." Reporting scatter as clean on that evidence would have been a false green across nine datasets.
The cause is identified: scatter takes two measures (x and y), and objectui#7147's fixture supplies a single measure. The fixture does not bind scatter, so scatter never had a chance to draw.
What a correct measurement needs
A two-measure fixture for the scatter arm, so its all-positive control actually draws marks. ⛔ Until that control returns non-zero, no scatter reading means anything.
Then the same nine datasets, with the degenerate value applied to each measure independently and to both — a scatter can plausibly degenerate on x, on y, or on both, and those may not behave alike.
Per-tile screenshot hashing against per-family all-positive and one-row controls, the instrument that produced verdicts for the other seven families. A hash collision between a degenerate tile and a legitimately-sparse one is the finding; that is how pie's full-circle case and treemap's four-datasets-to-one-image case were caught.
Blocked, and on what
pm:blocked on PR #7169 landing — it is live in packages/plugin-charts/src/AdvancedChartImpl.tsx, which is where any scatter arm work would sit. Unblocks automatically.
Why this is worth tracking rather than dropping
⚠️This is the shape that produces false confidence. After PR objectui#7169 lands, the honest statement is "seven of eight families measured; scatter unknown." Without this card, the natural summary becomes "the degenerate-magnitude class is handled," which is not what was measured — and the next person to look would have to re-derive the whole sweep to discover the hole.
⚠️ And note the pattern this card family keeps producing: objectui#7140 (early return), objectui#7147 (degenerate geometry), objectui#7148 (silent row drop) are three distinct mechanisms behind one reader-facing symptom. Scatter may be a fourth. ⛔ Do not assume PR objectui#7169's predicate transfers to it — measure first.
Related
objectui#7147 / PR objectui#7169 (the sweep and the fix) · objectui#7148 (silent row drop) · objectui#7140 (the empty-frame early return) · objectui#7146.
Serial constraints cleared: plugin-charts has no other open PR. The three landed answers in AdvancedChartImpl.tsx are all in the tree and verified (no-positive-flow 3, no-positive-magnitude 1, unsized-rows 1, omitted-rows 1).
Clause ② — not engaged
A sweep is a measurement. If it finds a defect, the repair shape is the one #7146 / #7147 / #7148 already landed at normal tier — a refusal or a footnote inside the renderer, no declared surface moved.
⚠️ The one thing that must not be inherited
This card exists because scatter's result in #7147's sweep was NOT MEASURED, and NOT MEASURED is neither red nor green. Its all-positive control drew zero marks (markN: 0 on every scatter tile, the control included), so a control that returns zero is no control, and none of its zeros carry information.
⭐ And the predicate that fixed the other four families almost certainly does not transfer. Pie, funnel and treemap size by magnitude, so Number.isFinite(v) && v > 0 is the right test there. Scatter plots position — a negative or zero coordinate is perfectly legitimate data. Mechanically reusing no-positive-magnitude on scatter would refuse correct charts. The dispatch order carries that as a fence: the degenerate class for a two-measure positional chart has to be derived, not copied.
{
"issue": 7171,
"status": "done",
"branch": "claude/issue-7171-scatter-degenerate-sweep",
"pr": "https://github.com/objectstack-ai/objectui/pull/7197",
"premise_still_valid": true,
"summary": "THE SCATTER CONTROL DREW — 3 of 3 marks, 245px of mark area, on a run whose live console positive control also fired; every reading below is therefore a measurement, not a zero. A2.1 confirmed: scatter's XAxis and YAxis are both type=number bound to xAxisKey and series[0].dataKey, and #7147's single-measure fixture put a category column on the numeric X axis — reproduced as its own tile, 0 marks, byte-identical to five other broken shapes. The sweep (33 tiles, real Chromium, one page load per tile, 520x360 above the 280 floor) found three fix-grade defects: rows unplaceable on BOTH coordinates rendered byte-identically (0.000%) to a scatter handed no rows at all; six distinct authoring failures shared one image; and one placeable row among three was 99.75% pixel-identical to a genuinely one-row scatter. Shipped a positional predicate — no-plottable-points refusal and unplotted-points footnote, scatter only — DERIVED not copied: #7147's magnitude predicate would have blanked the all-negative (3 of 3 marks), all-zero (2 of 2) and mixed-sign (3 of 3) tiles, which are correct charts. Zero variance, constant-x and constant-y are measured-and-DECLINED (A2.3's axis-domain-collapse hypothesis did not reproduce as collapse; the real failure is an axis that renders no scale at all). 14 tiles moved, 19 byte-identical, all three landed answers still firing under their own codes.",
"tests": "Union run on the final commit 9953dd5fc (git rev-parse --short HEAD), tree clean. `pnpm exec vitest run packages/plugin-charts/src/AdvancedChartImpl.degeneratePosition.test.tsx` -> `Test Files 1 passed (1)` / `Tests 40 passed (40)`; `pnpm exec vitest run packages/plugin-charts/` -> `Test Files 40 passed (40)` / `Tests 371 passed (371)`. The FIRST test asserts the control draws a non-zero mark count, as an assertion rather than an observation. ABLATION, direction and counts predicted in writing before running (leg A 17 red, leg B 7 red, disjoint): leg A neutered the refusal guard (points.plottable === 0 -> === -1) and gave 17 failed / 354 passed, split 15 refusal block + 1 seam + 1 unprojectedSeriesDimension; leg B inverted the note guard (unplotted <= 0 -> >= 0) and gave 7 failed / 364 passed, all in the note block. Both legs proved mutation on disk by anchor count AND blob hash (aceb44fc -> d740c74f in A, -> 53138e45 in B; anchor 1->0, injected 0->1), and proved restore by STATE (git diff HEAD empty, git diff --cached empty, blob back to aceb44fc), under an absolute-path trap using `git checkout HEAD -- path` rather than a bare checkout. The browser instrument was rebuilt (`vite build`) on each tree and the marker strings verified present in the bundle the browser actually reads before every reading. GATES on 9953dd5fc: type-check exit 0 with tsc --listFiles confirming all four edited files in the compiled set (1 hit each); package lint `299 problems (0 errors, 299 warnings)` exit 0; check-changeset-presence/no-major/overwrite/fixed all exit 0; check-control-bytes `OK (scanned 5956 tracked text file(s))`; check-vi-mock-specifiers/inherit `OK (4106 tracked source file(s) ... 526 carry a mock)` — counts moved from #7169's 5942/525, which proves the gates saw the new test rather than skipping it as untracked; check-i18n-keys/drift, check-self-import, check-phantom-deps, check-esm-specifiers, check-side-effects-array, check-shell-escape-residue all exit 0; check-lint-coverage `46/46 packages linted, 0 with outstanding errors`; check-type-check-coverage `41/41 packages compile their tests`. NOT MEASURED, reported as such and not as failures: check-eager-closure exited 2 with `not applicable to a local run — this check compares a pull_request merge ref` (CI owns it); the probe's first vite build failed to resolve @object-ui/core because the dependency closure was unbuilt, built with `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-charts^...' build` and re-run to a real verdict. DECLARED NARROWING: repo-wide `pnpm lint` not run locally, CI owns it — the per-package run is a measurement: eslint's own config selected 53 files (--format json count, up from #7169's 52, the delta being the new test file), all four edited files in that set with 0 errors, and eslint.config.js extends tseslint.configs.recommended with zero matches for parserOptions.project or projectService, so no rule is type-aware and this diff cannot move a verdict in a file it does not touch.",
"mcp_calls": "10",
"open_questions": [],
"out_of_scope_findings": [
"filed as #7194: a multi-series scatter draws every series at the FIRST series' y values — measured, two series over `ym` and `zm` produced 4 symbols at only 2 distinct positions, each drawn twice, and `zm`'s values (5 and 90) appear nowhere on the plot; good data, confidently wrong picture, and it needs a shape decision rather than a one-liner",
"filed as #7195: an all-boolean scatter coordinate draws nothing and says nothing — deliberately left unguarded by this PR, because a boolean beside a genuinely numeric row draws every mark (3 of 3), so rejecting booleans would put a false 'not drawn' count over visible points; the first draft DID reject them and the pin went red, which is how the mixed reading was found"
]
}
The instrument is NOT committed. It is a browser probe over a built bundle and cannot run in CI, where it would read as coverage while measuring nothing. Its findings are committed as the 40 jsdom assertions instead — including the control assertion, which runs in CI on every push.
✅ LANDED — PR #7197 squash-merged to main (f08cbd996)
Verified by content with live controls. The strongest available shape here is differential: every previously-landed seam answer is still in the same file, so they discriminate a real landing from a broken probe.
probe
result
SUBJECT — no-plottable-points (the new refusal code)
1
SUBJECT — unplotted-points (the new note)
7
SUBJECT — new suite AdvancedChartImpl.degeneratePosition.test.tsx
⭐ All five seam codes now coexist in AdvancedChartImpl.tsx, which is the mutual-exclusivity assertion holding in the tree rather than only in the suite.
The card's premise was NOT MEASURED, and this closed it the right way
This card existed because #7147's scatter result was neither red nor green: its all-positive control drew zero marks, so none of its zeros carried information.
The control drew — 3 of 3 marks, 245px of mark area, on a run whose live console positive control also fired. And the first test asserts that non-zero mark count rather than observing it, so the sweep cannot silently degrade back into blank tiles.
⭐ Better still, the old zero is now explained rather than merely superseded: scatter's XAxis and YAxis are both type=number bound to xAxisKey and series[0].dataKey, and #7147's single-measure fixture put a category column on the numeric X axis — reproduced as its own tile, 0 marks, byte-identical to five other broken shapes. A known artefact beats an unexplained one.
⭐ The predicate was DERIVED, and the derivation is proved by what copying would have broken
The fence I set was: ⛔ do not reuse #7147's magnitude predicate. It was not merely respected, it was measured:
#7147's magnitude predicate would have blanked the all-negative (3 of 3 marks), all-zero (2 of 2) and mixed-sign (3 of 3) tiles — which are correct charts.
⇒ Mechanical reuse would have refused three working charts, which is worse than the silence being fixed. Scatter plots position; negative and zero coordinates are legitimate data. That is why a separate scatter-only positional predicate earns its existence.
What the sweep found
rows unplaceable on both coordinates render byte-identically (0.000%) to a scatter handed no rows at all;
six distinct authoring failures share one image;
one placeable row among three is 99.75% pixel-identical to a genuinely one-row scatter.
14 tiles moved, 19 byte-identical, and all three landed answers keep firing under their own codes.
⚠️Zero-variance, constant-x and constant-y are measured-and-DECLINED — and my A2.3 guess that the interesting failure would be axis domain collapsedid not reproduce as collapse; the real failure there is an axis that renders no scale at all. Naming what the mechanism actually is, instead of accepting my framing, is what Zone 2 exists for.
An all-boolean scatter coordinate draws nothing and says nothing, and it was deliberately left unguarded: a boolean beside a genuinely numeric row draws every mark (3 of 3), so rejecting booleans would put a false "not drawn" count over visible points. And the discovery route is the part worth keeping:
the first draft DID reject them and the pin went red, which is how the mixed reading was found.
That is the ablation discipline running forward, as a design instrument rather than an after-the-fact proof, and it prevented shipping a refusal that lies about what is on screen.
Filed, not ridden
#7194 — graded p1 bug this round. A multi-series scatter draws every series at the first series' y values: two series produced 4 symbols at 2 distinct positions, each drawn twice, with one series' values appearing nowhere. ⚠️ Unlike everything this card fixed, that needs no degenerate input — valid data, confidently wrong picture, and invisible to inspection because 4 symbols at 2 positions look exactly like legitimate coincident points. It needs a shape ruling and is escalated on #5560.
Activity
os-warren commentedon Sep 1, 2026
Claim —
domain:uiexecution seat, R19. Unblocked: PR #7169 landed (5eddeeb68).session_012wwHa4aaFybxXrfmfHioDMclaude/issue-7171-scatter-degenerate-sweepui· Worktree: dedicated, offorigin/mainplugin-chartshas no other open PR. The three landed answers inAdvancedChartImpl.tsxare all in the tree and verified (no-positive-flow3,no-positive-magnitude1,unsized-rows1,omitted-rows1).Clause ② — not engaged
A sweep is a measurement. If it finds a defect, the repair shape is the one #7146 / #7147 / #7148 already landed at normal tier — a refusal or a footnote inside the renderer, no declared surface moved.
This card exists because scatter's result in #7147's sweep was NOT MEASURED, and NOT MEASURED is neither red nor green. Its all-positive control drew zero marks (
markN: 0on every scatter tile, the control included), so a control that returns zero is no control, and none of its zeros carry information.⭐ And the predicate that fixed the other four families almost certainly does not transfer. Pie, funnel and treemap size by magnitude, so
Number.isFinite(v) && v > 0is the right test there. Scatter plots position — a negative or zero coordinate is perfectly legitimate data. Mechanically reusingno-positive-magnitudeon scatter would refuse correct charts. The dispatch order carries that as a fence: the degenerate class for a two-measure positional chart has to be derived, not copied.Generated by Claude Code
os-warren commentedon Sep 1, 2026
os-dev-report
{ "issue": 7171, "status": "done", "branch": "claude/issue-7171-scatter-degenerate-sweep", "pr": "https://github.com/objectstack-ai/objectui/pull/7197", "premise_still_valid": true, "summary": "THE SCATTER CONTROL DREW — 3 of 3 marks, 245px of mark area, on a run whose live console positive control also fired; every reading below is therefore a measurement, not a zero. A2.1 confirmed: scatter's XAxis and YAxis are both type=number bound to xAxisKey and series[0].dataKey, and #7147's single-measure fixture put a category column on the numeric X axis — reproduced as its own tile, 0 marks, byte-identical to five other broken shapes. The sweep (33 tiles, real Chromium, one page load per tile, 520x360 above the 280 floor) found three fix-grade defects: rows unplaceable on BOTH coordinates rendered byte-identically (0.000%) to a scatter handed no rows at all; six distinct authoring failures shared one image; and one placeable row among three was 99.75% pixel-identical to a genuinely one-row scatter. Shipped a positional predicate — no-plottable-points refusal and unplotted-points footnote, scatter only — DERIVED not copied: #7147's magnitude predicate would have blanked the all-negative (3 of 3 marks), all-zero (2 of 2) and mixed-sign (3 of 3) tiles, which are correct charts. Zero variance, constant-x and constant-y are measured-and-DECLINED (A2.3's axis-domain-collapse hypothesis did not reproduce as collapse; the real failure is an axis that renders no scale at all). 14 tiles moved, 19 byte-identical, all three landed answers still firing under their own codes.", "tests": "Union run on the final commit 9953dd5fc (git rev-parse --short HEAD), tree clean. `pnpm exec vitest run packages/plugin-charts/src/AdvancedChartImpl.degeneratePosition.test.tsx` -> `Test Files 1 passed (1)` / `Tests 40 passed (40)`; `pnpm exec vitest run packages/plugin-charts/` -> `Test Files 40 passed (40)` / `Tests 371 passed (371)`. The FIRST test asserts the control draws a non-zero mark count, as an assertion rather than an observation. ABLATION, direction and counts predicted in writing before running (leg A 17 red, leg B 7 red, disjoint): leg A neutered the refusal guard (points.plottable === 0 -> === -1) and gave 17 failed / 354 passed, split 15 refusal block + 1 seam + 1 unprojectedSeriesDimension; leg B inverted the note guard (unplotted <= 0 -> >= 0) and gave 7 failed / 364 passed, all in the note block. Both legs proved mutation on disk by anchor count AND blob hash (aceb44fc -> d740c74f in A, -> 53138e45 in B; anchor 1->0, injected 0->1), and proved restore by STATE (git diff HEAD empty, git diff --cached empty, blob back to aceb44fc), under an absolute-path trap using `git checkout HEAD -- path` rather than a bare checkout. The browser instrument was rebuilt (`vite build`) on each tree and the marker strings verified present in the bundle the browser actually reads before every reading. GATES on 9953dd5fc: type-check exit 0 with tsc --listFiles confirming all four edited files in the compiled set (1 hit each); package lint `299 problems (0 errors, 299 warnings)` exit 0; check-changeset-presence/no-major/overwrite/fixed all exit 0; check-control-bytes `OK (scanned 5956 tracked text file(s))`; check-vi-mock-specifiers/inherit `OK (4106 tracked source file(s) ... 526 carry a mock)` — counts moved from #7169's 5942/525, which proves the gates saw the new test rather than skipping it as untracked; check-i18n-keys/drift, check-self-import, check-phantom-deps, check-esm-specifiers, check-side-effects-array, check-shell-escape-residue all exit 0; check-lint-coverage `46/46 packages linted, 0 with outstanding errors`; check-type-check-coverage `41/41 packages compile their tests`. NOT MEASURED, reported as such and not as failures: check-eager-closure exited 2 with `not applicable to a local run — this check compares a pull_request merge ref` (CI owns it); the probe's first vite build failed to resolve @object-ui/core because the dependency closure was unbuilt, built with `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-charts^...' build` and re-run to a real verdict. DECLARED NARROWING: repo-wide `pnpm lint` not run locally, CI owns it — the per-package run is a measurement: eslint's own config selected 53 files (--format json count, up from #7169's 52, the delta being the new test file), all four edited files in that set with 0 errors, and eslint.config.js extends tseslint.configs.recommended with zero matches for parserOptions.project or projectService, so no rule is type-aware and this diff cannot move a verdict in a file it does not touch.", "mcp_calls": "10", "open_questions": [], "out_of_scope_findings": [ "filed as #7194: a multi-series scatter draws every series at the FIRST series' y values — measured, two series over `ym` and `zm` produced 4 symbols at only 2 distinct positions, each drawn twice, and `zm`'s values (5 and 90) appear nowhere on the plot; good data, confidently wrong picture, and it needs a shape decision rather than a one-liner", "filed as #7195: an all-boolean scatter coordinate draws nothing and says nothing — deliberately left unguarded by this PR, because a boolean beside a genuinely numeric row draws every mark (3 of 3), so rejecting booleans would put a false 'not drawn' count over visible points; the first draft DID reject them and the pin went red, which is how the mixed reading was found" ] }Notes on the two report fields PM reads first
premise_still_valid: true— the card's diagnosis was correct in full. Scatter takes two measures; finding(plugin-charts): funnel and pie handed all-zero rows render a pixel-identical blank tile — same silence as #7140, a different trigger #7147's fixture supplied one; the control was dead for exactly that reason. Verified by reproducing finding(plugin-charts): funnel and pie handed all-zero rows render a pixel-identical blank tile — same silence as #7140, a different trigger #7147's own fixture as a tile in the new instrument (scatter--fixture7147, 0 marks) beside a two-measure control that drew.Generated by Claude Code
os-warren commentedon Sep 1, 2026
✅ LANDED — PR #7197 squash-merged to
main(f08cbd996)Verified by content with live controls. The strongest available shape here is differential: every previously-landed seam answer is still in the same file, so they discriminate a real landing from a broken probe.
no-plottable-points(the new refusal code)unplotted-points(the new note)AdvancedChartImpl.degeneratePosition.test.tsxno-positive-flow(#7146)no-positive-magnitude(#7147)unsized-rows(#7148)⭐ All five seam codes now coexist in
AdvancedChartImpl.tsx, which is the mutual-exclusivity assertion holding in the tree rather than only in the suite.The card's premise was NOT MEASURED, and this closed it the right way
This card existed because #7147's scatter result was neither red nor green: its all-positive control drew zero marks, so none of its zeros carried information.
The control drew — 3 of 3 marks, 245px of mark area, on a run whose live console positive control also fired. And the first test asserts that non-zero mark count rather than observing it, so the sweep cannot silently degrade back into blank tiles.
⭐ Better still, the old zero is now explained rather than merely superseded: scatter's
XAxisandYAxisare bothtype=numberbound toxAxisKeyandseries[0].dataKey, and #7147's single-measure fixture put a category column on the numeric X axis — reproduced as its own tile, 0 marks, byte-identical to five other broken shapes. A known artefact beats an unexplained one.⭐ The predicate was DERIVED, and the derivation is proved by what copying would have broken
The fence I set was: ⛔ do not reuse #7147's magnitude predicate. It was not merely respected, it was measured:
⇒ Mechanical reuse would have refused three working charts, which is worse than the silence being fixed. Scatter plots position; negative and zero coordinates are legitimate data. That is why a separate scatter-only positional predicate earns its existence.
What the sweep found
14 tiles moved, 19 byte-identical, and all three landed answers keep firing under their own codes.
⭐ #7195 — their own pin caught their overreach
An all-boolean scatter coordinate draws nothing and says nothing, and it was deliberately left unguarded: a boolean beside a genuinely numeric row draws every mark (3 of 3), so rejecting booleans would put a false "not drawn" count over visible points. And the discovery route is the part worth keeping:
That is the ablation discipline running forward, as a design instrument rather than an after-the-fact proof, and it prevented shipping a refusal that lies about what is on screen.
Filed, not ridden
#7194 — graded p1⚠️ Unlike everything this card fixed, that needs no degenerate input — valid data, confidently wrong picture, and invisible to inspection because 4 symbols at 2 positions look exactly like legitimate coincident points. It needs a shape ruling and is escalated on #5560.
bugthis round. A multi-series scatter draws every series at the first series' y values: two series produced 4 symbols at 2 distinct positions, each drawn twice, with one series' values appearing nowhere.Clearing
pm:dispatched.Generated by Claude Code