# Front-end verification log

Started 2026-08-18. This file is written incrementally; if this agent dies, read this before
restarting.

## Plan

1. Read intended design (done): docs/plans/2026-08-18-m1-climbable-slice.md Task 13-14,
   docs/specs/2026-08-18-design-system.md.
2. Read actual ui-src/ implementation and diff against intent.
3. Static checks (build, no external links, no "Loading", --gold- usage).
4. Set up throwaway problem + server instance in scratchpad, try Puppeteer; fall back to static
   analysis + reasoning trace if flaky.
5. Verify the three behaviors.

## Progress log

- Read intent: Task 13-14 of the M1 plan and the design-system spec (Seam Lantern, reduced motion,
  zone rule, gold rule). Read actual implementation: SeamLantern.tsx, SkeletonSprint.tsx, Editor.tsx,
  MotionProvider.tsx, motion.ts, grain.ts, ClockRing.tsx, Shimmer.tsx, tokens.css, main.tsx.
  Finding: the shipped code is materially *ahead* of the plan's naive code samples — it fixes bugs
  the plan's own snippets would have had (regex injection from corpus-authored seam names escaped;
  an infinite re-render loop from array-identity deps in useSeamNodes closed via joined-string deps
  + value-equality bail-out; CodeMirror's gutter not refreshing on a pure StateField dispatch fixed
  via `lineMarkerChange`; GutterMarker `eq()` added so keystrokes don't replay the drop animation).
  This means the two previous agents plausibly did real, careful work before dying to network flake,
  not that the code is unbuilt.

- **Static check: no "Loading" text.** `grep -rn "Loading" ui-src/src` → zero matches (exit 1).
  PASS.

- **Static check: --gold- usage.** `grep -rn "gold"` across ui-src/src hits only: Lozenge.tsx
  ("earned" tone), SeamLantern.tsx (lit node/connector/glow), SkeletonSprint.tsx (passed-result
  strong text), ExtractDrill.tsx (passed score numeral, found-chip styling), VocabDrill.tsx
  (completed-rep progress dots, "Correct shape" success text), tokens.css (`.panel.earned`). All are
  achievement contexts (lit seam, passed rung/drill, satisfied progress step). No chrome, link or
  border use found. PASS.

- **Static check: reduced motion, read (not yet browser-observed).**
  - `MotionProvider.tsx`: wraps app in `MotionConfig reducedMotion={reduced ? 'always' : 'never'}`
    (collapses all `motion/react` springs) AND only calls `mountGrain()` when `!reduced` (grain DOM
    node is never even created, not just hidden).
  - `AmbientDepth` returns `null` outright when `useReducedMotion()` is true — no ambient drift node.
  - `ClockRing.tsx`: breathing/dimming animation is `reduced ? {} : {...}` — ring does not breathe or
    dim under reduced motion, but the SVG progress arc's own `strokeDashoffset` transition still
    plays (via `motion.circle`'s own `transition` prop, not gated by MotionConfig since it's an
    explicit prop) — CONFIRMED this is fine, `MotionConfig`'s `reducedMotion="always"` overrides
    *all* motion values including explicitly-specified transitions per the `motion` library's
    documented behavior, collapsing them to instant, so this does not violate the spec. (Read-only
    inference from library docs, not executed.)
  - `Shimmer.tsx` uses a raw CSS `@keyframes shimmer` animation, NOT gated by MotionConfig (it's not
    a `motion.*` element). However `tokens.css` has a global
    `@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .01ms
    !important; transition-duration: .01ms !important; } }` which forces this (and the Editor's
    `diag-drop`/`editor-sheen` keyframes, also raw CSS) to near-zero duration regardless of
    specificity. This closes what would otherwise be a gap (motion-library gating alone would have
    missed all raw-CSS animations). PASS by static reading; not yet confirmed by an actual OS-level
    reduced-motion browser run (see below).
  - Seam Lantern under reduced motion: `SeamLantern.tsx`'s node color/scale/glow are all driven by
    `motion.span`/`motion.div` `animate` props with `spring.*` transitions — these go through
    `MotionConfig`, which per `motion`'s reduced-motion contract still applies the *end state* of the
    animation (just without the tween), so the node still visibly changes to gold on satisfaction.
    This matches the requirement "Seam Lantern still changing colour" under reduced motion.

- **Static check: no external hosts in built `ui/index.html`.** Read built file: only
  `/assets/index-*.js` and `/assets/index-*.css`, both same-origin relative paths. No external
  `<link>`/`<script>`. PASS. (Grepped the built JS/CSS bundle too for `https?://` — only hits are
  inert string constants: XML/SVG namespace URIs used by React's DOM renderer, and a
  `react.dev/errors/` string embedded in React's minified error decoder — neither is a `<link>` or
  `<script>` tag and neither is fetched at runtime.)

- **`node tools/build-ui.mjs` — succeeded**, but investigated further: `ui-src` had no
  `tsconfig.json` and no `@types/react`/`@types/react-dom` devDependency, so `vite build` (which
  transpiles via esbuild and never type-checks) was giving a false sense of "no TypeScript errors" —
  there was in fact no mechanism by which a TS error could ever have been caught, before or after
  any code change. Fixed within the `ui-src/` boundary: added `ui-src/tsconfig.json` (strict,
  bundler resolution, React 19 JSX) and added `@types/react@^19.2.18` / `@types/react-dom@^19.2.4` to
  `ui-src/package.json` devDependencies. Ran `npx tsc --noEmit` from `ui-src/` — **zero errors**
  against the real project source. Re-ran `node tools/build-ui.mjs` — still succeeds, same output.
  This is a real fix (closing a verification gap), not a refactor of behavior.

- **Runtime harness set up.** Puppeteer was not `require`-able from the project (only present as a
  transitive dep of a global mermaid-cli install), so it was installed fresh inside the scratchpad as
  its own throwaway npm project (`npm init -y && npm install puppeteer`) — nothing under `ui-src/` or
  elsewhere in the repo was touched for this. A throwaway problem
  (`.../scratchpad/corpus/parking-lot/problem.json`, `required_types: ["ParkingLot","Ticket"]`,
  `required_seams: ["PricingPolicy"]`) and a throwaway server instance
  (`.../scratchpad/run-server.mjs`, importing the real `server/index.mjs` via `createApp({corpusRoot,
  dbFile, uiDir: <repo>/ui, workspace, tier: 'T3'})`) were started on port 7778, serving the real
  built `ui/`. Confirmed via `curl /api/status` and `/api/problems` that it serves the throwaway
  problem correctly.

## 1 · Seam Lantern lights live, no server round trip — CONFIRMED by direct browser observation

Method: Puppeteer driving a real Chromium against the running app (not inference). Script:
`scratchpad/test1-seam-lantern.mjs`. Navigated Ladder → "Parking Lot" card → Brief →
"Phase 2 · Skeleton sprint · 12 min" (had to disambiguate from the sidebar's phase-2 nav item, which
also contains the substring "Skeleton sprint" but is a route-less needs-a-problem stub when clicked
with no problem context — that's a reasonable UX fallback, not a bug, just a test-script gotcha).

Read the computed `background-color` of each seam node's dot span directly from the live DOM (no
mocking) before and after actions, and recorded every `/api/*` request fired during each window.

- Typed `\ninterface PricingPolicy {}\n` into the live CodeMirror editor (Main.java, the only file at
  that point), pressed neither Compile nor Submit. After a 400 ms wait (well past the 150 ms
  debounce):
  ```
  AFTER typing: [{"name":"ParkingLot","bg":"rgb(56, 66, 79)"},
                 {"name":"Ticket","bg":"rgb(56, 66, 79)"},
                 {"name":"PricingPolicy","bg":"rgb(221, 187, 123)"}]
  API calls fired during typing window: []
  ```
  `rgb(56,66,79)` is `--line-hi` (unlit). `rgb(221,187,123)` is within a few units per channel of
  `--gold-400` (`#d9b779` = `rgb(217,183,121)`) — the spring was still mid-flight at the 400 ms
  sample, clearly converging on gold. **Zero `/api/*` requests fired** during the entire typing
  window — confirmed client-side-only, no server round trip.

- Multi-file check (the "reads all open files, not just the active one" requirement): clicked
  "+ file", supplied `Ticket.java` via the `window.prompt` dialog (auto-populates
  `public class Ticket {\n}\n` and switches the active tab to it, per `addFile`'s own logic). Then
  clicked back to the `Main.java` tab, leaving `Ticket.java` open but inactive, and re-read the rail:
  ```
  AFTER switching back to Main.java (Ticket.java still open, inactive):
  [{"name":"ParkingLot","bg":"rgb(56, 66, 79)"},
   {"name":"Ticket","bg":"rgb(217, 183, 121)"},
   {"name":"PricingPolicy","bg":"rgb(217, 183, 121)"}]
  API calls fired during file-add/switch window: []
  ```
  Ticket's node is fully settled gold (`rgb(217,183,121)` — exact `--gold-400`) *while Main.java, not
  Ticket.java, is the active tab*. This directly confirms `useSeamNodes` is driven by the join of all
  open files (`allSource = files.map(f => f.content).join('\n')` in `SkeletonSprint.tsx`), not the
  active file alone. Again zero `/api/*` calls.

**Verdict: PASS, confirmed by live observation, not inference.** The flagship trick works exactly as
specified: live, debounced, client-only, multi-file-aware.

## 2 · Compile diagnostics land on the exact line — CONFIRMED by direct browser observation

Method: Puppeteer, same running instance. Script: `scratchpad/test2-diagnostics.mjs`. Replaced
Main.java's body with:
```
public class Main {
    public static void main(String[] args) {
        int x = 5
        System.out.println(x);
    }
}
```
(missing semicolon on the 3rd line), pressed **Compile**, and captured the real `/api/compile`
network response as ground truth alongside the live DOM.

```
server diagnostic (source of truth): [{"path":"Main.java","line":3,"column":26,"severity":"error","message":"';' expected"}]
diagnostics panel code labels:        ["Main.java:3"]
row alignment check: { "markerFoundOnExactRow": true, "markerTitleOnRow": "error: ';' expected" }
```

Row-alignment was checked by comparing `getBoundingClientRect().top` of the `.cm-line` at index
`line-1` against every `[title]` element inside `.cm-diag-gutter`, asserting one lands within 4px
(i.e. the same visual row) — not by trusting the marker's mere existence. It matched exactly, and its
`title` attribute (`error: ';' expected`) is exactly the hover text `DiagMarker.toDOM` sets
(`${severity}: ${message}`). The diagnostics panel below the editor lists `Main.java:3`, matching the
gutter and the server response exactly. (Editor auto-indent added extra whitespace to typed lines,
visible in the raw content dump above — cosmetic only, did not shift line numbers.)

**Verdict: PASS, confirmed by live observation.** Gutter wiring, hover message, and panel listing all
land on the precise line the real `javac`-backed parser reports.

## 3 · Reduced motion leaves every surface usable — CONFIRMED by direct browser observation

Method: Puppeteer with `page.emulateMediaFeatures([{name:'prefers-reduced-motion', value:'reduce'}])`
set **before** navigation (so the OS preference is genuinely emulated at first paint, not toggled
after). Scripts: `scratchpad/test3-reduced-motion.mjs`, `scratchpad/test4-extract-reduced.mjs`.

```
grain element present: false
ambient drift div present: false
probe: raw CSS keyframe duration under reduced motion: 1e-05s        (= 0.01ms, i.e. the global
                                                                        tokens.css media-query rule
                                                                        fired on a live `shimmer`
                                                                        animation probe)
Vocab drill (Phase 0) responded under reduced motion: true
ClockRing transform sample 1 / sample 2 (900ms apart): "none" / "none"  (static — no breathing)
Seam Lantern state under reduced motion after typing:
  [{"name":"ParkingLot","bg":"rgb(56, 66, 79)"},       <- still unlit, correct
   {"name":"Ticket","bg":"rgb(56, 66, 79)"},            <- still unlit, correct
   {"name":"PricingPolicy","bg":"rgb(224, 188, 123)"}]  <- turned gold, confirms Trick 1 survives
page errors observed during the whole run: []
```
Separately, Phase 1 (Extract requirements) was filled and submitted under the same emulated
condition and produced a graded result with no page errors:
```
Extract drill graded under reduced motion: true
page errors: []
```
Phase 2 (Skeleton sprint) was exercised in the same run as the Seam Lantern check above and rendered
and accepted keyboard input normally.

**Verdict: PASS, confirmed by live observation across all three drills.** No grain, no ambient
drift, no ring breathing, and the global CSS rule verifiably collapses raw-CSS-keyframe animations
(Shimmer/diag-drop/editor-sheen) to near-zero duration — while the Seam Lantern still changes colour
on satisfaction, and every drill remained fully interactive and gradeable.

## Cleanup

Stopped the throwaway server (background task, port 7778). Scratchpad harness files
(`run-server.mjs`, `test1..4-*.mjs`, `debug1.mjs`, throwaway `corpus/`, `data/`, `workspace/`,
`node_modules/`) live only under the session scratchpad, never under the repo. `ui-src/` changes are
limited to the tsconfig/type-checking fix recorded above — no other file under `ui-src/` was
modified; all three flagship behaviours were already correctly implemented.

## Four quick checks — final status

1. `node tools/build-ui.mjs` succeeds with no TypeScript errors — **PASS**. (Required adding
   `ui-src/tsconfig.json` + `@types/react`/`@types/react-dom` devDeps for the check to be meaningful
   at all; see above. `npx tsc --noEmit` from `ui-src/` now reports zero errors against real project
   source.)
2. No `<link>`/`<script>` in built `ui/index.html` points at an external host — **PASS**. Verified
   by reading the built file (only same-origin `/assets/...`) and grepping the built JS/CSS bundle for
   `https?://` (only hits: inert XML/SVG namespace URI string constants and a `react.dev/errors/`
   string baked into React's minified error decoder — neither is a request-issuing tag or a runtime
   fetch).
3. The word "Loading" appears nowhere in `ui-src/src/` — **PASS**. `grep -rn "Loading" ui-src/src`
   → zero matches.
4. `--gold-` used only for achievement — **PASS**. Every usage found (Lozenge "earned" tone, Seam
   Lantern lit/connector/glow, passed-result strong text and score numeral, found-chip styling in
   ExtractDrill, completed-rep progress dots and "Correct shape" text in VocabDrill, `.panel.earned`
   in tokens.css) is an achievement context. No chrome, link or border usage found.

## Files changed (all under the permitted boundary)

- `ui-src/tsconfig.json` — **created**. Minimal strict config (ES2020, DOM libs, `moduleResolution:
  Bundler`, `jsx: react-jsx`, `noEmit`) so `npx tsc --noEmit` can actually run against the project.
  Previously there was no tsconfig at all, so no TypeScript error could ever have been caught by any
  tool — `vite build` only transpiles via esbuild and never type-checks.
- `ui-src/package.json` — **added devDependencies** `@types/react@^19.2.18`,
  `@types/react-dom@^19.2.4` (and the corresponding `ui-src/package-lock.json` update from `npm
  install`). Without these, `tsc` cannot resolve JSX/React types at all and every file fails with
  "Could not find a declaration file for module 'react'" — a tooling gap, not a real code issue, but
  one that made the "no TypeScript errors" quick check unverifiable. With them, the real project
  source type-checks clean (0 errors).
- No other file under `ui-src/` was modified. `SeamLantern.tsx`, `Editor.tsx`, `SkeletonSprint.tsx`,
  `MotionProvider.tsx`, `motion.ts`, `grain.ts`, `ClockRing.tsx`, `tokens.css`, `Shimmer.tsx`,
  `App.tsx`, `Ladder.tsx`, `Brief.tsx`, `VocabDrill.tsx`, `ExtractDrill.tsx`, `Shell.tsx` were all read
  and verified but needed no changes — the three flagship behaviours already work correctly, and in
  places go beyond the plan's own naive reference code (regex-escaping corpus-authored seam names,
  closing an infinite-render loop the plan's `useSeamNodes` snippet would have had, fixing
  CodeMirror's gutter-refresh-on-StateField-only-dispatch gap, adding `GutterMarker.eq()` so keystrokes
  don't replay the drop animation).

## Anything not verified

- Did not test macOS/Linux paths (Windows only, per the environment).
- Did not exercise the frontier tier (F1–F13) or signature-move layer (curtain reveal, magnetic
  cursor, etc.) — these are explicitly M5–M6/deferred per the design spec and outside Task 13–14's
  scope; not part of the three required checks.
- Did not verify sound (M6, not yet built) or the command palette (M6, not yet built) — correctly
  absent per the plan's deferral list, not a gap in M1.
- The exact pixel-perfect settle-timing (`spring.soft` 2px drop) and sheen travel were read from
  source and confirmed to fire (via `settle`/`sheenKey` state changes observed indirectly through the
  DOM), but frame-by-frame animation smoothness was not visually inspected — only end states and
  absence/presence of motion were checked, which is what the three required checks call for.

## Status: DONE

All three required behaviors were confirmed by live Puppeteer observation against the real server
and real built UI (not inference): the Seam Lantern lights live with zero network round trips and
correctly reads all open files; compile diagnostics land on the exact reported line with correct
hover text and panel listing; and reduced motion removes grain/drift/breathing/shimmer-travel while
every drill, including the Seam Lantern's colour change, remains fully functional. All four quick
checks pass. One real gap was found and fixed within the `ui-src/` boundary: there was no
TypeScript-checking mechanism at all (no tsconfig, no React type declarations), so "no TypeScript
errors" was previously unverifiable rather than actually verified; that is now fixed and the real
source is confirmed to type-check clean.
