Files
ai_mouse/docs/superpowers/plans/2026-07-09-mouse-postprocess-quality.md
2026-07-09 17:16:20 +08:00

24 KiB
Raw Permalink Blame History

Mouse Post-Processing Quality Rework Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Remove the endpoint artifacts (vertical walls, hooks, start kinks) in generated mouse trajectories by reworking the post-processing pipeline, and reduce sampling jitter by switching 10-step Euler to 10-step Heun.

Architecture: Three new pure-numpy functions in src/ai_mouse/_postprocess.py (soften_forward, damp_start, warp_endpoints) replace the three hard-clamping functions (enforce_forward_monotonic, smooth_start, snap_endpoints). mouse.py wires them in a new order (soft-monotonic → start damping → smoothing both axes → global endpoint correction) and replaces the Euler ODE loop with Heun predictor-corrector. Golden regression baselines are re-captured (intentional behavior change).

Tech Stack: Python 3.12+, numpy, onnxruntime, pytest. Package manager: uv.

Spec: docs/superpowers/specs/2026-07-09-mouse-postprocess-quality-design.md

Global Constraints

  • src/ai_mouse/ is wheel content: NEVER import torch/fastapi/scipy/matplotlib there (CI library job installs runtime deps only).
  • All new post-processing functions are pure (no I/O, no global state), matching the existing _postprocess.py convention.
  • Public API (generate() signature and return shape, exact endpoint hit) must not change.
  • Scroll subsystem and golden_scroll.npz are untouched.
  • Run library tests with: uv run pytest tests/unit (add -v per test as noted).
  • All commits end with Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>.

Task 1: soften_forward — soft monotonic with overshoot compression

Replaces the hard clip(0,1) + strict monotonicity of enforce_forward_monotonic. Tolerates small backtracking (real hands micro-correct), allows natural overshoot past 1.0, and soft-compresses extreme overshoot with tanh so the path never flies far past the target.

Files:

  • Modify: src/ai_mouse/_postprocess.py (add function; do NOT delete old ones yet — removal happens in Task 4)
  • Test: tests/unit/test_postprocess.py

Interfaces:

  • Produces: soften_forward(forward: np.ndarray, backtrack_tol: float = 0.02, overshoot_span: float = 0.08) -> np.ndarray — returns a new array; Task 4 calls it with defaults.

  • Step 1: Write the failing tests

Append to tests/unit/test_postprocess.py:

from ai_mouse._postprocess import soften_forward


def test_soften_forward_tolerates_small_backtrack() -> None:
    # A 0.01 dip is within the 0.02 tolerance and must survive untouched.
    f = np.array([0.0, 0.30, 0.29, 0.60, 1.0])
    out = soften_forward(f)
    assert np.isclose(out[2], 0.29)


def test_soften_forward_limits_large_backtrack() -> None:
    # A 0.30 dip is noise; it gets pulled up to prev - tol.
    f = np.array([0.0, 0.50, 0.20, 0.70, 1.0])
    out = soften_forward(f)
    assert np.isclose(out[2], 0.50 - 0.02)


def test_soften_forward_allows_moderate_overshoot() -> None:
    # Overshoot past 1.0 is natural; small overshoot survives (compressed
    # but strictly > 1.0).
    f = np.array([0.0, 0.5, 0.9, 1.04, 1.0])
    out = soften_forward(f)
    assert out[3] > 1.0


def test_soften_forward_compresses_extreme_overshoot() -> None:
    # tanh compression: no output value may exceed 1 + overshoot_span.
    f = np.array([0.0, 0.5, 1.30, 1.50, 1.0])
    out = soften_forward(f)
    assert out.max() <= 1.0 + 0.08 + 1e-9
    assert out[2] > 1.0  # still an overshoot, not clipped flat


def test_soften_forward_no_lower_clip() -> None:
    # Small wind-up behind the start is allowed (warp_endpoints pins
    # the first point later; interior may be slightly negative).
    f = np.array([0.0, -0.01, 0.30, 0.70, 1.0])
    out = soften_forward(f)
    assert out[1] < 0.0
  • Step 2: Run tests to verify they fail

Run: uv run pytest tests/unit/test_postprocess.py -k soften_forward -v Expected: 5 failures/errors with ImportError: cannot import name 'soften_forward'

  • Step 3: Write the implementation

Add to src/ai_mouse/_postprocess.py (after enforce_forward_monotonic):

def soften_forward(
    forward: np.ndarray,
    backtrack_tol: float = 0.02,
    overshoot_span: float = 0.08,
) -> np.ndarray:
    """Softly regularise the forward axis without destroying natural motion.

    Real trajectories contain small backward corrections and overshoot
    past the target; hard clipping turns both into visible artifacts
    (stacked points, vertical walls). Instead:

    - Backtracking is tolerated up to ``backtrack_tol``; larger dips are
      raised to ``prev - backtrack_tol``.
    - Values above 1.0 are compressed with tanh so they asymptote at
      ``1 + overshoot_span`` (moderate overshoot survives, extremes
      cannot fly far past the target).
    - No lower clip: the endpoint warp pins the first point later.

    Args:
        forward: (T,) forward coordinates.
        backtrack_tol: max allowed per-step regression.
        overshoot_span: asymptotic max excess above 1.0.

    Returns:
        New (T,) array.
    """
    out = forward.copy()
    for i in range(1, len(out)):
        floor = out[i - 1] - backtrack_tol
        if out[i] < floor:
            out[i] = floor
    over = out > 1.0
    out[over] = 1.0 + overshoot_span * np.tanh((out[over] - 1.0) / overshoot_span)
    return out
  • Step 4: Run tests to verify they pass

Run: uv run pytest tests/unit/test_postprocess.py -k soften_forward -v Expected: 5 passed

  • Step 5: Commit
git add src/ai_mouse/_postprocess.py tests/unit/test_postprocess.py
git commit -m "feat: add soften_forward (backtrack tolerance + tanh overshoot compression)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"

Task 2: damp_start — continuous start damping

Replaces smooth_start, whose ×1/5-then-abrupt-release lateral damping creates start kinks. New version ramps damping with smoothstep so the weight approaches 1 continuously at the release boundary, and touches only lateral (forward regularisation is soften_forward's job).

Files:

  • Modify: src/ai_mouse/_postprocess.py
  • Test: tests/unit/test_postprocess.py

Interfaces:

  • Produces: damp_start(lateral: np.ndarray, n: int = 4) -> np.ndarray — returns a new array; Task 4 calls it with defaults.

  • Step 1: Write the failing tests

Append to tests/unit/test_postprocess.py:

from ai_mouse._postprocess import damp_start


def test_damp_start_dampens_early_lateral() -> None:
    lat = np.full(16, 1.0)
    out = damp_start(lat, n=4)
    assert out[1] < out[2] < out[3] < out[4] < 1.0  # monotone ramp
    assert np.all(out[5:] == 1.0)  # untouched past n


def test_damp_start_no_release_jump() -> None:
    # The weight at i=n must be close to 1 (continuous release):
    # smoothstep(4/5) = 0.896, vs the old linear 4/5 = 0.8.
    lat = np.full(16, 1.0)
    out = damp_start(lat, n=4)
    assert out[4] > 0.85


def test_damp_start_short_input_safe() -> None:
    lat = np.array([0.0, 0.5, 0.3])
    out = damp_start(lat, n=4)  # n capped to len//4 = 0 → no-op
    assert np.array_equal(out, lat)
  • Step 2: Run tests to verify they fail

Run: uv run pytest tests/unit/test_postprocess.py -k damp_start -v Expected: 3 failures with ImportError: cannot import name 'damp_start'

  • Step 3: Write the implementation

Add to src/ai_mouse/_postprocess.py:

def damp_start(lateral: np.ndarray, n: int = 4) -> np.ndarray:
    """Dampen lateral oscillation over the first ``n`` points, continuously.

    Weights follow smoothstep(i / (n+1)) so the damping releases smoothly
    into the untouched region (the old linear blend jumped from 0.8 to
    1.0 and left a visible kink).

    Args:
        lateral: (T,) lateral coordinates.
        n: number of leading points to dampen (capped at len//4).

    Returns:
        New (T,) array.
    """
    out = lateral.copy()
    n = min(n, len(out) // 4)
    for i in range(1, n + 1):
        t = i / (n + 1)
        w = t * t * (3.0 - 2.0 * t)  # smoothstep
        out[i] *= w
    return out
  • Step 4: Run tests to verify they pass

Run: uv run pytest tests/unit/test_postprocess.py -k damp_start -v Expected: 3 passed

  • Step 5: Commit
git add src/ai_mouse/_postprocess.py tests/unit/test_postprocess.py
git commit -m "feat: add damp_start (smoothstep lateral damping, no release kink)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"

Task 3: warp_endpoints — global residual correction

Replaces snap_endpoints. Instead of dragging the last 6 points toward (1,0) (which fights the trajectory's own direction and creates hooks), compute the first/last-point residuals and distribute the correction across the whole curve with smoothstep weights. Endpoints land exactly; local shape and approach direction are preserved.

Files:

  • Modify: src/ai_mouse/_postprocess.py
  • Test: tests/unit/test_postprocess.py

Interfaces:

  • Produces: warp_endpoints(forward: np.ndarray, lateral: np.ndarray) -> tuple[np.ndarray, np.ndarray] — returns new arrays with forward[0]==0.0, lateral[0]==0.0, forward[-1]==1.0, lateral[-1]==0.0 exactly; Task 4 calls it last in the pipeline.

  • Step 1: Write the failing tests

Append to tests/unit/test_postprocess.py:

from ai_mouse._postprocess import warp_endpoints


def test_warp_endpoints_exact_pin() -> None:
    f = np.linspace(0.05, 1.10, 32)
    l = np.linspace(0.03, -0.07, 32)
    fo, lo = warp_endpoints(f, l)
    assert fo[0] == 0.0 and lo[0] == 0.0
    assert fo[-1] == 1.0 and lo[-1] == 0.0


def test_warp_endpoints_identity_when_already_pinned() -> None:
    f = np.linspace(0.0, 1.0, 32)
    l = np.sin(np.linspace(0, np.pi, 32)) * 0.1
    l[0] = l[-1] = 0.0
    fo, lo = warp_endpoints(f.copy(), l.copy())
    assert np.allclose(fo, f, atol=1e-12)
    assert np.allclose(lo, l, atol=1e-12)


def test_warp_endpoints_preserves_smoothness() -> None:
    # Correcting a smooth curve must not introduce sharp local bends:
    # the warp adds a smoothstep-weighted offset, so the second
    # difference (discrete curvature proxy) stays small.
    f = np.linspace(0.02, 1.08, 32)
    l = np.full(32, 0.05)
    fo, lo = warp_endpoints(f, l)
    assert np.abs(np.diff(lo, 2)).max() < 0.01
    assert np.abs(np.diff(fo, 2)).max() < 0.01


def test_warp_endpoints_correction_local_to_each_end() -> None:
    # A start-only residual should barely move the last quarter.
    # (smoothstep weight at i=24/31 is ~0.13, so 0.08 residual leaves
    # ~0.010 there — threshold 0.02 gives margin without losing meaning)
    f = np.linspace(0.0, 1.0, 32) + 0.0
    l = np.zeros(32)
    f[0] = 0.08  # start residual only
    fo, _ = warp_endpoints(f, l)
    assert np.abs(fo[24:] - f[24:]).max() < 0.02
  • Step 2: Run tests to verify they fail

Run: uv run pytest tests/unit/test_postprocess.py -k warp_endpoints -v Expected: 4 failures with ImportError: cannot import name 'warp_endpoints'

  • Step 3: Write the implementation

Add to src/ai_mouse/_postprocess.py:

def warp_endpoints(
    forward: np.ndarray,
    lateral: np.ndarray,
) -> tuple[np.ndarray, np.ndarray]:
    """Warp the whole curve so endpoints land exactly on (0,0) and (1,0).

    Computes the residual of the first point vs (0, 0) and the last point
    vs (1, 0), then subtracts each residual weighted by a smoothstep that
    is 1 at its own end and 0 at the opposite end. Unlike tail-dragging,
    this preserves the trajectory's local shape and approach direction.

    Args:
        forward: (T,) forward coordinates.
        lateral: (T,) lateral coordinates.

    Returns:
        ``(forward, lateral)`` new arrays, endpoints pinned exactly.
    """
    t = np.linspace(0.0, 1.0, len(forward))
    w_end = t * t * (3.0 - 2.0 * t)      # smoothstep: 0 at start → 1 at end
    w_start = 1.0 - w_end                # mirrored

    res_f0, res_l0 = forward[0] - 0.0, lateral[0] - 0.0
    res_f1, res_l1 = forward[-1] - 1.0, lateral[-1] - 0.0

    fo = forward - w_start * res_f0 - w_end * res_f1
    lo = lateral - w_start * res_l0 - w_end * res_l1

    fo[0], lo[0] = 0.0, 0.0
    fo[-1], lo[-1] = 1.0, 0.0
    return fo, lo
  • Step 4: Run tests to verify they pass

Run: uv run pytest tests/unit/test_postprocess.py -k warp_endpoints -v Expected: 4 passed

  • Step 5: Commit
git add src/ai_mouse/_postprocess.py tests/unit/test_postprocess.py
git commit -m "feat: add warp_endpoints (global residual correction, shape-preserving)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"

Task 4: Wire new pipeline into mouse.py; remove old functions

Swap the pipeline in MouseModel.generate to the spec order (soft-monotonic → start damping → smooth both axes → global endpoint correction), then delete the three replaced functions and their tests. Golden tests will now fail — that is expected and fixed in Task 6; all other tests must pass.

Files:

  • Modify: src/ai_mouse/mouse.py:14-24 (imports), src/ai_mouse/mouse.py:106-109 (pipeline)
  • Modify: src/ai_mouse/_postprocess.py (delete snap_endpoints, smooth_start, enforce_forward_monotonic; also update the stale snap_endpoints cross-reference in any remaining docstring)
  • Modify: tests/unit/test_postprocess.py (delete the 6 tests of the removed functions and their two mid-file import lines)

Interfaces:

  • Consumes: soften_forward(forward) (Task 1), damp_start(lateral) (Task 2), warp_endpoints(forward, lateral) (Task 3), existing gaussian_smooth(x, sigma).

  • Step 1: Update mouse.py imports

Replace the import block at src/ai_mouse/mouse.py:15-24 with:

from ai_mouse._postprocess import (
    build_timestamps,
    damp_start,
    gaussian_smooth,
    resample_arc,
    sample_duration,
    soften_forward,
    truncnorm_sample,
    warp_endpoints,
)
  • Step 2: Replace the pipeline

Replace src/ai_mouse/mouse.py:106-109:

        forward, lateral = snap_endpoints(forward, lateral, self._seq_len)
        forward, lateral = smooth_start(forward, lateral)
        forward = enforce_forward_monotonic(forward)
        lateral = gaussian_smooth(lateral, sigma=1.0)

with:

        forward = soften_forward(forward)
        lateral = damp_start(lateral)
        forward = gaussian_smooth(forward, sigma=1.0)
        lateral = gaussian_smooth(lateral, sigma=1.0)
        forward, lateral = warp_endpoints(forward, lateral)
  • Step 3: Delete replaced functions and their tests

  • In src/ai_mouse/_postprocess.py: delete snap_endpoints (lines 34-62), smooth_start (65-80), enforce_forward_monotonic (83-92).

  • In tests/unit/test_postprocess.py: delete test_snap_endpoints_pins_first_and_last, test_snap_endpoints_preserves_middle, test_smooth_start_dampens_lateral, test_enforce_forward_monotonic_repairs_inversions, test_enforce_forward_monotonic_clips_to_unit_interval, and the from ai_mouse._postprocess import snap_endpoints / from ai_mouse._postprocess import enforce_forward_monotonic, smooth_start import lines.

  • Step 4: Run the unit suite (golden mouse failures expected)

Run: uv run pytest tests/unit -v Expected: everything passes EXCEPT tests/unit/test_golden.py::test_mouse_golden[...] cases, which may exceed the path envelope (behavior intentionally changed; re-baselined in Task 6). If anything else fails, fix before committing. In particular test_mouse.py (endpoint snap, seed reproducibility, shape) must pass.

  • Step 5: Commit
git add src/ai_mouse/mouse.py src/ai_mouse/_postprocess.py tests/unit/test_postprocess.py
git commit -m "feat: rework mouse post-processing pipeline (soft monotonic, global endpoint warp)

Golden mouse baselines temporarily failing; re-captured in follow-up commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"

Task 5: Euler → Heun sampling

Second-order predictor-corrector at the same 10 steps (NFE 10 → 20; each call is a small d_model=128 transformer, ~1-2 ms CPU). Reduces integration error and sampling jitter.

Files:

  • Modify: src/ai_mouse/mouse.py:27 (constant), src/ai_mouse/mouse.py:93-97 (ODE loop)

Interfaces:

  • Consumes: the existing ONNX session I/O contract session.run(["v"], {"x_t", "t", "cond"}) — unchanged.

  • Step 1: Replace the ODE loop

Rename the constant at src/ai_mouse/mouse.py:27:

_N_ODE_STEPS = 10

Replace the loop at src/ai_mouse/mouse.py:93-97:

        dt = 1.0 / _N_EULER_STEPS
        for step in range(_N_EULER_STEPS):
            t = np.full((1,), step * dt, dtype=np.float32)
            v = self._session.run(["v"], {"x_t": x, "t": t, "cond": cond})[0]
            x = x + v * dt

with:

        # Heun (2nd-order predictor-corrector): same step count as the old
        # Euler loop but far lower integration error for 2x NFE.
        dt = 1.0 / _N_ODE_STEPS
        for step in range(_N_ODE_STEPS):
            t0 = np.full((1,), step * dt, dtype=np.float32)
            v1 = self._session.run(["v"], {"x_t": x, "t": t0, "cond": cond})[0]
            x_pred = (x + v1 * dt).astype(np.float32)
            t1 = np.full((1,), (step + 1) * dt, dtype=np.float32)
            v2 = self._session.run(["v"], {"x_t": x_pred, "t": t1, "cond": cond})[0]
            x = x + (v1 + v2) * (dt / 2.0)
  • Step 2: Run the unit suite (same expectation as Task 4)

Run: uv run pytest tests/unit -v Expected: all pass except test_mouse_golden envelope cases (still pending re-baseline). test_mouse.py::test_mouse_model_seed_reproducibility must pass — Heun adds no new randomness.

  • Step 3: Commit
git add src/ai_mouse/mouse.py
git commit -m "feat: switch flow ODE sampling from Euler to Heun (10 steps, NFE 20)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"

Task 6: Quality regression test, golden re-baseline, CHANGELOG, visual verification

Add a numeric guard against the two artifact classes (sharp turns near the endpoint, vertical walls), re-capture golden_mouse.npz from the new implementation (documented procedure in tests/unit/test_golden.py docstring), and verify visually.

Files:

  • Test: tests/unit/test_mouse.py (append)
  • Modify: tests/unit/data/golden_mouse.npz (re-captured binary)
  • Modify: CHANGELOG.md

Interfaces:

  • Consumes: generate(start, end, *, seed, click) public API.

  • Step 1: Write the quality regression test (fails on OLD pipeline, passes on new)

Append to tests/unit/test_mouse.py (note: this file has no module-level numpy/generate imports — the test is self-contained):

def test_no_sharp_turns_or_walls_near_endpoint() -> None:
    """Guard against the two endpoint artifact classes:

    - sharp turns (>90°) between consecutive substantial segments in the
      final approach (the old tail-drag created hooks);
    - vertical walls: many points stacked at the target's forward
      position (the old clip(0,1) stacked overshoot at forward=1).
    """
    import math

    import numpy as np

    from ai_mouse import generate

    cases = [((100, 300), (900, 350)), ((100, 100), (700, 600)),
             ((800, 200), (150, 550))]
    for (start, end) in cases:
        for seed in range(6):
            pts = generate(start, end, seed=seed, click=False)
            arr = np.array([(x, y) for x, y, _ in pts], dtype=float)
            tail = arr[-12:]
            seg = np.diff(tail, axis=0)
            lens = np.linalg.norm(seg, axis=1)
            # Only consider substantial segments: integer-pixel staircase
            # on 1-2 px steps produces spurious 90° angles.
            keep = lens >= 3.0
            headings = np.arctan2(seg[keep][:, 1], seg[keep][:, 0])
            if len(headings) >= 2:
                turns = np.abs(np.diff(np.unwrap(headings)))
                max_turn = math.degrees(turns.max())
                assert max_turn < 90.0, (
                    f"{start}->{end} seed={seed}: {max_turn:.0f}° turn "
                    f"in final approach"
                )
            # Vertical wall: >=4 consecutive tail points within 2 px of
            # the target x while spanning >10 px of y.
            near_x = np.abs(tail[:, 0] - end[0]) <= 2.0
            run = 0
            for i, flag in enumerate(near_x[:-1]):  # exclude final point
                run = run + 1 if flag else 0
                assert run < 4 or np.ptp(tail[near_x][:, 1]) <= 10.0, (
                    f"{start}->{end} seed={seed}: vertical wall at target x"
                )
  • Step 2: Run the new test

Run: uv run pytest tests/unit/test_mouse.py::test_no_sharp_turns_or_walls_near_endpoint -v Expected: PASS (the new pipeline removed the artifacts). If it fails, treat it as a real defect in Tasks 1-5 — do not loosen the thresholds; investigate which pipeline step reintroduces the artifact.

  • Step 3: Re-capture the mouse golden baseline

Write a throwaway script (scratchpad or temp path, NOT committed):

# recapture_golden.py — regenerate golden_mouse.npz from new implementation
import numpy as np
from ai_mouse import generate

CASES = [
    ((100, 200), (900, 400)), ((500, 500), (500, 100)),
    ((200, 600), (800, 200)), ((100, 100), (130, 110)),
    ((50, 50), (1500, 900)), ((400, 300), (500, 300)),
    ((300, 300), (700, 700)), ((600, 400), (200, 100)),
]
out = {}
for ci, (s, e) in enumerate(CASES):
    for seed in range(4):
        pts = generate(s, e, seed=seed)
        out[f"case{ci}_seed{seed}"] = np.array(pts, dtype=np.int64)
np.savez_compressed("tests/unit/data/golden_mouse.npz", **out)
print(f"wrote {len(out)} golden traces")

Run: uv run python <path>/recapture_golden.py Expected: wrote 32 golden traces

  • Step 4: Full unit suite must be green

Run: uv run pytest tests/unit -v Expected: ALL pass, including all 32 test_mouse_golden and all 32 test_scroll_golden cases (scroll goldens untouched — if any scroll test fails, something leaked outside mouse scope; stop and investigate).

  • Step 5: Update CHANGELOG

Add under the top of CHANGELOG.md (after the intro, before ## [0.2.0]):

## [Unreleased]

### Changed

- Mouse post-processing pipeline reworked to remove endpoint artifacts:
  hard forward clip → backtrack-tolerant soft monotonic with tanh
  overshoot compression (`soften_forward`); tail-drag endpoint snapping →
  whole-curve smoothstep residual correction (`warp_endpoints`); abrupt
  start damping → continuous smoothstep damping (`damp_start`);
  gaussian smoothing now applied to both axes.
- Flow ODE sampling switched from 10-step Euler to 10-step Heun
  (predictor-corrector); ~2x model calls per trajectory, still ~40 ms CPU.
- `tests/unit/data/golden_mouse.npz` re-baselined against the new
  pipeline (intentional behavior change; scroll goldens unchanged).
  • Step 6: Visual verification (diagnostic plot + Web UI)
  1. Re-run the diagnostic script from the investigation (same 4 cases × 6 seeds; it lives in the session scratchpad as diag_traj.py) and visually compare against the "before" plot: no vertical walls in end zoom, no hooks, no start kinks; turns>45deg counts in the numeric output should drop sharply vs the before-values (3-8 per trace).
  2. Start the Web UI: uv run python tools/serve.py, open the verify page, and have the user visually approve. This is the final acceptance gate — post-processing is Python-side, so no ONNX re-export is needed, but the server must be (re)started to pick up the new library code.
  • Step 7: Commit
git add tests/unit/test_mouse.py tests/unit/data/golden_mouse.npz CHANGELOG.md
git commit -m "test: quality guard for endpoint artifacts; re-baseline mouse goldens

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"

Acceptance summary (from spec)

  • No vertical wall / hooks / start kinks in diagnostic plots (Task 6 step 6)
  • turns>45° count drops sharply vs baseline (was 1-8 per trace)
  • uv run pytest tests/unit fully green, goldens re-baselined
  • User approves Web UI verify page visually
  • Out of scope confirmed untouched: scroll subsystem, training code, ONNX assets