# LT Code (Luby Transform) Fountain Streaming Engine Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Replace the fixed sequential chunking mechanism in `qrcode_transceiver` with a rateless Fountain Code (LT Code with Belief Propagation) streaming engine and raw 8-bit binary QR code mode.

**Architecture:** Create a standalone, unit-tested JavaScript LT Engine module (`lt_engine.js`) implementing Mulberry32 PRNG, Robust Soliton Distribution sampling, binary packet packing/unpacking, and a Belief Propagation graph solver. Then integrate `lt_engine.js` into `transmitter.html` for continuous rateless frame generation and `receiver.html` for real-time decoding and UI progress tracking.

**Tech Stack:** Vanilla JavaScript (ES6+), HTML5 Canvas, WebRTC (`navigator.mediaDevices.getUserMedia`), `easy.qrcode.min.js`, `qr-scanner`, Node.js (for headless unit testing).

## Global Constraints
- Standard JavaScript (ES6) running natively in browser environments.
- Use `easy.qrcode.min.js` with `binary: true` for raw 8-bit byte QR code generation.
- No external runtime server dependencies beyond the existing Python HTTPS server.

---

### Task 1: Create LT Code Engine (`lt_engine.js`) and Unit Test Suite

**Files:**
- Create: `lt_engine.js`
- Create: `test_lt_engine.js`

**Interfaces:**
- Produces: `LTEngine.Mulberry32(seed)`, `LTEngine.RobustSoliton(K, c, delta)`, `LTEngine.packPacket(seed, K, L, flags, filename, payloadBytes)`, `LTEngine.unpackPacket(uint8Array)`, `LTEngine.BPDecoder(K, L)`

- [ ] **Step 1: Write failing unit test for LT Engine components**

Create `test_lt_engine.js`:
```javascript
const fs = require('fs');
const assert = require('assert');
const LTEngine = require('./lt_engine.js');

// Test 1: Mulberry32 PRNG determinism
const prng1 = LTEngine.Mulberry32(1042);
const val1 = prng1();
const prng2 = LTEngine.Mulberry32(1042);
const val2 = prng2();
assert.strictEqual(val1, val2, "Mulberry32 must be deterministic for identical seeds");

// Test 2: Binary Packet Pack & Unpack Roundtrip
const seed = 12345;
const K = 10;
const L = 16;
const flags = 1; // File flag
const filename = "test.bin";
const payload = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);

const packed = LTEngine.packPacket(seed, K, L, flags, filename, payload);
const unpacked = LTEngine.unpackPacket(packed);

assert.strictEqual(unpacked.seed, seed);
assert.strictEqual(unpacked.K, K);
assert.strictEqual(unpacked.L, L);
assert.strictEqual(unpacked.flags, flags);
assert.strictEqual(unpacked.filename, filename);
assert.deepStrictEqual(Array.from(unpacked.payload), Array.from(payload));

// Test 3: LT Encoder & BP Decoder full reconstruction
const testData = new Uint8Array(100);
for (let i = 0; i < 100; i++) testData[i] = i & 0xFF;

const blockSize = 10;
const sourceBlocks = LTEngine.sliceSourceBlocks(testData, blockSize);
const totalK = sourceBlocks.length;

const decoder = new LTEngine.BPDecoder(totalK, blockSize);
let frameSeed = 1;
while (!decoder.isComplete() && frameSeed < 200) {
    const packetBytes = LTEngine.encodeFrame(sourceBlocks, frameSeed, 0, null);
    const drop = LTEngine.unpackPacket(packetBytes);
    decoder.processDrop(drop);
    frameSeed++;
}

assert.strictEqual(decoder.isComplete(), true, "BP Decoder should reconstruct data");
const reconstructed = decoder.getReconstructedData();
assert.deepStrictEqual(Array.from(reconstructed.subarray(0, 100)), Array.from(testData));

console.log("ALL LT ENGINE UNIT TESTS PASSED!");
```

- [ ] **Step 2: Run unit test script to verify it fails**

Run: `node test_lt_engine.js`
Expected: FAIL with "Cannot find module './lt_engine.js'"

- [ ] **Step 3: Write implementation of `lt_engine.js`**

Create `lt_engine.js`:
```javascript
(function (root, factory) {
    if (typeof exports === 'object' && typeof module === 'object') {
        module.exports = factory();
    } else {
        root.LTEngine = factory();
    }
}(typeof self !== 'undefined' ? self : this, function () {

    function Mulberry32(seed) {
        return function() {
            let t = seed += 0x6D2B79F5;
            t = Math.imul(t ^ (t >>> 15), t | 1);
            t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
            return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
        };
    }

    function RobustSoliton(K, c = 0.1, delta = 0.5) {
        if (K === 1) return [0, 1.0];
        const S = [];
        let rhoSum = 0;
        const rho = new Float64Array(K + 1);
        rho[1] = 1 / K;
        rhoSum += rho[1];
        for (let d = 2; d <= K; d++) {
            rho[d] = 1 / (d * (d - 1));
            rhoSum += rho[d];
        }

        const R = c * Math.log(K / delta) * Math.sqrt(K);
        const tau = new Float64Array(K + 1);
        let tauSum = 0;
        const pivot = Math.floor(K / R);

        for (let d = 1; d <= K; d++) {
            if (d < pivot) {
                tau[d] = R / (d * K);
            } else if (d === pivot) {
                tau[d] = (R * Math.log(R / delta)) / K;
            } else {
                tau[d] = 0;
            }
            tauSum += tau[d];
        }

        const Z = rhoSum + tauSum;
        const cdf = new Float64Array(K + 1);
        let cumulative = 0;
        for (let d = 1; d <= K; d++) {
            cumulative += (rho[d] + tau[d]) / Z;
            cdf[d] = cumulative;
        }
        cdf[K] = 1.0;
        return cdf;
    }

    function sampleDegree(cdf, prng) {
        const u = prng();
        for (let d = 1; d < cdf.length; d++) {
            if (u <= cdf[d]) return d;
        }
        return 1;
    }

    function sampleBlocks(K, degree, prng) {
        const selected = new Set();
        while (selected.size < degree && selected.size < K) {
            const idx = Math.floor(prng() * K);
            selected.add(idx);
        }
        return Array.from(selected);
    }

    function sliceSourceBlocks(uint8Array, blockSize) {
        const K = Math.ceil(uint8Array.length / blockSize);
        const blocks = [];
        for (let i = 0; i < K; i++) {
            const block = new Uint8Array(blockSize);
            const slice = uint8Array.subarray(i * blockSize, Math.min((i + 1) * blockSize, uint8Array.length));
            block.set(slice);
            blocks.push(block);
        }
        return blocks;
    }

    function packPacket(seed, K, L, flags, filename, payload) {
        const encoder = new TextEncoder();
        const nameBytes = filename ? encoder.encode(filename) : new Uint8Array(0);
        const headerLen = 9 + (nameBytes.length > 0 ? 1 + nameBytes.length : 0);
        const totalLen = headerLen + L;

        const buffer = new Uint8Array(totalLen);
        const view = new DataView(buffer.buffer);

        view.setUint32(0, seed, false); // Big endian
        view.setUint16(4, K, false);
        view.setUint16(6, L, false);

        let flagByte = flags & 0x01;
        if (nameBytes.length > 0) flagByte |= 0x02;
        buffer[8] = flagByte;

        let offset = 9;
        if (nameBytes.length > 0) {
            buffer[offset] = nameBytes.length & 0xFF;
            offset++;
            buffer.set(nameBytes, offset);
            offset += nameBytes.length;
        }

        buffer.set(payload, offset);
        return buffer;
    }

    function unpackPacket(uint8Array) {
        const view = new DataView(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteLength);
        const seed = view.getUint32(0, false);
        const K = view.getUint16(4, false);
        const L = view.getUint16(6, false);
        const flags = uint8Array[8];

        const isFile = (flags & 0x01) !== 0;
        const hasName = (flags & 0x02) !== 0;

        let offset = 9;
        let filename = null;
        if (hasName) {
            const nameLen = uint8Array[offset];
            offset++;
            const nameBytes = uint8Array.subarray(offset, offset + nameLen);
            filename = new TextDecoder().decode(nameBytes);
            offset += nameLen;
        }

        const payload = uint8Array.subarray(offset, offset + L);
        return { seed, K, L, flags, isFile, filename, payload };
    }

    function encodeFrame(sourceBlocks, seed, flags, filename) {
        const K = sourceBlocks.length;
        const L = sourceBlocks[0].length;
        const cdf = RobustSoliton(K);
        const prng = Mulberry32(seed);

        const degree = sampleDegree(cdf, prng);
        const blockIndices = sampleBlocks(K, degree, prng);

        const xorPayload = new Uint8Array(L);
        for (const idx of blockIndices) {
            const b = sourceBlocks[idx];
            for (let i = 0; i < L; i++) {
                xorPayload[i] ^= b[i];
            }
        }

        return packPacket(seed, K, L, flags, filename, xorPayload);
    }

    function uint8ArrayToBinaryString(uint8Array) {
        let str = '';
        const chunkSize = 8192;
        for (let i = 0; i < uint8Array.length; i += chunkSize) {
            const sub = uint8Array.subarray(i, i + chunkSize);
            str += String.fromCharCode.apply(null, sub);
        }
        return str;
    }

    function binaryStringToUint8Array(str) {
        const bytes = new Uint8Array(str.length);
        for (let i = 0; i < str.length; i++) {
            bytes[i] = str.charCodeAt(i) & 0xFF;
        }
        return bytes;
    }

    class BPDecoder {
        constructor(K, L) {
            this.K = K;
            this.L = L;
            this.cdf = RobustSoliton(K);
            this.blocks = new Array(K).fill(null);
            this.solvedCount = 0;
            this.graph = [];
            this.filename = null;
            this.isFile = false;
        }

        processDrop(drop) {
            if (this.solvedCount === this.K) return true;

            if (drop.filename && !this.filename) {
                this.filename = drop.filename;
                this.isFile = drop.isFile;
            }

            const prng = Mulberry32(drop.seed);
            const degree = sampleDegree(this.cdf, prng);
            const blockIndices = sampleBlocks(this.K, degree, prng);

            let payload = new Uint8Array(drop.payload);
            const neighborSet = new Set();

            for (const bIdx of blockIndices) {
                if (this.blocks[bIdx] !== null) {
                    const known = this.blocks[bIdx];
                    for (let i = 0; i < this.L; i++) {
                        payload[i] ^= known[i];
                    }
                } else {
                    neighborSet.add(bIdx);
                }
            }

            if (neighborSet.size === 0) {
                return this.solvedCount === this.K;
            }

            if (neighborSet.size === 1) {
                const newSolveIdx = Array.from(neighborSet)[0];
                this.resolveBlock(newSolveIdx, payload);
            } else {
                this.graph.push({ neighbors: neighborSet, payload });
            }

            return this.solvedCount === this.K;
        }

        resolveBlock(bIdx, blockPayload) {
            if (this.blocks[bIdx] !== null) return;
            this.blocks[bIdx] = blockPayload;
            this.solvedCount++;

            const queue = [bIdx];
            while (queue.length > 0) {
                const targetIdx = queue.shift();
                const targetPayload = this.blocks[targetIdx];

                for (let i = this.graph.length - 1; i >= 0; i--) {
                    const node = this.graph[i];
                    if (node.neighbors.has(targetIdx)) {
                        for (let k = 0; k < this.L; k++) {
                            node.payload[k] ^= targetPayload[k];
                        }
                        node.neighbors.delete(targetIdx);

                        if (node.neighbors.size === 1) {
                            const nextSolveIdx = Array.from(node.neighbors)[0];
                            this.graph.splice(i, 1);
                            if (this.blocks[nextSolveIdx] === null) {
                                this.blocks[nextSolveIdx] = node.payload;
                                this.solvedCount++;
                                queue.push(nextSolveIdx);
                            }
                        } else if (node.neighbors.size === 0) {
                            this.graph.splice(i, 1);
                        }
                    }
                }
            }
        }

        isComplete() {
            return this.solvedCount === this.K;
        }

        getReconstructedData() {
            if (!this.isComplete()) return null;
            let totalLen = 0;
            for (let i = 0; i < this.K; i++) {
                totalLen += this.blocks[i].length;
            }
            const out = new Uint8Array(totalLen);
            let offset = 0;
            for (let i = 0; i < this.K; i++) {
                out.set(this.blocks[i], offset);
                offset += this.blocks[i].length;
            }
            return out;
        }
    }

    return {
        Mulberry32,
        RobustSoliton,
        sampleDegree,
        sampleBlocks,
        sliceSourceBlocks,
        packPacket,
        unpackPacket,
        encodeFrame,
        uint8ArrayToBinaryString,
        binaryStringToUint8Array,
        BPDecoder
    };
}));
```

- [ ] **Step 4: Run unit test script to verify it passes**

Run: `node test_lt_engine.js`
Expected: PASS with "ALL LT ENGINE UNIT TESTS PASSED!"

- [ ] **Step 5: Commit**

```bash
git add lt_engine.js test_lt_engine.js
git commit -m "feat: add LT Code Fountain Engine with Mulberry32 PRNG and BP Decoder"
```

---

### Task 2: Integrate LT Engine into Transmitter (`transmitter.html`)

**Files:**
- Modify: `transmitter.html`

**Interfaces:**
- Consumes: `LTEngine.sliceSourceBlocks`, `LTEngine.encodeFrame`, `LTEngine.uint8ArrayToBinaryString`

- [ ] **Step 1: Include `lt_engine.js` script tag in `transmitter.html`**

In `<head>` of `transmitter.html`, import `<script src="./lt_engine.js"></script>` alongside `easy.qrcode.min.js`.

- [ ] **Step 2: Update transmitter logic for continuous Fountain streaming**

Modify `transmitter.html` script:
1. Replace sequential array payload generation with `LTEngine.sliceSourceBlocks`.
2. Determine `blockSize` based on selected QR version (Version 10 = ~250 bytes, Version 20 = ~600 bytes, etc.).
3. Maintain continuous state `currentSeed = 1`.
4. In `playMovie()`, on each interval tick, call `LTEngine.encodeFrame(sourceBlocks, currentSeed, flags, filename)`, convert to binary string via `LTEngine.uint8ArrayToBinaryString()`, and update QR Code with `binary: true` options. Increment `currentSeed++`.
5. Update UI stats: `Frames Generated: currentSeed | K: sourceBlocks.length`.

- [ ] **Step 3: Test transmitter UI in browser**

Serve via `python3 server.py 8000` and load `https://localhost:8000/transmitter.html` to verify rateless Fountain QR frame animation.

- [ ] **Step 4: Commit**

```bash
git add transmitter.html
git commit -m "feat: integrate LT Code Fountain rateless streaming into transmitter"
```

---

### Task 3: Integrate LT Engine and Progress Tracker into Receiver (`receiver.html`)

**Files:**
- Modify: `receiver.html`

**Interfaces:**
- Consumes: `LTEngine.binaryStringToUint8Array`, `LTEngine.unpackPacket`, `LTEngine.BPDecoder`

- [ ] **Step 1: Include `lt_engine.js` script tag in `receiver.html`**

Import `<script src="./lt_engine.js"></script>` in `<head>` or module scope of `receiver.html`.

- [ ] **Step 2: Update receiver logic for real-time Soliton Belief Propagation decoding**

Modify `receiver.html` script:
1. Initialize `bpDecoder = null`.
2. When scanning a QR frame in `processScanResults()`:
   - Extract raw bytes from scan result: `LTEngine.binaryStringToUint8Array(result.data)` (or `result.bytes`).
   - Unpack packet: `drop = LTEngine.unpackPacket(bytes)`.
   - If `bpDecoder === null`: instantiate `bpDecoder = new LTEngine.BPDecoder(drop.K, drop.L)`. Initialize UI progress grid with `drop.K` cells.
   - Process drop: `bpDecoder.processDrop(drop)`.
   - Update UI: Highlight solved grid cells, update progress bar (`bpDecoder.solvedCount / drop.K * 100%`), display total scanned frames count.
3. Upon `bpDecoder.isComplete()`:
   - Stop camera scanning.
   - Reconstruct raw byte data: `data = bpDecoder.getReconstructedData()`.
   - If `drop.isFile`: Convert bytes to Blob and trigger automatic browser file download.
   - If plain text: Decode UTF-8 string and display in text result box.

- [ ] **Step 3: Test end-to-end air-gapped transmission**

Transmit text and binary files between transmitter and receiver to verify real-time Belief Propagation decoding and file reassembly.

- [ ] **Step 4: Commit**

```bash
git add receiver.html
git commit -m "feat: integrate real-time LT Code BP decoder and progress tracking into receiver"
```

---

### Task 4: Update Documentation and ADRs

**Files:**
- Modify: `README.md`
- Modify: `README.ko.md`
- Modify: `docs/adr/0001-animated-qr-code-frame-streaming.md`
- Modify: `docs/adr/0002-chunking-and-json-metadata-packet-schema.md`

- [ ] **Step 1: Update README and README.ko.md**

Update architecture diagrams, packet schema details, and system feature bullet points to document the rateless Fountain Code (LT Code) engine and raw 8-bit binary QR code mode.

- [ ] **Step 2: Update ADRs**

Update ADR 0001 and ADR 0002 to reflect the transition from sequential JSON chunking to rateless LT Code binary packet streaming.

- [ ] **Step 3: Commit**

```bash
git add README.md README.ko.md docs/adr/
git commit -m "docs: update README and ADRs for LT Code Fountain engine"
```
