···214214 absorbed into the final plan.
215215 </p>
216216 <p>
217217- During the emit phase, the root bracket — the final plan — is walked linearly.
218218- The amber playhead on the output strip is the linear scan position. For each step,
219219- the plan tells us which byte_log entry to fetch: a dashed line drops from the
220220- byte_log frame down to the root bracket at the same x, showing the plan→byte_log
221221- fetch. The byte_log indicator jumps non-monotonically (random access) while the
222222- output playhead advances linearly (linear write).
217217+ The <em>emit_plan</em> strip below the brackets shows the root's plan as a flat
218218+ sequence of <em>pointer squares</em> — one per byte_log entry. Each square's color
219219+ matches the byte_log frame it points to (with a small <code>→N</code> label when
220220+ wide enough). On readout, we walk the plan linearly: pending squares are dim, the
221221+ current one lights amber, already-read squares stay full-color. The dashed line
222222+ from byte_log down to the current plan square is the random-access fetch the
223223+ current pointer triggered.
223224 </p>
224225 {/if}
225226 </footer>
+81-50
star-lite/viz/src/components/Viz.svelte
···3939 // visible slices (count-based to avoid per-snapshot copies)
4040 let frozen = $derived(frozenList.slice(0, snapshot.frozenCount));
4141 let byteLogVisible = $derived(byteLog.slice(0, snapshot.byteLogCount));
4242- let emitVisible = $derived(emitOutput.slice(0, snapshot.emitCount));
4242+4343+ // the emit_plan exists once the root has been framed — i.e. once the byteLog is fully
4444+ // populated. From then on, all N pointer squares are shown; "read state" tracks how
4545+ // many have been walked.
4646+ let planAvailable = $derived(
4747+ mode === 'car' && byteLog.length > 0 && snapshot.byteLogCount === byteLog.length,
4848+ );
43494450 let observedMaxLayer = $derived(
4551 Math.max(0, snapshot.stack.length - 1, ...frozen.map((f) => f.layer)),
···127133 const layerTints = ['#1a1d24', '#1f3550', '#3a3214', '#4a1e3e', '#3e2c5a', '#1f4a3a'];
128134 function layerColor(L) { return layerTints[Math.min(L, layerTints.length - 1)]; }
129135130130- // colors for byte_log frames
136136+ // colors for byte_log frames. records are blue; L0 nodes are teal-green so they're
137137+ // visually distinct from records (the common case). L4 inherits the blue slot since
138138+ // it's rare (~1 in 256 keys).
131139 const recordFrameFill = '#1d3a4d';
132140 const recordFrameStroke = '#3a6a8a';
133133- const nodeFrameFills = ['#264a6e', '#5a4218', '#5a2440', '#4a2e6a', '#1f4a3a'];
134134- const nodeFrameStrokes = ['#6cd0ff', '#ffd86c', '#ff8aa8', '#a98aff', '#7adcb8'];
141141+ const nodeFrameFills = ['#1f4a3a', '#5a4218', '#5a2440', '#4a2e6a', '#264a6e'];
142142+ const nodeFrameStrokes = ['#7adcb8', '#ffd86c', '#ff8aa8', '#a98aff', '#6cd0ff'];
135143 function frameFill(f) {
136144 if (f.kind === 'record') return recordFrameFill;
137145 if (f.isRoot) return '#3a3514';
···295303 </text>
296304 {/if}
297305 {/each}
298298- <!-- emit-plan ↔ byte_log fetch indicator: vertical line at the byte_log position
299299- the plan currently points to, drawn from the byte_log frame down through the
300300- bracket area to the root bracket (= the active plan). -->
306306+ <!-- byte_log → emit_plan fetch: the plan square at idx i says "read byte_log[plan[i]]";
307307+ a vertical mark on byte_log at the read position, plus a slanted line down to the
308308+ current plan square shows the random-access read driven by the linear plan walk. -->
301309 {#if snapshot.emitCount > 0}
302302- {@const lastFramePos = emitOutput[snapshot.emitCount - 1]}
310310+ {@const lastOutputIdx = snapshot.emitCount - 1}
311311+ {@const lastFramePos = emitOutput[lastOutputIdx]}
303312 <line
304313 x1={frameXInLog(lastFramePos) + FRAME_W / 2}
305314 x2={frameXInLog(lastFramePos) + FRAME_W / 2}
306315 y1={byteLogY - 4}
307307- y2={bracketsBottomY + 2}
316316+ y2={byteLogY + STRIP_H + 4}
308317 class="bytelog-read"
309318 />
319319+ <line
320320+ x1={frameXInLog(lastFramePos) + FRAME_W / 2}
321321+ y1={byteLogY + STRIP_H + 4}
322322+ x2={frameXInLog(lastOutputIdx) + FRAME_W / 2}
323323+ y2={outputY - 4}
324324+ class="emit-fetch"
325325+ />
310326 {/if}
311327312328 <!-- emit_plan brackets: each frozen subtree as a contiguous range in byte_log order.
···346362 {/if}
347363 {/each}
348364349349- <!-- output strip: frames in stream-order (preorder) -->
350350- <text x={4} y={outputY - 3} class="strip-label">
351351- output (stream order, {emitVisible.length}/{emitOutput.length})
352352- </text>
353353- <rect
354354- x={PAD_X - 2}
355355- y={outputY}
356356- width={svgWidth - 2 * PAD_X + 4}
357357- height={STRIP_H}
358358- class="strip-bg"
359359- />
360360- {#each emitVisible as framePos, idx (idx)}
361361- {@const f = byteLog[framePos]}
365365+ <!-- emit_plan strip: each square is a pointer into byte_log. shown all-at-once
366366+ the moment the root is framed; read-state styling shows the linear walk. -->
367367+ {#if planAvailable}
368368+ <text x={4} y={outputY - 3} class="strip-label">
369369+ emit_plan ({snapshot.emitCount} read / {emitOutput.length} pointers)
370370+ </text>
362371 <rect
363363- x={frameXInLog(idx)}
372372+ x={PAD_X - 2}
364373 y={outputY}
365365- width={Math.max(1, FRAME_W - 0.5)}
374374+ width={svgWidth - 2 * PAD_X + 4}
366375 height={STRIP_H}
367367- fill={frameFill(f)}
368368- stroke={frameStroke(f)}
369369- stroke-width={FRAME_W >= 4 ? 0.8 : 0.3}
370370- class="frame"
371371- class:pulse-frame={pulse?.kind === 'emit' && pulse?.outputIdx === idx}
372372- class:isRoot={f.isRoot}
376376+ class="strip-bg"
373377 />
374374- {#if FRAME_W >= 24}
375375- <text x={frameXInLog(idx) + FRAME_W / 2} y={outputY + STRIP_H / 2 + 4} text-anchor="middle" class="frame-label">
376376- {f.kind === 'record' ? 'r' : f.isRoot ? '★' : `n${f.layer}`}
377377- </text>
378378- {/if}
379379- {/each}
380380-381381- <!-- output strip linear playhead: where the plan walker currently is -->
382382- {#if snapshot.emitCount > 0}
383383- {@const lastOutputIdx = snapshot.emitCount - 1}
384384- <line
385385- x1={frameXInLog(lastOutputIdx) + FRAME_W / 2}
386386- x2={frameXInLog(lastOutputIdx) + FRAME_W / 2}
387387- y1={outputY - 6}
388388- y2={outputY + STRIP_H + 6}
389389- class="output-playhead"
390390- />
378378+ {#each emitOutput as framePos, idx (idx)}
379379+ {@const f = byteLog[framePos]}
380380+ {@const isRead = idx < snapshot.emitCount}
381381+ {@const isCurrent = idx === snapshot.emitCount - 1}
382382+ <rect
383383+ x={frameXInLog(idx)}
384384+ y={outputY}
385385+ width={Math.max(1, FRAME_W - 0.5)}
386386+ height={STRIP_H}
387387+ fill={frameFill(f)}
388388+ stroke={frameStroke(f)}
389389+ stroke-width={FRAME_W >= 4 ? 0.8 : 0.3}
390390+ class="plan-square"
391391+ class:read={isRead && !isCurrent}
392392+ class:current={isCurrent}
393393+ class:isRoot={f.isRoot}
394394+ />
395395+ {#if FRAME_W >= 24}
396396+ <text
397397+ x={frameXInLog(idx) + FRAME_W / 2}
398398+ y={outputY + STRIP_H / 2 + 4}
399399+ text-anchor="middle"
400400+ class="frame-label"
401401+ class:dim={!isRead && !isCurrent}
402402+ >
403403+ →{framePos}
404404+ </text>
405405+ {/if}
406406+ {/each}
391407 {/if}
392408 {/if}
393409 </svg>
···459475 .emit-bracket.active-plan { opacity: 1; stroke: #ffd86c !important; stroke-width: 1.5 !important; }
460476 .emit-bracket.pulse-frozen { opacity: 1; stroke: #ffd86c !important; stroke-width: 1.5 !important; }
461477462462- .output-playhead { stroke: #ffd86c; stroke-width: 1.5; opacity: 0.9; }
478478+ .plan-square { opacity: 0.32; transition: opacity 100ms; }
479479+ .plan-square.read { opacity: 0.85; }
480480+ .plan-square.current {
481481+ opacity: 1;
482482+ stroke: #ffd86c !important;
483483+ stroke-width: 2 !important;
484484+ }
485485+ .frame-label.dim { opacity: 0.45; }
486486+463487 .bytelog-read {
464488 stroke: #ffd86c;
465465- stroke-width: 1.2;
466466- opacity: 0.75;
489489+ stroke-width: 1.4;
490490+ opacity: 0.85;
467491 stroke-dasharray: 2 2;
492492+ pointer-events: none;
493493+ }
494494+ .emit-fetch {
495495+ stroke: #ffd86c;
496496+ stroke-width: 1.2;
497497+ opacity: 0.6;
498498+ stroke-dasharray: 3 2;
468499 pointer-events: none;
469500 }
470501 .bracket-label {