Suggested model tier: Sonnet 4.6 medium effort (small API surface, but the seam placement matters)
Context
The playback half. The architectural seam already exists: consumers render from a Flow<VoxelFrame> (or equivalent — confirm in recon) that feeds ComposeLattice → LumosCanvas. TracePlayer produces that same stream from a VoxelTrace instead of a live simulation. Everything downstream — projection, rotation, glyph overlay, theming — stays live and unchanged, which is exactly why Option A was chosen: event-driven glyphs still fire over baked traces, because glyph overlay happens after the recording point.
LumosSource is the consumer-facing seal: callers declare where frames come from and never touch player internals.
Objective
A consumer can swap a live simulation for a trace with a one-line source change and see pixel-identical output for the same seed.
Expected outcomes
TracePlayer(trace: VoxelTrace) in :phosphor-trace: frames(timeSource: TimeSource = Monotonic): Flow<VoxelFrame> — maps elapsed wall time → recorded frame index (hold-last-frame semantics between recorded frames; display refresh runs faster than the recorded fps and that's fine), honors loop on the active segment, supports play/pause/seek(frameIndex)/setSegment(name).
- Sealed
LumosSource { data class Live(...existing runtime inputs...); data class Trace(trace: VoxelTrace, initialSegment: String) } with a single factory producing the frame stream either way — recon determines whether this lives in :phosphor-trace or :phosphor-lumos to keep dependency direction clean.
- Reconstruction: dynamic channels + static lattice → full
VoxelFrame instances, allocation-conscious (reuse buffers; no 1,500-object churn per frame).
Technical constraints
commonMain; kotlinx-coroutines Flow allowed here; zero changes to ComposeLattice/LumosCanvas signatures; Result-typed trace-load errors.
Tasks
A — Recon (comment before code): Identify the exact live frame-stream type consumers collect today (#54 shows lumosFrames.collectAsState() — find its producer and type). Confirm where LumosSource should live. STOP for approval.
B — TracePlayer + LumosSource: Implement per expected outcomes.
C — Golden equivalence test: Capture a trace with seed S (T2), run the live sim with seed S at the same fixed dt, assert frame-by-frame VoxelFrame equality. This test is the epic's keystone — it proves replay ≡ live.
D — Playback semantics tests: loop wrap has no index gap; seek lands exactly; pause holds frame; time-mapping holds correct frame between recorded timestamps.
Validation / DoD
Golden equivalence green on JVM; playback semantics tests green on JVM + iOS sim; demo app runs a trace through the existing LumosCanvas with glyph overlay visibly functioning on top.
Out of scope
State-transition logic between segments (T5 — this ticket only plays one segment at a time); crossfading; Socket integration; any change to projection or canvas code.
Human review gate
Miley reviews the recon seam decision before B; visually confirms the demo (trace + live glyph) before close.
Agent handoff prompt
You are implementing PHO-37 in socket-link/phosphor. Depends on PHO-36 (merged).
1. RECON FIRST: find the live frame-stream producer consumers collect
(the thing behind lumosFrames in PHO-27). Comment its type, location, and
your proposed LumosSource placement. STOP for approval.
2. Implement TracePlayer (time→frame mapping, hold-last semantics, loop,
play/pause/seek/setSegment) and sealed LumosSource(Live | Trace).
3. Zero signature changes to ComposeLattice / LumosCanvas. Reuse frame buffers —
no per-frame 1,500-object allocation.
4. Tests: golden equivalence (trace seed S == live seed S, frame by frame);
loop/seek/pause/time-mapping semantics.
5. Wire the demo to play a .vxt through LumosCanvas with a glyph firing on top.
6. DoD: all green JVM + iosSimulatorArm64; demo screenshot in PR.
If recon contradicts any assumption in this ticket, STOP and comment.
Suggested model tier: Sonnet 4.6 medium effort (small API surface, but the seam placement matters)
Context
The playback half. The architectural seam already exists: consumers render from a
Flow<VoxelFrame>(or equivalent — confirm in recon) that feedsComposeLattice → LumosCanvas.TracePlayerproduces that same stream from aVoxelTraceinstead of a live simulation. Everything downstream — projection, rotation, glyph overlay, theming — stays live and unchanged, which is exactly why Option A was chosen: event-driven glyphs still fire over baked traces, because glyph overlay happens after the recording point.LumosSourceis the consumer-facing seal: callers declare where frames come from and never touch player internals.Objective
A consumer can swap a live simulation for a trace with a one-line source change and see pixel-identical output for the same seed.
Expected outcomes
TracePlayer(trace: VoxelTrace)in:phosphor-trace:frames(timeSource: TimeSource = Monotonic): Flow<VoxelFrame>— maps elapsed wall time → recorded frame index (hold-last-frame semantics between recorded frames; display refresh runs faster than the recorded fps and that's fine), honorsloopon the active segment, supportsplay/pause/seek(frameIndex)/setSegment(name).LumosSource { data class Live(...existing runtime inputs...); data class Trace(trace: VoxelTrace, initialSegment: String) }with a single factory producing the frame stream either way — recon determines whether this lives in:phosphor-traceor:phosphor-lumosto keep dependency direction clean.VoxelFrameinstances, allocation-conscious (reuse buffers; no 1,500-object churn per frame).Technical constraints
commonMain; kotlinx-coroutines Flow allowed here; zero changes to
ComposeLattice/LumosCanvassignatures; Result-typed trace-load errors.Tasks
A — Recon (comment before code): Identify the exact live frame-stream type consumers collect today (#54 shows
lumosFrames.collectAsState()— find its producer and type). Confirm whereLumosSourceshould live. STOP for approval.B — TracePlayer + LumosSource: Implement per expected outcomes.
C — Golden equivalence test: Capture a trace with seed S (T2), run the live sim with seed S at the same fixed dt, assert frame-by-frame
VoxelFrameequality. This test is the epic's keystone — it proves replay ≡ live.D — Playback semantics tests: loop wrap has no index gap; seek lands exactly; pause holds frame; time-mapping holds correct frame between recorded timestamps.
Validation / DoD
Golden equivalence green on JVM; playback semantics tests green on JVM + iOS sim; demo app runs a trace through the existing
LumosCanvaswith glyph overlay visibly functioning on top.Out of scope
State-transition logic between segments (T5 — this ticket only plays one segment at a time); crossfading; Socket integration; any change to projection or canvas code.
Human review gate
Miley reviews the recon seam decision before B; visually confirms the demo (trace + live glyph) before close.
Agent handoff prompt