Sky.Spa de-reflection (prod web) — progress tracker

Resume: read this + .claude/AUTONOMOUS_GOAL.md. Goal: de-reflect the Sky.Spa CLIENT core so a real Sky-emitted client compiles under TinyGo to a web-viable bundle, ISOLATED in a client runtime so Sky.Live's reflect path is untouched. Branch exp/spa. TinyGo at /opt/homebrew/bin (0.41.1).

Measured baseline (surrogates, prod-web.md)

Phases

PhaseStateNotes
D0 — Architecture-Consult✅map spa-counter client reflection surface + de-reflection mechanism + TinyGo constraints + phased plan
D1 — dispatch de-reflection✅typed-closure emission (rt.SpaFns) — the 4 dispatch sites, client-only
D2 — spa-counter → TinyGo✅real Sky-emitted counter compiles+renders under TinyGo; 1.59 MB raw / 521 KB gz
D3 — codec + ADT de-reflection⏳reflection-free client codec/adt for data-decoding (todos)
D4 — todos → TinyGo + e2e⏳real todos client TinyGo build + render + e2e; measure
D5 — Judge + isolation proof + full sweep⏳Sky.Live untouched; §0.2.1 green; DONE list

Decisions / findings

D0 findings (consult, verified on-machine)

D1 findings (implemented)

D2 findings (implemented — option A, tag-split, one rt package)

D1+D2 DONE (@e825d428, pushed) + D3 plan (consult)

D3 progress (coordinator, hands-on foreground)

D3 DEFINITIVE finding — the coercion narrow needs CODEGEN, not runtime fast-paths

Chasing the runtime panic forward (FieldByName→ResultCoerce ✓ → ConvertibleTo→ coerceInner/Coerce) reaches a FUNDAMENTAL wall: coerceInner[[]Todo_R] / Coerce[[]Todo_R] must narrow []any (each element a BOXED Todo_R) into a typed []Todo_R. In the generic runtime helper the element type is erased, so the narrow is inherently reflect (ConvertibleTo/reflect.Convert) — TinyGo-unimplemented. Narrowing it reflection-free requires the STATIC element type, which exists only at CODEGEN (the emit site knows Todo_R). So the emit must either (a) produce already-typed decode results (no post-decode coerce), or (b) emit the narrow using the known element type (e.g. AsListT[Todo_R] directly on an A=any result rather than coerceInner[[]Todo_R]). This is the pervasive "reflection-free coercion" codegen work (prod-web.md's large lever), NOT a bounded runtime patch.

Achieved (committed, verified)

Remaining for functional todos (record-app) under TinyGo + the size cliff

  1. Codegen: emit the client's any→typed coercion reflection-free using the static element/field types (route slice narrows through AsListT[Elem] on A=any results; struct narrows via typed field assigns) — gated on the SPA client target so the server reflect coercion is unchanged.
  2. Then D3c isolation (client-only reflection-free variants of the residual reflect refs: sky_call cold-fallbacks, unwrapADTShape in HtmlToVNode, adaptFuncValue) so the client graph reaches ZERO reflect → DCE trips the size cliff (644 KB → target ~100 KB). This is a bounded-but-substantial codegen effort (multi-session), now precisely scoped. Resume here.

Compiler-wide de-reflection (post-mandate) — coercion, general

Re-classification (N-strikes on the dispatch lever) — first-class-function ABI

LANDED on exp/spa (general, compiler-wide reflection-free wins, committed):

Todos client under TinyGo now: renders + routes + filters + persists to backend. RESIDUAL (re-classified — NOT another per-site fix): first-class FUNCTION VALUES and HOF PARAMS lower to DIVERSE typed Go func shapes. Root example: Std.Spa.getJson's toMsg param lowers to func(rt.SkyResult[Error,any]) any (typed param), a Msg ctor is func(any) Main_Msg; neither matches the runtime fast paths, and the SPA/Cmd runtime SkyCalls it → reflect.NumIn panic. This is a first-class-function ABI matter (make HOF params + fn values uniformly func(any)any WITHOUT breaking the typed HOF twins that need func(A)B). An Architecture-Consult (agent a1b267019) is designing the systematic lever (erase-fn-param-types vs box-at-widen) + grilling regressions before implementing. Verification of the landed batch (server regression) is running (/tmp/verify.log).

Function-value boxing landed (commit 1430e4c0) + the ABI residual

LANDED + verified server-safe (cargo test workspace, roundtrip 178/178, 09-live-counter build+run HTTP 200, 19-skyforum build):

Todos client under TinyGo: renders + routes + filters + persists to backend.

THE RESIDUAL — first-class-function ABI (needs a coordinated design, NOT a per-site patch):

ABI-design consult verdict (2026-08-22): RECOMMEND-STOP at checkpoint 1430e4c0

The residual is CLOSEABLE, not floor. The coherent fix is Option C (curried boxing at coerce_if_needed's Any path @ lower.rs:2911 + curry-aware chained application c(a)(b) of a boxed func VALUE at lower.rs:4526/4554 + curried make_partial nests @ lower.rs:4653). It is NOT floor-touching (touches lowering only; no §0.3-rule-5 site) and emits NO dispatch token (unlike routing through SkyCall, which would trip the armed coerce-floor dispatch tripwire + brush the §4.3 TEA-boundary floor).

WHY STOP NOW (not "impossible" — deliberately deferred):

  1. The verification leg does not exist: examples/60-spa-todos/run.sh:34 builds the client with STANDARD GOOS=js GOARCH=wasm go build (full reflect), so the residual is INERT on the shipping path and the TinyGo path is unbuilt/ungated. Can't ratchet a property nothing tests. A TinyGo build+run gate must land first.
  2. The residual bites ONLY under TinyGo, which is not the current build path.
  3. Option C edits a central hot path (coerce_if_needed) already reverted once — an N-strikes signal for a separate, scoped, gated effort, not an inline grind.

DISCIPLINED PATH FORWARD (when authorized): (a) land a TinyGo build+run gate for the SPA client; (b) execute Option C with the exact loci above, re-blessing coerce-floor (adapter down, dispatch stays 0, justified narrow movement); (c) until then, codec-decode-under-TinyGo is a documented known limitation. The app runs correctly on the standard-Go-wasm path (this is what run.sh serves).