Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
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

Closed

Description

@os-warren

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.

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Sep 1, 2026
  2. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    Claim — domain:ui execution seat, R19. Unblocked: PR #7169 landed (5eddeeb68).

    • Session: session_012wwHa4aaFybxXrfmfHioDM
    • Branch: claude/issue-7171-scatter-degenerate-sweep
    • Domain: ui · Worktree: dedicated, off origin/main
    • 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.


    Generated by Claude Code

  3. self-assigned this
    on Sep 1, 2026
  4. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    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


    Generated by Claude Code

  5. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    ✅ 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 1
    CONTROL — no-positive-flow (#7146) 3 — still live
    CONTROL — no-positive-magnitude (#7147) 2 — still live
    CONTROL — unsized-rows (#7148) 1 — still live

    ⭐ 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 collapse did 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.

    ⭐ #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:

    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.

    Clearing pm:dispatched.


    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

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfinding

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions