diff --git a/examples/pwa-loader/app.js b/examples/pwa-loader/app.js
new file mode 100644
index 00000000..d4b21774
--- /dev/null
+++ b/examples/pwa-loader/app.js
@@ -0,0 +1,1210 @@
+/**
+ * RVF Seed Decoder - Main Application
+ *
+ * Loads the rvf-wasm module for segment-level operations on .rvf files.
+ * Parses RVQS cognitive seed headers in pure JS (matching the 64-byte
+ * binary layout from rvf-types/src/qr_seed.rs).
+ */
+
+'use strict';
+
+// ---------------------------------------------------------------------------
+// Configuration
+// ---------------------------------------------------------------------------
+
+const WASM_PATH = window.RVF_WASM_PATH || './rvf_wasm_bg.wasm';
+
+// RVQS seed constants (from rvf-types/src/qr_seed.rs)
+const SEED_MAGIC = 0x52565153; // "RVQS"
+const SEED_HEADER_SIZE = 64;
+const FLAG_HAS_MICROKERNEL = 0x0001;
+const FLAG_HAS_DOWNLOAD = 0x0002;
+const FLAG_SIGNED = 0x0004;
+const FLAG_OFFLINE_CAPABLE = 0x0008;
+const FLAG_ENCRYPTED = 0x0010;
+const FLAG_COMPRESSED = 0x0020;
+const FLAG_HAS_VECTORS = 0x0040;
+const FLAG_STREAM_UPGRADE = 0x0080;
+
+// Download manifest TLV tags
+const DL_TAG_HOST_PRIMARY = 0x0001;
+const DL_TAG_HOST_FALLBACK = 0x0002;
+const DL_TAG_CONTENT_HASH = 0x0003;
+const DL_TAG_TOTAL_SIZE = 0x0004;
+const DL_TAG_LAYER_MANIFEST = 0x0005;
+const DL_TAG_SESSION_TOKEN = 0x0006;
+const DL_TAG_TTL = 0x0007;
+const DL_TAG_CERT_PIN = 0x0008;
+
+// Layer ID names
+const LAYER_NAMES = {
+ 0: 'Level 0 Manifest',
+ 1: 'Hot Cache',
+ 2: 'HNSW Layer A',
+ 3: 'Quant Dict',
+ 4: 'HNSW Layer B',
+ 5: 'Full Vectors',
+ 6: 'HNSW Layer C',
+};
+
+// RVF segment magic (from rvf-types constants)
+const SEGMENT_MAGIC = 0x52564653; // "RVFS"
+
+// Data type names
+const DTYPE_NAMES = {
+ 0: 'F32',
+ 1: 'F16',
+ 2: 'BF16',
+ 3: 'I8',
+ 4: 'Binary',
+};
+
+// Signature algorithm names
+const SIG_ALGO_NAMES = {
+ 0: 'Ed25519',
+ 1: 'ML-DSA-65',
+ 2: 'HMAC-SHA256',
+};
+
+// ---------------------------------------------------------------------------
+// State
+// ---------------------------------------------------------------------------
+
+let wasmInstance = null;
+let wasmMemory = null;
+let wasmReady = false;
+let scannerStream = null;
+let scannerAnimFrame = null;
+
+// ---------------------------------------------------------------------------
+// WASM Loader
+// ---------------------------------------------------------------------------
+
+async function loadWasm() {
+ try {
+ setStatus('Loading WASM module...');
+ const response = await fetch(WASM_PATH);
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+ const bytes = await response.arrayBuffer();
+ const importObject = { env: {} };
+ const result = await WebAssembly.instantiate(bytes, importObject);
+ wasmInstance = result.instance;
+ wasmMemory = wasmInstance.exports.memory;
+ wasmReady = true;
+ setStatus('WASM loaded -- ready', 'success');
+ return true;
+ } catch (err) {
+ wasmReady = false;
+ setStatus(`WASM unavailable: ${err.message}. Falling back to JS-only parsing.`, 'error');
+ return false;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// WASM Helpers
+// ---------------------------------------------------------------------------
+
+/** Allocate bytes in WASM memory and copy data in. Returns ptr. */
+function wasmWrite(data) {
+ const ptr = wasmInstance.exports.rvf_alloc(data.length);
+ if (ptr === 0) throw new Error('WASM allocation failed');
+ const mem = new Uint8Array(wasmMemory.buffer, ptr, data.length);
+ mem.set(data);
+ return ptr;
+}
+
+/** Free WASM memory. */
+function wasmFree(ptr, size) {
+ wasmInstance.exports.rvf_free(ptr, size);
+}
+
+/** Read bytes from WASM memory. */
+function wasmRead(ptr, len) {
+ return new Uint8Array(wasmMemory.buffer, ptr, len).slice();
+}
+
+// ---------------------------------------------------------------------------
+// Binary Read Helpers
+// ---------------------------------------------------------------------------
+
+function readU16LE(buf, off) {
+ return buf[off] | (buf[off + 1] << 8);
+}
+
+function readU32LE(buf, off) {
+ return (buf[off] | (buf[off + 1] << 8) | (buf[off + 2] << 16) | (buf[off + 3] << 24)) >>> 0;
+}
+
+function readU64LE(buf, off) {
+ const lo = readU32LE(buf, off);
+ const hi = readU32LE(buf, off + 4);
+ return lo + hi * 0x100000000;
+}
+
+function toHex(bytes, maxLen) {
+ const arr = maxLen ? bytes.slice(0, maxLen) : bytes;
+ let hex = '';
+ for (let i = 0; i < arr.length; i++) {
+ hex += arr[i].toString(16).padStart(2, '0');
+ }
+ if (maxLen && bytes.length > maxLen) {
+ hex += '...';
+ }
+ return hex;
+}
+
+function formatBytes(n) {
+ if (n < 1024) return n + ' B';
+ if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
+ if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(2) + ' MB';
+ return (n / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
+}
+
+function formatTimestamp(ns) {
+ if (ns === 0) return '(not set)';
+ try {
+ const ms = ns / 1e6;
+ return new Date(ms).toISOString();
+ } catch {
+ return '(invalid)';
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Seed Header Parser (Pure JS - matches rvf-types/src/qr_seed.rs layout)
+// ---------------------------------------------------------------------------
+
+/**
+ * Parse RVQS seed header from raw bytes.
+ * @param {Uint8Array} data - Full seed payload (>= 64 bytes)
+ * @returns {object} Parsed seed header and manifest
+ */
+function parseSeedHeader(data) {
+ if (data.length < SEED_HEADER_SIZE) {
+ throw new Error(`Seed too small: ${data.length} bytes (need >= ${SEED_HEADER_SIZE})`);
+ }
+
+ const magic = readU32LE(data, 0x00);
+ if (magic !== SEED_MAGIC) {
+ throw new Error(
+ `Bad magic: 0x${magic.toString(16).padStart(8, '0')} (expected 0x${SEED_MAGIC.toString(16).padStart(8, '0')} "RVQS")`
+ );
+ }
+
+ const header = {
+ magic,
+ version: readU16LE(data, 0x04),
+ flags: readU16LE(data, 0x06),
+ fileId: data.slice(0x08, 0x10),
+ totalVectorCount: readU32LE(data, 0x10),
+ dimension: readU16LE(data, 0x14),
+ baseDtype: data[0x16],
+ profileId: data[0x17],
+ createdNs: readU64LE(data, 0x18),
+ microkernelOffset: readU32LE(data, 0x20),
+ microkernelSize: readU32LE(data, 0x24),
+ downloadManifestOffset: readU32LE(data, 0x28),
+ downloadManifestSize: readU32LE(data, 0x2c),
+ sigAlgo: readU16LE(data, 0x30),
+ sigLength: readU16LE(data, 0x32),
+ totalSeedSize: readU32LE(data, 0x34),
+ contentHash: data.slice(0x38, 0x40),
+ };
+
+ // Decode flags
+ header.flagNames = [];
+ const flagDefs = [
+ [FLAG_HAS_MICROKERNEL, 'MICROKERNEL'],
+ [FLAG_HAS_DOWNLOAD, 'DOWNLOAD'],
+ [FLAG_SIGNED, 'SIGNED'],
+ [FLAG_OFFLINE_CAPABLE, 'OFFLINE'],
+ [FLAG_ENCRYPTED, 'ENCRYPTED'],
+ [FLAG_COMPRESSED, 'COMPRESSED'],
+ [FLAG_HAS_VECTORS, 'VECTORS'],
+ [FLAG_STREAM_UPGRADE, 'STREAM_UPGRADE'],
+ ];
+ for (const [bit, name] of flagDefs) {
+ if (header.flags & bit) header.flagNames.push(name);
+ }
+
+ return header;
+}
+
+/**
+ * Parse download manifest TLV from seed payload.
+ * @param {Uint8Array} data - Full seed payload
+ * @param {object} header - Parsed header from parseSeedHeader
+ * @returns {object} Manifest with hosts, layers, etc.
+ */
+function parseManifest(data, header) {
+ const manifest = {
+ hosts: [],
+ contentHash: null,
+ totalFileSize: null,
+ layers: [],
+ sessionToken: null,
+ tokenTtl: null,
+ certPin: null,
+ };
+
+ if (!(header.flags & FLAG_HAS_DOWNLOAD) || header.downloadManifestSize === 0) {
+ return manifest;
+ }
+
+ const start = header.downloadManifestOffset;
+ const end = start + header.downloadManifestSize;
+ if (end > data.length) return manifest;
+
+ let pos = start;
+ while (pos + 4 <= end) {
+ const tag = readU16LE(data, pos);
+ const length = readU16LE(data, pos + 2);
+ pos += 4;
+
+ if (pos + length > end) break;
+
+ const value = data.slice(pos, pos + length);
+
+ switch (tag) {
+ case DL_TAG_HOST_PRIMARY:
+ case DL_TAG_HOST_FALLBACK: {
+ if (length >= 150) {
+ const urlLength = readU16LE(value, 0);
+ const urlBytes = value.slice(2, 2 + Math.min(urlLength, 128));
+ let url = '';
+ try { url = new TextDecoder().decode(urlBytes); } catch { /* ignore */ }
+ const priority = readU16LE(value, 130);
+ const region = readU16LE(value, 132);
+ const hostKeyHash = value.slice(134, 150);
+ manifest.hosts.push({
+ url,
+ priority,
+ region,
+ hostKeyHash,
+ isPrimary: tag === DL_TAG_HOST_PRIMARY,
+ });
+ }
+ break;
+ }
+ case DL_TAG_CONTENT_HASH: {
+ if (length >= 32) {
+ manifest.contentHash = value.slice(0, 32);
+ }
+ break;
+ }
+ case DL_TAG_TOTAL_SIZE: {
+ if (length >= 8) {
+ manifest.totalFileSize = readU64LE(value, 0);
+ }
+ break;
+ }
+ case DL_TAG_LAYER_MANIFEST: {
+ if (length > 0) {
+ const layerCount = value[0];
+ let lpos = 1;
+ for (let i = 0; i < layerCount; i++) {
+ if (lpos + 27 > value.length) break;
+ const layerId = value[lpos];
+ const priority = value[lpos + 1];
+ const offset = readU32LE(value, lpos + 2);
+ const size = readU32LE(value, lpos + 6);
+ const contentHash = value.slice(lpos + 10, lpos + 26);
+ const required = value[lpos + 26];
+ manifest.layers.push({
+ layerId,
+ name: LAYER_NAMES[layerId] || `Layer ${layerId}`,
+ priority,
+ offset,
+ size,
+ contentHash,
+ required: required === 1,
+ });
+ lpos += 27;
+ }
+ }
+ break;
+ }
+ case DL_TAG_SESSION_TOKEN: {
+ if (length >= 16) {
+ manifest.sessionToken = value.slice(0, 16);
+ }
+ break;
+ }
+ case DL_TAG_TTL: {
+ if (length >= 4) {
+ manifest.tokenTtl = readU32LE(value, 0);
+ }
+ break;
+ }
+ case DL_TAG_CERT_PIN: {
+ if (length >= 32) {
+ manifest.certPin = value.slice(0, 32);
+ }
+ break;
+ }
+ default:
+ // Unknown tags are forward-compatible, skip.
+ break;
+ }
+
+ pos += length;
+ }
+
+ return manifest;
+}
+
+/**
+ * Extract signature bytes from seed payload.
+ * @param {Uint8Array} data - Full seed payload
+ * @param {object} header - Parsed header
+ * @returns {Uint8Array|null} Signature bytes
+ */
+function extractSignature(data, header) {
+ if (!(header.flags & FLAG_SIGNED) || header.sigLength === 0) return null;
+ const sigStart = header.totalSeedSize - header.sigLength;
+ const sigEnd = header.totalSeedSize;
+ if (sigEnd > data.length) return null;
+ return data.slice(sigStart, sigEnd);
+}
+
+/**
+ * Extract microkernel bytes from seed payload.
+ * @param {Uint8Array} data - Full seed payload
+ * @param {object} header - Parsed header
+ * @returns {Uint8Array|null} Microkernel bytes (compressed)
+ */
+function extractMicrokernel(data, header) {
+ if (!(header.flags & FLAG_HAS_MICROKERNEL) || header.microkernelSize === 0) return null;
+ const start = header.microkernelOffset;
+ const end = start + header.microkernelSize;
+ if (end > data.length) return null;
+ return data.slice(start, end);
+}
+
+// ---------------------------------------------------------------------------
+// RVF Segment Parser (uses WASM when available, pure JS fallback)
+// ---------------------------------------------------------------------------
+
+/**
+ * Parse .rvf file segments.
+ * @param {Uint8Array} data - Raw .rvf file bytes
+ * @returns {object} Parsed segment info
+ */
+function parseRvfSegments(data) {
+ const result = {
+ segmentCount: 0,
+ segments: [],
+ storeHandle: -1,
+ headerValid: false,
+ };
+
+ // Try WASM path first
+ if (wasmReady) {
+ try {
+ return parseRvfSegmentsWasm(data);
+ } catch (err) {
+ // Fall through to JS fallback
+ }
+ }
+
+ // JS fallback: scan for segment headers
+ return parseRvfSegmentsJS(data);
+}
+
+function parseRvfSegmentsWasm(data) {
+ const bufPtr = wasmWrite(data);
+ try {
+ // Header verification
+ const headerResult = wasmInstance.exports.rvf_verify_header(bufPtr);
+
+ // Segment count
+ const segCount = wasmInstance.exports.rvf_segment_count(bufPtr, data.length);
+
+ // Segment info (28 bytes per segment)
+ const segments = [];
+ const infoSize = 28;
+ const infoPtr = wasmInstance.exports.rvf_alloc(infoSize);
+ try {
+ for (let i = 0; i < segCount; i++) {
+ const rc = wasmInstance.exports.rvf_segment_info(bufPtr, data.length, i, infoPtr);
+ if (rc === 0) {
+ const info = wasmRead(infoPtr, infoSize);
+ const segId = readU64LE(info, 0);
+ const segType = info[8];
+ const payloadLength = readU64LE(info, 12);
+ const offset = readU64LE(info, 20);
+ segments.push({ segId, segType, payloadLength, offset });
+ }
+ }
+ } finally {
+ wasmFree(infoPtr, infoSize);
+ }
+
+ // CRC32C verification
+ let checksumValid = null;
+ if (data.length >= 4) {
+ checksumValid = wasmInstance.exports.rvf_verify_checksum(bufPtr, data.length) === 1;
+ }
+
+ // Try to open as a store
+ let storeHandle = -1;
+ try {
+ storeHandle = wasmInstance.exports.rvf_store_open(bufPtr, data.length);
+ } catch { /* ignore */ }
+
+ return {
+ segmentCount: segCount,
+ segments,
+ headerValid: headerResult === 0,
+ checksumValid,
+ storeHandle,
+ };
+ } finally {
+ wasmFree(bufPtr, data.length);
+ }
+}
+
+function parseRvfSegmentsJS(data) {
+ const MAGIC_BYTES = [
+ SEGMENT_MAGIC & 0xff,
+ (SEGMENT_MAGIC >> 8) & 0xff,
+ (SEGMENT_MAGIC >> 16) & 0xff,
+ (SEGMENT_MAGIC >> 24) & 0xff,
+ ];
+ const HEADER_SIZE = 64; // rvf-types SEGMENT_HEADER_SIZE
+ const segments = [];
+
+ if (data.length < HEADER_SIZE) {
+ return { segmentCount: 0, segments, headerValid: false, storeHandle: -1 };
+ }
+
+ let i = 0;
+ const last = data.length - HEADER_SIZE;
+
+ while (i <= last) {
+ if (
+ data[i] === MAGIC_BYTES[0] &&
+ data[i + 1] === MAGIC_BYTES[1] &&
+ data[i + 2] === MAGIC_BYTES[2] &&
+ data[i + 3] === MAGIC_BYTES[3]
+ ) {
+ const version = data[i + 4];
+ if (version === 1) {
+ const segType = data[i + 5];
+ const segId = readU64LE(data, i + 8);
+ const payloadLength = readU64LE(data, i + 16);
+
+ segments.push({
+ segId,
+ segType,
+ payloadLength,
+ offset: i,
+ });
+
+ const total = HEADER_SIZE + payloadLength;
+ const next = i + total;
+ if (next > i && next <= data.length) {
+ i = next;
+ continue;
+ }
+ }
+ }
+ i++;
+ }
+
+ // Check first 4 bytes for magic
+ const headerValid =
+ data.length >= 4 &&
+ readU32LE(data, 0) === SEGMENT_MAGIC;
+
+ return {
+ segmentCount: segments.length,
+ segments,
+ headerValid,
+ storeHandle: -1,
+ };
+}
+
+// Segment type names
+const SEG_TYPE_NAMES = {
+ 0x01: 'Vec',
+ 0x02: 'HNSW',
+ 0x03: 'IVF',
+ 0x04: 'PQ',
+ 0x05: 'Manifest',
+ 0x06: 'Metadata',
+ 0x10: 'WASM',
+};
+
+// ---------------------------------------------------------------------------
+// Decode Entry Points
+// ---------------------------------------------------------------------------
+
+/**
+ * Decode a seed (RVQS) or RVF file from raw bytes.
+ * Detects type by magic number.
+ */
+async function decodeSeed(bytes) {
+ const data = new Uint8Array(bytes);
+ if (data.length < 4) {
+ throw new Error('File too small to contain valid data');
+ }
+
+ const magic = readU32LE(data, 0);
+
+ if (magic === SEED_MAGIC) {
+ // RVQS cognitive seed
+ const header = parseSeedHeader(data);
+ const manifest = parseManifest(data, header);
+ const signature = extractSignature(data, header);
+ const microkernel = extractMicrokernel(data, header);
+ return {
+ type: 'seed',
+ header,
+ manifest,
+ signature,
+ microkernel,
+ raw: data,
+ };
+ }
+
+ if (magic === SEGMENT_MAGIC) {
+ // Raw .rvf file
+ const segInfo = parseRvfSegments(data);
+ return {
+ type: 'rvf',
+ segInfo,
+ raw: data,
+ };
+ }
+
+ // Try as witness bundle (check for witness-specific patterns)
+ return decodeWitness(data);
+}
+
+/**
+ * Decode a witness bundle header.
+ * Witness bundles are envelope structures containing evidence chains.
+ * We parse the outer framing to show whatever structure is present.
+ */
+function decodeWitness(data) {
+ if (data.length < 4) {
+ throw new Error('File too small for witness bundle');
+ }
+
+ const magic = readU32LE(data, 0);
+
+ // If this is actually a seed or RVF segment, redirect
+ if (magic === SEED_MAGIC || magic === SEGMENT_MAGIC) {
+ throw new Error('Not a witness bundle (detected seed or segment magic)');
+ }
+
+ // Generic binary inspection for unknown formats
+ return {
+ type: 'witness',
+ size: data.length,
+ magic: '0x' + magic.toString(16).padStart(8, '0'),
+ raw: data,
+ preview: data.slice(0, Math.min(256, data.length)),
+ };
+}
+
+// ---------------------------------------------------------------------------
+// UI Rendering
+// ---------------------------------------------------------------------------
+
+function setStatus(msg, cls) {
+ const el = document.getElementById('status');
+ el.textContent = msg;
+ el.className = 'status-bar' + (cls ? ' ' + cls : '');
+}
+
+function renderResults(result) {
+ const container = document.getElementById('results');
+ container.innerHTML = '';
+
+ if (result.type === 'seed') {
+ renderSeedResult(container, result);
+ } else if (result.type === 'rvf') {
+ renderRvfResult(container, result);
+ } else if (result.type === 'witness') {
+ renderWitnessResult(container, result);
+ }
+}
+
+function renderSeedResult(container, result) {
+ const { header, manifest, signature, microkernel, raw } = result;
+
+ // -- Header Card --
+ const headerCard = createCard('Seed Header');
+ const grid = document.createElement('dl');
+ grid.className = 'info-grid';
+
+ addInfoRow(grid, 'Magic', `0x${header.magic.toString(16).padStart(8, '0')} (RVQS)`);
+ addInfoRow(grid, 'Version', header.version.toString());
+ addInfoRow(grid, 'File ID', toHex(header.fileId));
+ addInfoRow(grid, 'Total Vectors', header.totalVectorCount.toLocaleString());
+ addInfoRow(grid, 'Dimension', header.dimension.toString());
+ addInfoRow(grid, 'Data Type', DTYPE_NAMES[header.baseDtype] || `0x${header.baseDtype.toString(16)}`);
+ addInfoRow(grid, 'Profile ID', header.profileId.toString());
+ addInfoRow(grid, 'Created', formatTimestamp(header.createdNs));
+ addInfoRow(grid, 'Seed Size', formatBytes(header.totalSeedSize));
+ addInfoRow(grid, 'Content Hash', toHex(header.contentHash));
+
+ if (header.flags & FLAG_HAS_MICROKERNEL) {
+ addInfoRow(grid, 'Microkernel', `${formatBytes(header.microkernelSize)} @ offset ${header.microkernelOffset}`);
+ }
+
+ if (header.flags & FLAG_SIGNED) {
+ addInfoRow(grid, 'Signature', `${SIG_ALGO_NAMES[header.sigAlgo] || `algo ${header.sigAlgo}`}, ${header.sigLength} bytes`);
+ }
+
+ headerCard.appendChild(grid);
+
+ // Flags badges
+ const flagsWrap = document.createElement('div');
+ flagsWrap.style.marginTop = '0.75rem';
+ const flagsLabel = document.createElement('dt');
+ flagsLabel.style.color = 'var(--text-muted)';
+ flagsLabel.style.fontSize = '0.85rem';
+ flagsLabel.style.marginBottom = '0.35rem';
+ flagsLabel.textContent = 'Flags';
+ flagsWrap.appendChild(flagsLabel);
+
+ const flagsList = document.createElement('ul');
+ flagsList.className = 'flags-list';
+ const allFlags = [
+ [FLAG_HAS_MICROKERNEL, 'MICROKERNEL'],
+ [FLAG_HAS_DOWNLOAD, 'DOWNLOAD'],
+ [FLAG_SIGNED, 'SIGNED'],
+ [FLAG_OFFLINE_CAPABLE, 'OFFLINE'],
+ [FLAG_ENCRYPTED, 'ENCRYPTED'],
+ [FLAG_COMPRESSED, 'COMPRESSED'],
+ [FLAG_HAS_VECTORS, 'VECTORS'],
+ [FLAG_STREAM_UPGRADE, 'STREAM_UPGRADE'],
+ ];
+ for (const [bit, name] of allFlags) {
+ const li = document.createElement('li');
+ const badge = document.createElement('span');
+ badge.className = 'badge ' + (header.flags & bit ? 'badge-on' : 'badge-off');
+ badge.textContent = name;
+ li.appendChild(badge);
+ flagsList.appendChild(li);
+ }
+ flagsWrap.appendChild(flagsList);
+ headerCard.appendChild(flagsWrap);
+
+ container.appendChild(headerCard);
+
+ // -- Hosts Card --
+ if (manifest.hosts.length > 0) {
+ const hostsCard = createCard('Download Hosts');
+ const table = document.createElement('table');
+ table.className = 'data-table';
+ table.innerHTML = `
+
+
+ `;
+ const tbody = document.createElement('tbody');
+ for (const host of manifest.hosts) {
+ const tr = document.createElement('tr');
+ tr.innerHTML = `
+ Type URL Priority Region
+
Drop an .rvf, seed binary, or witness bundle here
+