Sky.Spa v1 — progress tracker (autonomous build)

Resume protocol: read this file + .claude/AUTONOMOUS_GOAL.md first. This tracks the unattended build of the desktop/mobile-first explicit-boundary Sky.Spa v1. Updated at every phase boundary. Work branch: exp/spa. Verified prototype baseline: exp/spa-prototype.

Status: P5 DONE ✅ — P6 next (Judge + docs/templates + final sweep)

PhaseStateNotes
P1 — productionize + land partition✅ donepartition + census/kernel/coverage fixes on exp/spa (@c39dd8a0). Full §0.2.1 verified SERIALLY green: cargo test --workspace=0, example-sweep=0, conformance=0, 29 harness gates pass, entry_exit_contract 3/3, GOOS=js rt build=0, Sky.Live 09/19 build=0, spa render ALL PASS.
P2 — client-side diff renderer✅ doneFull re-render replaced by diffTrees(prev,new,clientState) → []Patch applied by sky-id (spaApplyPatches); spaPrev *VNode kept across dispatches; first render still full-mounts. Focus/caret/dirty-input authority ported from __skyApplyPatches to the Go/syscall.js Patch VALUE model. spa-input/ acceptance test 23/23 PASS (focus retained, caret preserved mid-string + across programmatic value write, value not clobbered, node identity stable, 0 elements created per keystroke = minimal patch); spa-counter/ still ALL PASS. diffTrees/live_core.go UNCHANGED — all edits in the two //go:build js files, so Sky.Live server diff is untouched.
P3 — interpretCmd real effects✅ doneinterpretCmd runs real effects: Cmd.perform on a per-perform cooperatively-scheduled goroutine (wasm single thread, NOT an OS thread) that dispatches toMsg(result); sync kernels (Time.now/Random) return inline; async Http.get/post split to a browser-fetch kernel (http_wasm.go) that BLOCKS on a channel the Promise fills and returns a real Result (required by typed-emit's TaskCoerceT). subscriptions : model -> Sub msg added to Std.Spa.config; the driver reconciles Sub.every timers after every dispatch (start/stop/leave via setInterval/clearInterval). Cmd.publish = documented client no-op. Headless acceptance: spa-perform / spa-sub / spa-http ALL PASS; spa-counter + spa-input (23/23) still PASS. live_core.go + all !js runtime UNCHANGED; only shared change is the Http_get/Http_post build-split (host net/http impl moved verbatim to http_notjs.go).
P4 — Std.Spa v1 + explicit boundary✅ doneClient-side routing (History API) + the explicit typed server boundary. Std.Spa gains Route/route/withRoutes/withNotFound/withOnNavigate (opt-in builders — config stays the 4 TEA fields so the 5 route-less spa apps keep compiling; same route/withOnNavigate names as Sky.Live) and getJson/postJson (pure Sky over Cmd.perform+Http+Codec, NO new kernel). Runtime: spa_core.go (portable) config-map + Spa_route/Spa_with* + spaMatchRoute/spaResolveRoutes (reimplements Sky.Live's matchRoute/splitPath client-side — live.go NOT touched); live_wasm.go (js-only) deep-link at mount + document-click interception (pushState, skips external/target=_blank/download/sky-external/modified-click) + popstate + RecordUpdate sets model.Page (Live convention) + onNavigate via TEA step. Router installed ONLY when routes exist. shared vs js: shared portable changes = spa_core.go, Std/Spa.sky, kernel_surface.rs, docs/coverage/; js-only = live_wasm.go. Tests: spa-router/ routing ALL PASS (deep-link, intercepted no-reload nav, pushState, onNavigate, popstate, notFound, external passthrough); spa-boundary/ real wasm-client↔stateless-Sky-backend round-trip ALL PASS with ONE symlinked Shared.sky (shared-type-flows-both-ways proven: a field added to Shared breaks BOTH compiles). Verify (serial): build.sh=0, GOOS=js rt build=0, go build ./...+go test ./rt/...=0, cargo test -p sky=0, kernel_surface+dark-module ratchets+both census --check=PASS, sky doc Std.Spa OK, Sky.Live 09+19 clean-slate build + run HTTP 200 (sky-nav intact), all 5 prior spa apps rebuilt+headless ALL PASS.
P5 — real e2e example + verify✅ doneexamples/60-spa-todos — the culminating full-stack Sky.Spa app. Client (wasm) = Model { page, ui, data }, Std.Ui Element view (cross-platform; DONE-list criterion 6 — the Spa client renderer paints Element to the DOM, verified headlessly), pure client-local UI (new-todo text, filter, edit buffer) with zero round-trip, durable data via Spa.getJson/postJson + shared Std.Codec, History-API filter routes. Server = stateless Sky.Http.Server + SQLite (Std.Db.Store), re-validates every request, Server.api (CSRF-bypassed stateless JSON API), serves the client same-origin. shared/Shared.sky symlinked (mode 120000) into both — one wire contract. e2e run_roundtrip.sh (real wasm client ↔ real backend, headless): 24/24 PASS — durable add/toggle/rename/delete persist (backend curl truth), pure UI (typing + 3 filter navs + edit buffer) makes provably 0 network calls, routing changes the view without reload, a reloaded client rehydrates from the backend. Bundle: main.wasm 9,493,611 B raw / 2,519,062 B gzip (desktop/mobile-embed weight; web is the documented v2 open decision). Browser: extension DISCONNECTED (list_connected_browsers → []); app serves same-origin (/, /main.wasm, /wasm_exec.js all HTTP 200), run.sh + README give the exact URL/steps — headless is the acceptance proof, real-browser pixel check pending the extension. Verify (serial): build.sh=0, GOOS=js rt build=0, go build ./...+go test ./rt/...=0, cargo test -p sky=0, gates_measure_a_fresh_compiler 21/21 (both run scripts carry the fresh-compiler guard; also fixed the pre-existing boundary-script miss on the branch base), denominators+coverage-ledger --check=PASS (no regen needed), coerce-floor golden gains the two measured rows (client narrow=238, server narrow=131), example builds clean-slate, Sky.Live 09+19 build+run HTTP 200, all 7 prior spa apps PASS.
P6 — Judge + docs/templates + final sweep⏳fresh-context Judge vs DONE list

Verified baseline (do not re-litigate)

Known P1 risks to handle

Decisions / deviations log