Skip to content

Fix eleven defects from the second review - #63

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

Everything a second multi-agent review turned up. Each was reproduced before the change, verified after, and carries a test that fails without its fix. Two are regressions in last week's own work, which is the main argument for having pointed one of the review's angles at that diff.

Regressions in the recent fixes

A mid-run save dropped the authored colour mix when the total happened to be unchanged (js/model.js:493). Commit b868dcc gated both initialResources and initialColorMap behind one amount comparison. In a balanced loop, income and spend at matching rates, the total still reads 20 at the moment of the write, so neither field was recorded and the reload rebased the authored mix to whatever was in transit:

authored      20 {orange: 20}
mid-run       20 {orange: 14, green: 6}     total unchanged
saved         initialResources: false, initialColorMap: false
reload+reset  20 {orange: 14, green: 6}     authored mix gone

The sameMap check written to catch exactly that drift sat inside the guard and never executed. The amount and the mix drift independently, so they are tested independently now. At-rest files still carry neither field, verified byte-identical.

A sparkline built while the timeline was scrubbed started live (js/app-fields.js:105). Commit 642ee0d taught _updateSparklines to push the scrub index into sparklines it already knew about, but a card built during a scrub, which is what selecting another node to see its earlier value does, created one at scrubIndex: null. The hero read the replayed value over a trace of the whole run. The smoke test added with that fix calls _onSelect before _scrubTo, so it could not have caught this.

Engine correctness

A trader paying out of a delay or a queue created resources from nothing (js/engine.js:980). Their contents are mirrored by an internal queue that releases on its own schedule; takeResources drew the count down and left it intact, so the same units were handed to the partner and released again later.

partner conserved total, 12 steps drift queue vs count
delay 25 → 36 +11 owes 2, holds 1
queue 25 → 26 +1 owes 11, holds 1
pool (control) 25 → 25 0 n/a

Nothing in transit is available to trade, so a delay or queue no longer pays on a trader leg. Receiving is untouched: _give already routes an arrival through the queue properly. All three now conserve exactly.

Keyboard

Shift+Tab was a dead end on the canvas (js/editor.js). Forward Tab past the last node released; backward from the first computed -1, clamped back to 0 and reselected the same node forever, after preventDefault had already eaten the key. Both ends release now. (The reviewer called this a WCAG 2.1.2 keyboard trap; the skeptic corrected that — forward Tab does release, so it is a focus-order defect, not a trap.)

Adding or deleting a row in the Params or Checks rail dropped focus out of the panel (js/app-props.js, js/app-analysis.js). The handler calls _renderProps(), which tears down #props-content and destroys the button that had focus. Add now lands on the new row's name box; delete lands on the panel's add button. The pool stepper buttons already mutate in place and keep focus, so this was an omission, not a house convention.

Mobile

The timeline drawer was 150px against 173-195px of content (css/style.css:1481), so the entire scrub row sat below the fold and neither html, body nor the drawer scrolls. Measured at three phone sizes: 0px visible before, 39px after, with #tl-range hit-testable where it previously returned nothing.

The concept guide had no responsive rule at all (css/style.css:995). A 230px nav with flex-shrink: 0 left a 69px article column on a 320px screen, roughly one word per line with long words running past the modal edge. Stacked below 768px: 69px → 299px, overflow 58px → 0.

Render and export

Note text wrapped only on spaces and was never clipped (js/renderer.js:887). A pasted URL is one 57-character "word" painted 193px past the note in near-black, and noteLayer sits above nodeLayer so it covered whatever was to the right. Long words break now, and the text is clipped to the note as a backstop, since counting characters cannot predict wide or CJK glyphs.

Exported SVG and PNG dropped the class-only styles (js/app-export.js:81). There was no rule for .n-caption at all, so a converter's recipe rendered black on the exported dark background (1.09:1 contrast) at nearly twice its intended size; and font-family reached only elements that already carried the attribute, so node labels, badges and connection pills fell back to the browser serif.

Documentation accuracy

The gate guide described two of the three routing modes and claimed a gate stores nothing (js/kb.js:56). Both are untrue since All mode, where the weights are not shares at all: each output takes its own weight in units, and the gate keeps what it cannot move. The panel's percentage rows (75%) and its weight help text said the same wrong thing and are corrected alongside, so the UI and the guide agree.

The Library article pointed at a File menu entry that does not exist (js/kb.js:682), and on touch .file-controls is hidden entirely, so both routes it named were absent there.

Testing

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

One error I introduced and the suite caught: the gate help-text fix referenced node.gateMode inside _connProps, where the variable is src. That threw a ReferenceError and failed the smoke run; fixed and re-verified rather than worked around.


Generated by Claude Code

Each reproduced before the change and verified after, with a test that
fails without its fix. Two are regressions in last week's own work.

A mid-run save dropped the authored colour mix whenever the total
happened to be unchanged. b868dcc gated both the amount and the colour
map behind one amount comparison, so a balanced loop, income and spend at
matching rates, recorded neither: the total still read 20 at the moment of
the write and the reload rebased {orange:20} to the {orange:14, green:6}
that was in transit. The sameMap check written to catch exactly that drift
sat inside the guard and never ran. The two drift independently, so they
are tested independently now; at-rest files still carry neither field.

A sparkline built while the timeline was scrubbed started live. 642ee0d
taught _updateSparklines to push the scrub index into sparklines it
already knew about, but a card built during a scrub, which is what
selecting another node to see its earlier value does, created one at null.
The hero read the replayed value over a trace of the whole run, the same
card contradicting itself. The smoke test added with that fix selects
before scrubbing, so it could not have caught this.

A trader paying out of a delay or a queue created resources from nothing.
Their contents are mirrored by an internal queue that releases on its own
schedule; takeResources drew the count down and left it intact, so the
same units were handed over and released again. Measured: a delay partner
grew the system total from 25 to 36 over twelve steps while its queue owed
2 against a count of 1. Nothing in transit is available to trade, so it no
longer pays. Receiving is untouched.

Shift+Tab was a dead end on the canvas. Forward Tab past the last node
released, backward from the first computed -1, clamped to 0 and reselected
forever after preventDefault had eaten the key. Both ends release now.

Adding or deleting a row in the Params or Checks rail rebuilt the panel
and destroyed the focused button, dropping focus out of the panel. Add
lands on the new row's name box, delete on the panel's add button.

The mobile timeline drawer was 150px against 173 to 195px of content, so
the entire scrub row sat below the fold with between zero and three usable
pixels, and neither html, body nor the drawer scrolls. It fits its content
now: measured 0px visible before and 39px after at three phone sizes, with
the slider hit-testable.

The concept guide had no responsive rule at all, so a 230px nav that will
not shrink left a 69px article column on a 320px screen, roughly one word
per line with the long words running past the modal edge. Stacked below
768px: 69px becomes 299px and the overflow goes to zero.

Note text wrapped only on spaces and was never clipped, so a pasted URL
was one 57-character word painted 193px past the note in near-black, over
whatever sat to the right. Long words break, and the text is clipped to
the note as a backstop, since counting characters cannot predict wide or
CJK glyphs.

Exported SVG and PNG dropped the class-only styles: no rule for
.n-caption at all, so a converter's recipe rendered black on the dark
background at 1.09 to 1 contrast and nearly twice its intended size, and
font-family reached only elements that already carried the attribute, so
node labels, badges and connection pills fell back to the browser serif.

The gate guide described two of the three routing modes and claimed a gate
stores nothing, both untrue since All mode: there the weights are not
shares, each output takes its own weight in units. The panel's percentage
rows and its weight help said the same thing and are corrected with it.
The Library article pointed at a File menu entry that does not exist, and
on touch the whole file-controls container is hidden anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg
@zntznt
zntznt merged commit fdff692 into main Aug 28, 2026
1 check passed
@zntznt
zntznt deleted the claude/ui-ux-run-4qmy5r branch August 28, 2026 20:10
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