···11-// verification algorithm with state snapshots for visualization.
22-// faithful to the pseudocode in ../../../readme.md, with an extra
33-// per-event snapshot push so the viz can step through it.
11+// MST-walking algorithm with state snapshots for visualization.
22+// faithful to the pseudocode in ../../../readme.md (verification + CAR conversion).
33+//
44+// computeSnapshots returns:
55+// {
66+// snapshots: [{ cursor, prevLayer, stack, frozenCount, byteLogCount, emitCount, event }],
77+// frozen: [...], // shared: append-only list of frozen MST node records
88+// byteLog: [...], // shared: only populated in 'car' mode
99+// emitOutput: [...], // shared: only populated in 'car' mode (after root frozen)
1010+// mode: 'verify' | 'car',
1111+// }
1212+// snapshots index into the shared lists via *Count fields, so snapshotting is O(stack)
1313+// per step rather than O(state) per step.
414515let cidCounter = 0;
616function fakeCid(prefix) {
···10201121class MstNode {
1222 constructor() {
1313- this.entries = []; // [{ key, recordCid, keyIdx, rightSubtree }]
1414- this.leftSubtree = null;
1515- this.firstKeyIdx = null; // span: leftmost key index covered (incl. subtrees)
1616- this.lastKeyIdx = null; // span: rightmost key index covered
2323+ this.entries = []; // [{ key, recordCid, keyIdx, framePosition, rightSubtree, rightEmitPlan }]
2424+ this.leftSubtree = null; // CID
2525+ this.leftEmitPlan = null; // CAR mode: emit plan of the left subtree
2626+ this.firstKeyIdx = null;
2727+ this.lastKeyIdx = null;
1728 }
1829 isEmpty() {
1930 return this.leftSubtree === null && this.entries.length === 0;
···2132 reset() {
2233 this.entries = [];
2334 this.leftSubtree = null;
3535+ this.leftEmitPlan = null;
2436 this.firstKeyIdx = null;
2537 this.lastKeyIdx = null;
2638 }
2727- linkRecord(key, recordCid, keyIdx) {
2828- this.entries.push({ key, recordCid, keyIdx, rightSubtree: null });
3939+ linkRecord(key, recordCid, keyIdx, framePosition) {
4040+ this.entries.push({
4141+ key, recordCid, keyIdx, framePosition,
4242+ rightSubtree: null, rightEmitPlan: null,
4343+ });
2944 if (this.firstKeyIdx === null) this.firstKeyIdx = keyIdx;
3045 this.lastKeyIdx = keyIdx;
3146 }
3232- linkSubtree(cid, spanFirst, spanLast) {
4747+ linkSubtree(cid, spanFirst, spanLast, emitPlan) {
3348 if (this.entries.length === 0) {
3449 this.leftSubtree = cid;
5050+ this.leftEmitPlan = emitPlan;
3551 } else {
3636- this.entries[this.entries.length - 1].rightSubtree = cid;
5252+ const last = this.entries[this.entries.length - 1];
5353+ last.rightSubtree = cid;
5454+ last.rightEmitPlan = emitPlan;
3755 }
3856 if (this.firstKeyIdx === null || spanFirst < this.firstKeyIdx) this.firstKeyIdx = spanFirst;
3957 if (this.lastKeyIdx === null || spanLast > this.lastKeyIdx) this.lastKeyIdx = spanLast;
···4967 }
5068}
51697070+function buildSubtreeEmitPlan(node, framePosition) {
7171+ const plan = [framePosition];
7272+ if (node.leftEmitPlan) plan.push(...node.leftEmitPlan);
7373+ for (const entry of node.entries) {
7474+ plan.push(entry.framePosition);
7575+ if (entry.rightEmitPlan) plan.push(...entry.rightEmitPlan);
7676+ }
7777+ return plan;
7878+}
7979+5280function snap(state, event) {
5381 return {
5482 cursor: state.cursor,
5583 prevLayer: state.prevLayer,
5684 stack: state.stack.map(n => n.snapshot()),
5757- // frozen records are never mutated after push, so we can share them across snapshots
5858- frozen: state.frozen.slice(),
8585+ frozenCount: state.frozen.length,
8686+ byteLogCount: state.byteLog ? state.byteLog.length : 0,
8787+ emitCount: state.emitOutput ? state.emitOutput.length : 0,
5988 event,
6089 };
6190}
62916363-function freezeNode(state, layer, finalCascade) {
9292+function freezeNode(state, layer, finalCascade, mode) {
6493 const node = state.stack[layer];
6594 const parent = state.stack[layer + 1];
6695 if (node.isEmpty()) {
···7099 })];
71100 }
72101 const cid = fakeCid(`n${layer}`);
102102+ let framePosition = null;
103103+ let emitPlan = null;
104104+ if (mode === 'car') {
105105+ framePosition = state.byteLog.length;
106106+ state.byteLog.push({
107107+ position: framePosition,
108108+ kind: 'node',
109109+ layer,
110110+ keyIdx: node.lastKeyIdx,
111111+ cid,
112112+ });
113113+ emitPlan = buildSubtreeEmitPlan(node, framePosition);
114114+ }
73115 const frozenRecord = {
74116 layer,
75117 firstKeyIdx: node.firstKeyIdx,
···77119 entries: node.entries.map(e => ({ ...e })),
78120 leftSubtree: node.leftSubtree,
79121 cid,
122122+ framePosition,
123123+ emitPlan,
80124 };
81125 state.frozen.push(frozenRecord);
8282- parent.linkSubtree(cid, node.firstKeyIdx, node.lastKeyIdx);
126126+ parent.linkSubtree(cid, node.firstKeyIdx, node.lastKeyIdx, emitPlan);
83127 node.reset();
84128 return [snap(state, {
85129 type: finalCascade ? 'finalFreezeNode' : 'freezeNode',
86130 layer,
87131 cid,
132132+ framePosition,
88133 frozenRecord,
89134 })];
90135}
911369292-export function computeSnapshots(keyLayerPairs) {
137137+export function computeSnapshots(keyLayerPairs, mode = 'verify') {
93138 resetCidCounter();
9494- const state = { cursor: -1, prevLayer: -1, stack: [], frozen: [] };
139139+ const state = {
140140+ cursor: -1,
141141+ prevLayer: -1,
142142+ stack: [],
143143+ frozen: [],
144144+ byteLog: mode === 'car' ? [] : null,
145145+ emitOutput: mode === 'car' ? [] : null,
146146+ };
95147 const out = [];
96148 out.push(snap(state, { type: 'init' }));
97149···107159108160 if (keyLayer > state.prevLayer) {
109161 for (let l = 0; l < keyLayer; l++) {
110110- out.push(...freezeNode(state, l, false));
162162+ out.push(...freezeNode(state, l, false, mode));
111163 }
112164 }
113165114166 const recordCid = fakeCid('r');
115115- state.stack[keyLayer].linkRecord(key, recordCid, i);
116116- out.push(snap(state, { type: 'linkRecord', key, keyLayer, keyIdx: i, recordCid }));
167167+ let framePosition = null;
168168+ if (mode === 'car') {
169169+ framePosition = state.byteLog.length;
170170+ state.byteLog.push({
171171+ position: framePosition,
172172+ kind: 'record',
173173+ layer: keyLayer,
174174+ keyIdx: i,
175175+ cid: recordCid,
176176+ });
177177+ }
178178+ state.stack[keyLayer].linkRecord(key, recordCid, i, framePosition);
179179+ out.push(snap(state, { type: 'linkRecord', key, keyLayer, keyIdx: i, recordCid, framePosition }));
117180 state.prevLayer = keyLayer;
118181 }
119182···121184 out.push(snap(state, { type: 'streamEnd' }));
122185123186 for (let l = 0; l < state.stack.length - 1; l++) {
124124- out.push(...freezeNode(state, l, true));
187187+ out.push(...freezeNode(state, l, true, mode));
125188 }
126189127190 let rootCid;
191191+ let rootEmitPlan = null;
128192 if (state.stack.length > 0) {
129193 const topL = state.stack.length - 1;
130194 const root = state.stack[topL];
131195 if (!root.isEmpty()) {
132196 rootCid = fakeCid('root');
197197+ let rootFramePosition = null;
198198+ if (mode === 'car') {
199199+ rootFramePosition = state.byteLog.length;
200200+ state.byteLog.push({
201201+ position: rootFramePosition,
202202+ kind: 'node',
203203+ layer: topL,
204204+ keyIdx: root.lastKeyIdx,
205205+ cid: rootCid,
206206+ isRoot: true,
207207+ });
208208+ rootEmitPlan = buildSubtreeEmitPlan(root, rootFramePosition);
209209+ }
133210 state.frozen.push({
134211 layer: topL,
135212 firstKeyIdx: root.firstKeyIdx,
···137214 entries: root.entries.map(e => ({ ...e })),
138215 leftSubtree: root.leftSubtree,
139216 cid: rootCid,
217217+ framePosition: rootFramePosition,
218218+ emitPlan: rootEmitPlan,
140219 isRoot: true,
141220 });
142221 root.reset();
···147226 rootCid = 'bafyreih…(empty mst)';
148227 }
149228 out.push(snap(state, { type: 'rootCid', rootCid }));
150150- return out;
229229+230230+ if (mode === 'car' && rootEmitPlan) {
231231+ for (const framePos of rootEmitPlan) {
232232+ state.emitOutput.push(framePos);
233233+ out.push(snap(state, {
234234+ type: 'emitFrame',
235235+ framePos,
236236+ outputIdx: state.emitOutput.length - 1,
237237+ }));
238238+ }
239239+ out.push(snap(state, { type: 'carDone' }));
240240+ }
241241+242242+ return {
243243+ snapshots: out,
244244+ frozen: state.frozen,
245245+ byteLog: state.byteLog || [],
246246+ emitOutput: state.emitOutput || [],
247247+ mode,
248248+ };
151249}
152250153251export function describeEvent(ev) {
···156254 case 'read': return `read key "${ev.key}" (layer ${ev.keyLayer})`;
157255 case 'growStack': return `grow stack to include layer ${ev.layer}`;
158256 case 'freezeSkipEmpty': return `cascade: layer ${ev.layer} empty — skip`;
159159- case 'freezeNode': return `cascade: freeze layer ${ev.layer} → CID ${ev.cid}, link into layer ${ev.layer + 1}`;
160160- case 'linkRecord': return `link record "${ev.key}" into layer ${ev.keyLayer} (record CID ${ev.recordCid})`;
257257+ case 'freezeNode':
258258+ return ev.framePosition !== null && ev.framePosition !== undefined
259259+ ? `cascade: freeze layer ${ev.layer} → CID ${ev.cid}, frame at byte_log[${ev.framePosition}], link into layer ${ev.layer + 1}`
260260+ : `cascade: freeze layer ${ev.layer} → CID ${ev.cid}, link into layer ${ev.layer + 1}`;
261261+ case 'linkRecord':
262262+ return ev.framePosition !== null && ev.framePosition !== undefined
263263+ ? `link record "${ev.key}" into layer ${ev.keyLayer} (record CID ${ev.recordCid}, byte_log[${ev.framePosition}])`
264264+ : `link record "${ev.key}" into layer ${ev.keyLayer} (record CID ${ev.recordCid})`;
161265 case 'streamEnd': return 'stream end — final rollup';
162266 case 'finalFreezeSkipEmpty': return `final rollup: layer ${ev.layer} empty — skip`;
163163- case 'finalFreezeNode': return `final rollup: freeze layer ${ev.layer} → CID ${ev.cid}, link into layer ${ev.layer + 1}`;
267267+ case 'finalFreezeNode':
268268+ return ev.framePosition !== null && ev.framePosition !== undefined
269269+ ? `final rollup: freeze layer ${ev.layer} → CID ${ev.cid}, frame at byte_log[${ev.framePosition}], link into layer ${ev.layer + 1}`
270270+ : `final rollup: freeze layer ${ev.layer} → CID ${ev.cid}, link into layer ${ev.layer + 1}`;
164271 case 'rootCid': return `root CID: ${ev.rootCid}`;
272272+ case 'emitFrame': return `emit byte_log[${ev.framePos}] → output[${ev.outputIdx}]`;
273273+ case 'carDone': return 'CAR conversion complete';
165274 default: return ev.type;
166275 }
167276}
+3
star-lite/viz/vite.config.js
···11import { defineConfig } from 'vite';
22import { svelte } from '@sveltejs/vite-plugin-svelte';
3344+// `base: './'` makes built asset URLs relative, so the bundle can be hosted
55+// at any sub-path without rebuilding.
46export default defineConfig({
77+ base: './',
58 plugins: [svelte()],
69});