Skip to content

Fix the nine remaining defects found in review - #62

Merged
zntznt merged 1 commit into
mainfrom
claude/ui-ux-run-4qmy5r
Aug 28, 2026
Merged

zntznt merged 1 commit into
mainfrom
claude/ui-ux-run-4qmy5r

Conversation

@zntznt

@zntznt zntznt commented Aug 28, 2026

Copy link
Copy Markdown
Owner

The nine medium and low findings left after #61. Each was reproduced before the change, verified after, and carries a test that fails without its fix.

Contained fixes

Step never refreshed the scrub slider (js/app.js), so its range sat one entry behind the history. Live, the thumb already sits at max, so End and a rightward drag emit no input event and the newest step could not be reached at all. Run/Pause and Reset already refreshed it. Measured: max: 3 against a history of 5.

assertions was the one serialized Diagram field _clearAll missed (js/app.js), so design tests followed the user through File > New, every template load, and the restored-session Discard, then into autosave, Save as JSON, the .econ export and the share link, where cli.js --check would run them against nodes that no longer exist. Three leaking paths, not the two originally reported.

_exportFilename kept only ASCII (js/app-export.js), so a non-Latin name became a bare ".svg" that the browser renames to svg.svg:

before   经济 -> ".svg"      Экономика -> ".svg"      Économie -> "conomie.svg"
after    经济 -> "经济.svg"   Экономика -> "Экономика.svg"   Économie -> "Économie.svg"

Every export of such a diagram collided on one filename, .econ lost its type to econ.txt, and File > Save lost the name too (9 call sites, not 5). The Library's separate sanitizer is aligned on the same rule.

Charts and scrub

Sparkline mapped v/max, putting anything below zero off the bottom of the canvas (js/charts.js). An all-negative series measured 0 rows of trace ink — the canvas held only a false max: 1 caption. A series crossing zero drew only its positive half, which looks like real data. The scale spans zero now, positive-only series are unchanged, and the caption reports the true extremes rather than the zero-extended ones.

The properties panel had no scrub-aware read path at all (js/app-fields.js, js/charts.js, js/app.js). The canvas and the timeline each had one, so scrubbing showed the replayed step beside a hero card still reporting the end of the run: two numbers for the same node. Adding _renderProps() to _scrubTo would have changed nothing — _heroCard, _refreshResourceCount and Sparkline.update all read live model state. All three now read the previewed entry, and the amount field stops mirroring while scrubbing so a past value cannot be typed back into the model.

Economy-as-code

A node labelled with a .econ head keyword broke its own connections (js/dsl.js), because dslParse dispatches on tokens[0] before scanning for an arrow. All 19 heads were broken: 17 threw on reload, and economy and assert matched their handlers and dropped the connection silently. Such a label serializes quoted now; ordinary labels are still emitted bare. Only the source side, and only lowercase, was ever affected since default labels are capitalized.

Trader flows were attributed backwards (js/attribution.js). The engine books what A pays under the incoming leg and what B pays under the outgoing one, so reading direction off sourceId/targetId called B's payment income, never credited either partner with what it received, and gave the trader two rows for resources it never held. Rebuilt from the real payer and payee, which the positional ins[i]/outs[i] pairing makes recoverable. With A paying 3 and B paying 1:

before after
A -3 to Market, residual 1 -3 to B, +1 from B, residual 0
B +1 from Market (inverted), residual 1 +3 from A, -1 to A, residual 0
Market +3 from A / -1 to B no flow rows

The residual reaching zero for both partners is the proof the accounting balances.

Editor and panels

Group and connection handles painted below nodeLayer (js/renderer.js), so a node on a corner hid them while the press still resized. elementFromPoint at a group corner returned ns (a node) before and resize-handle after.

The reported cause was wrong and worth stating: "the handle hit test runs before hitTest" is deliberate and correct — a selected item's handles must beat what they sit on, and reordering would make handles unreachable whenever a node overlapped. The defect was paint order. Note and chart handles were always fine because their layers already paint above nodeLayer. Handles move to an overlay layer; the .conn-handles / .resize-handles wrapper classes are kept because app-export.js:69 strips them from exported SVGs by those selectors.

Renaming a parameter onto an existing name overwrote the other and deleted this one (js/app-props.js): {gold: 5, rate: 2} became {gold: 2}. The check now spans the whole shared store — params, custom variables, register labels and named state connections — since a duplicate collapses there whichever editor produced it. Verified a legitimate rename still works.

Testing

  • node test/run.js → 239 passed, 0 failed (was 237)
  • npm run smoke → SMOKE PASSED, no console or page errors
  • Every new test fails without its fix.

One regression I caused and fixed: the full smoke run failed on the pre-existing analysis test with tlShown: false. My new tests left the timeline drawer open, and that test toggles btn-timeline unconditionally, so an already-open drawer made its click close it. Fixed by restoring the drawer in my own tests rather than editing the older one.


Generated by Claude Code

Each reproduced before the change and verified after, with a test that
fails without its fix.

Step never refreshed the scrub slider, so its range sat one entry behind
the history. Live, the thumb is already at max, so End and a rightward
drag emitted no input event and the newest step could not be reached at
all. Run/Pause and Reset already refreshed it.

assertions was the one serialized Diagram field _clearAll missed, so
design tests followed the user through File > New, every template load and
the restored-session Discard, then into autosave, Save as JSON, the .econ
export and the share link, where cli.js --check ran them against nodes
that no longer existed.

_exportFilename kept only ASCII, so a Chinese or Cyrillic name became a
bare ".svg" that the browser renames to "svg.svg": every export of such a
diagram collided on one filename, .econ lost its type to "econ.txt", and
File > Save lost the name too. Unicode letters and digits are kept now,
with a "diagram" fallback so the result can never begin with a dot. The
Library's separate sanitizer is aligned on the same rule.

Sparkline mapped v/max, putting anything below zero off the bottom of the
canvas. A register holding a net or deficit, which is most of what a
register computes, drew nothing at all under a false "max: 1"; a series
crossing zero drew only its positive half, which looks like real data.
The scale spans zero now, positive-only series are unchanged, and the
caption reports the true extremes.

The properties panel had no scrub-aware read path at all. The canvas and
the timeline each had one, so scrubbing showed the replayed step beside a
hero card still reporting the end of the run: two numbers for one node.
The hero, the live readout and the sparkline now read the previewed entry,
and the amount field stops mirroring while scrubbing so a past value
cannot be typed back into the model.

A node labelled with a .econ head keyword broke its own connections,
because dslParse dispatches on tokens[0] before scanning for an arrow.
Seventeen of the nineteen heads threw on reload; economy and assert
matched their handlers and dropped the connection silently. Such a label
serializes quoted now. Only the source side and only lowercase were ever
affected, since default labels are capitalized.

Trader flows were attributed backwards. The engine books what A pays under
the incoming leg and what B pays under the outgoing one, so reading
direction off sourceId/targetId called B's payment income, never credited
either partner with what it received, and gave the trader two rows for
resources it never held. Rebuilt from the real payer and payee, which the
positional ins[i]/outs[i] pairing makes recoverable; both partners now
account to a zero residual.

Group and connection handles were painted below nodeLayer, so a node on a
corner hid them while the press still resized. The hit-test precedence was
never the bug and is unchanged; the handles move to an overlay above the
content so the visible region and the hot region are the same again.

Renaming a parameter onto an existing name overwrote the other and deleted
this one. The check spans the whole shared store, params, custom variables,
register labels and named state connections, since a duplicate collapses
there whichever editor produced it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg
@zntznt
zntznt merged commit ec696a4 into main Aug 28, 2026
1 check passed
@zntznt
zntznt deleted the claude/ui-ux-run-4qmy5r branch August 28, 2026 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants