Repository navigation
Expand file tree
/
Copy pathpunctim_adapter.jsonl
More file actions
7 lines (7 loc) · 146 KB
/
Copy pathpunctim_adapter.jsonl
File metadata and controls
7 lines (7 loc) · 146 KB
1
2
3
4
5
6
7
{"messages": [{"role": "system", "content": "You are an expert DCF adapter implementer. You build protocols (audio, game, text, SSTV, snake, mesh) as adapters over the 17-byte DeModFrame wire quantum.\n\nADAPTER PATTERN:\n- One adapter message → 1 + ceil(payload_len/4) DeModFrame packets\n- seq = packet_id[15:5] | frag_idx[4:0] (for audio/game); text uses frag_idx[9:0]\n- frag_idx 0 = descriptor: [len, frag_total, codec_id|msg_type_id, flags]\n- Payload ≤ 124 B/block (4 bytes per frame × 31 frames max)\n- L2 framing is codec/message-type-agnostic and byte-certified\n- Adding new codecs/message types never changes the wire vectors\n\nCONVENTIONS:\n- Encode: serialize payload → fragment into ≤124B chunks → wrap each in DeModFrame\n- Decode: collect frames by packet_id → reassemble payload → validate CRC per frame\n- Use CTRL (type 3) frames for audio; DATA (type 0) frames for game/text/SSTV\n- Descriptor flags: bit0 RELIABLE, bit1 ORDERED, bit2 END_TICK (adapter-specific)\n"}, {"role": "user", "content": "Implement the DCF audio adapter in Rust.\n\nWire quantum spec (for context):\n# DCF Wire Quantum — Canonical Specification\n\n**Version 1** · DeMoD LLC · This document is normative.\n\nThe DCF wire protocol has exactly **one** wire format: the 17-byte `DeModFrame`\n(version nibble = 1). All other formats (the proto-message UDP header, the C\nSDK's `DCFMessageHeader`) are **adapters** over this quantum and must be\nexplicitly documented as such.\n\n## Wire Layout\n\n```\n Byte Field Width Description\n ──── ────────── ───── ─────────────────────────────────────────\n 0 sync 1 B Fixed 0xD3 (first validity gate)\n 1 flags 1 B [7:4] version (currently 1) | [3:0] type (0–3)\n 2–3 seq 2 B Big-endian u16, rolling sequence counter\n 4–5 src_id 2 B Big-endian u16, source node ID\n 6–7 dst_id 2 B Big-endian u16, destination (0xFFFF = broadcast)\n 8–11 payload 4 B Application data\n 12–14 timestamp 3 B 24-bit µs offset, big-endian, wraps ~16.7 s\n 15–16 crc16 2 B CRC-16/CCITT-FALSE over bytes [0..14]\n```\n\n**Total: 17 bytes = 136 bits.** The free field data is 108 bits (4+16+16+16+32+24).\n\n## Frame Types\n\n| Value | Name | Purpose |\n|-------|--------|-----------------------------|\n| 0 | DATA | Application payload |\n| 1 | ACK | Acknowledgement |\n| 2 | BEACON | Clock sync / broadcast |\n| 3 | CTRL | Control / fragmented audio |\n\n## Validity\n\nA frame is valid iff:\n1. `frame[0] == 0xD3` (sync byte)\n2. `(frame[1] >> 4) == 1` (version nibble)\n3. `CRC-16/CCITT-FALSE(frame[0..14]) == (frame[15] << 8) | frame[16]` (CRC)\n\nFor a random byte stream, the probability of a 17-byte window passing all three\nchecks is 2⁻²⁸ (Theorem 2.1, `wire_quanta_category.md`).\n\n## CRC-16/CCITT-FALSE\n\n- Polynomial: `0x1021`\n- Init: `0xFFFF`\n- RefIn/RefOut: `false`\n- XorOut: `0x0000`\n- Check value: `CRC(\"123456789\") = 0x29B1`\n\n## Anchors\n\n| Quantity | Value | Source |\n|----------|-------|--------|\n| CRC(\"123456789\") | `0x29B1` |...\n\nAdapter specification:\n# DCF-Audio — Collaborative Audio over the DeModFrame Wire\n\n**Version 1** · DeMoD LLC · This document is normative. Companion to\n[`WIRE_QUANTUM_SPEC.md`](WIRE_QUANTUM_SPEC.md).\n\nDCF-Audio carries real-time, collaborative audio (jamming, talkback) over the\nPunctim mesh. It introduces **no new wire format**: a 20 ms codec block is an\n**adapter** over the 17-byte `DeModFrame` quantum, serialised into a short burst of\nordinary `CTRL` (type 3) frames. The framing is **codec-agnostic and\nbyte-deterministic across C, Rust, and Python**, and is pinned by a finite\ncertificate exactly like the wire quantum itself.\n\n## Layered model\n\n| Layer | What | Certified? |\n|-------|------|-----------|\n| **L1** codec registry | Pluggable codecs keyed by `codec_id` (Opus / PCM-diag / Faust-PM). Output is opaque to L2. | PCM-diag bytes & PM param layout: **yes**. Opus & PM *audio*: no (float/version-dependent). |\n| **L2** framing | `packetize` / reassemble: codec bytes ⇄ `DeModFrame` CTRL frames. | **Yes** — `audio_vectors.json` |\n| **L3** jitter buffer + PLC | Reorder/dejitter on `packet_id`/`timestamp_us`; conceal lost packets. | Runtime (unit-tested) |\n| **L4** transport + demo | Send frames over the mesh; 2-peer loopback jam. | Demo |\n\nThe key invariant: **`codec_id` lives in the L2 descriptor, so adding codecs never\nchanges the L2 vectors.**\n\n## L2 framing (normative)\n\nOne 20 ms block → one *audio packet* → `1 + frag_total` frames. Every frame is a\nfully valid `DeModFrame` (version = 1, type = `CTRL` = 3) and still passes the\n246-vector wire certificate.\n\n```\n seq (u16) = packet_id[15:5] (11 bits) | frag_idx[4:0] (5 bits)\n frag_idx == 0 descriptor : payload = [payload_len, frag_total, codec_id, flags]\n frag_idx 1..N data : payload = bytes[(k-1)*4 .. +4] (last frame zero-padded)\n frag_total = ceil(payload_len / 4)\n timestamp_us = 24-bit block capture time, identical across a packet's frames\n src_id/dst_id = node ids (0xFFFF = broadcast for a jam)\n```\n\nBounds (from the 5-bit fragment field):\n\n| Quantity | Value |\n|----------|-------|\n| Max data fragments | 31 |\n| **Max codec payload** | **124 bytes / 20 ms** (≈ 49.6 kbps) |\n| Max `packet_id` | 2047 (wraps ≈ 41 s at 20 ms) |\n| Frames per packet | `1 + ceil(payload_len/4)` (1–32) |\n\nDescriptor flags: bit 0 = end-of-talkspurt, bit 1 = PM voice profile.\n\n**Reassembly.** Buffer by modular `packet_id`; gather the descriptor and all data\nfragments; concatenate and truncate to `payload_len`; emit\n`(packet_id, timestamp_us, codec_id, flags, payload)`. Duplicates are ignored. Any\nmissing fragment ⇒ the packet is **lost** ⇒ L3 runs the codec's PLC.\n\n## Codec registry\n\n| id | Name | Profile (20 ms blocks) | ≤124 B? | Byte-deterministic? |\n|----|------|------------------------|---------|---------------------|\n| 0 | Opus | 48 kHz mono, ~24 kbps (≈60 B) | yes | No (libopus-version dependent) |\n| 1 | PCM-diag | 6 kHz, 8-bit linear, mono = 120 B | yes | **Yes** (round-trip pinned) |\n| 2 | Faust-PM | phase-mod synthesis, **8-byte param block** | yes | Param layout: **yes**; synthesis audio: no |\n| 3 | *reserved* | (declined low-bitrate speech codec) | — | — |\n\nBecause a full-band PCM block cannot fit in 124 B, the diagnostic PCM codec runs at\n6 kHz / 8-bit (one byte per sample, 120 B). Opus fragments ≈ 16 frames; PM is just\n3 frames, so PM and Opus tolerate frame loss far better than PCM-diag.\n\n### Faust phase-mod codec (id 2) — 8-byte parameter block\n\nThe wire carries compact synthesis parameters; the decoder resynthesises timbre via\na ratio-locked phase-modulation oscillator (`codec/faust/dcf_pm_codec.dsp`, committed\nas Faust→C in `dcf_pm_codec.gen.c`). The **layout is certified**; the synthesised\naudio is not.\n\n```\n byte: 0 1 2 3 4 5 6 7\n field: f0_hi f0_lo amp mod_index mod_ratio bright env flags\n u16 (cents-mapped fundamental) u8 each (carrier:mod is u8 fixed-point)\n```\n\n## Certification\n\nMirrors the wire quantum: a Python reference emits golden vectors; C and Rust diff\nagainst them byte-for-byte.\n\n```sh\n# regenerate + verify the laws, then diff against the committed vectors\npython3 python/MCP/gen_audio_vectors.py /tmp/audio_vectors.json\ndiff /tmp/audio_vectors.json Documentation/audio_vectors.json\ndiff /tmp/pm_param_vectors.json Documentation/pm_param_vectors.json\ndiff /tmp/audio_vectors.gen.h codec/audio_vectors.gen.h\n\n# C (L2-only; no libopus / Faust needed)\ngcc -std=c11 -Wall -Wextra -I codec C_SDK/tests/test_audio_certify.c -lm -o /tmp/ac && /tmp/ac\n\n# Rust\ncd codec && cargo test --test certify_audio\n```\n\n| Artifact | Role |\n|----------|------|\n| `Documentation/audio_vectors.json` (= `python/MCP/`) | framing + reassembly + PCM round-trip vectors |\n| `Documentation/pm_param_vectors.json` (= `python/MCP/`) | PM 8-byte param round-trip |\n| `codec/audio_vectors.gen.h` | same vectors as a C header (dependency-free C test) |\n| `python/MCP/audiolab_core.py` | Python L2 reference (imports the certified wire codec) |\n| `python/MCP/gen_audio_vectors.py` | executable laws + vector generator |\n\n## Reference implementations\n\n| Language | File | Entry points |\n|----------|------|--------------|\n| C | `codec/demod_audio.h` | `dcf_audio_packetize`, `dcf_audio_reasm_*`, `dcf_codec_get`, `dcf_pm_pack/unpack` |\n| Rust | `codec/src/audio.rs` | `packetize`, `AudioReassembler`, `codec_for`, `pm_pack/unpack` |\n| Python | `python/MCP/audiolab_core.py` | `packetize`, `AudioReassembler`, `pm_pack/unpack` |\n| Lua | `lua/dcf_audio.lua` | self-certifying L2 codec (LGPL-3.0-only, dual-licensed — see `lua/LICENSING.md`) |\n\n## Latency budget (informative)\n\n~5 ms capture · 5–10 ms encode (Opus/PM) · 20 ms one-way network · 20–40 ms jitter\nbuffer (1–2 packets) · 5–10 ms decode · 5 ms playout → **< 100 ms** one-way for\ncollaborative jamming. The jitter window and PLC aggressiveness are tunable.\n\n## Transport & demo\n\n- **Rust SDK:** `DcfNode::send_audio_dcf(codec_id, encoded, packet_id, ts_us)`\n packetizes a block and ships each 17-byte frame as an unreliable `AUDIO` message;\n the receiver feeds payloads to `reassemble_audio_payload`.\n- **2-peer jam:** `cd codec && cargo run --example jam_loopback -- --codec pcm`\n (add `--loss 0.05` to exercise PLC; `--features opus|pm` + `--codec opus|pm`).\n\n## TERMINUS integration (informative)\n\n`unified-UI` (DeMoD TERMINUS) is a separate repo. The intended hookup feeds the\norchestrator's real-time audio through L1–L4 and surfaces peers via the existing\ndevice-bridge `/v1/mesh` endpoint — a future `demod-jam` app, not built here.\n\n\nGolden vectors (sample):\n{\n \"encode_basis\": [],\n \"syndrome_basis\": []\n}\n\nProvide the complete implementation with encode/decode functions."}, {"role": "assistant", "content": "// SPDX-License-Identifier: LGPL-3.0-only\n//! DCF-Audio: collaborative audio over the DeModFrame wire.\n//!\n//! Audio is an *adapter* over the 17-byte DeModFrame quantum (`frame.rs`), not a new\n//! wire format. A 20 ms codec block is serialised into `1 + frag_total` ordinary CTRL\n//! frames. This L2 framing is codec-agnostic and byte-deterministic across C/Rust/Python\n//! — it is pinned by `Documentation/audio_vectors.json`. See `Documentation/DCF_AUDIO_SPEC.md`.\n//!\n//! Layout (all frames version=1, type=CTRL(3), big-endian):\n//! seq = packet_id[15:5] (11 bits) | frag_idx[4:0] (5 bits)\n//! frag_idx 0 descriptor : payload = [payload_len, frag_total, codec_id, flags]\n//! frag_idx k data : payload = bytes[(k-1)*4 .. +4] (last frame zero-padded)\n//! frag_total = ceil(payload_len/4) (<= 31 => payload_len <= 124 bytes / 20 ms)\n\nuse crate::{Frame, FrameType};\nuse std::collections::BTreeMap;\n\n// ── L2 constants ────────────────────────────────────────────────────────────\npub const FRAG_BITS: u16 = 5;\npub const FRAG_MASK: u16 = 0x1F;\npub const MAX_FRAGS: usize = 31;\npub const MAX_PAYLOAD: usize = MAX_FRAGS * 4; // 124 bytes / 20 ms block\npub const MAX_PACKET_ID: u16 = (1 << (16 - FRAG_BITS)) - 1; // 2047\n\n// Codec ids (profiles in DCF_AUDIO_SPEC.md).\npub const CODEC_OPUS: u8 = 0;\npub const CODEC_PCM_DIAG: u8 = 1;\npub const CODEC_FAUST_PM: u8 = 2;\n// id 3 reserved (declined low-bitrate speech codec)\n\n// Descriptor flag bits.\npub const FLAG_END_TALKSPURT: u8 = 0x01;\npub const FLAG_PM_VOICE: u8 = 0x02;\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum AudioError {\n /// Codec block exceeds the 124-byte / 20 ms cap.\n PayloadTooLarge,\n /// packet_id exceeds the 11-bit field.\n BadPacketId,\n}\n\n// ── L2: packetize ───────────────────────────────────────────────────────────\n/// Serialise one codec block into DeModFrame CTRL frames (descriptor first, then\n/// data fragments in order). Each frame is a fully valid 17-byte DeModFrame.\npub fn packetize(\n codec_id: u8,\n payload: &[u8],\n packet_id: u16,\n ts_us: u32,\n src: u16,\n dst: u16,\n flags: u8,\n) -> Result<Vec<[u8; 17]>, AudioError> {\n if payload.len() > MAX_PAYLOAD {\n return Err(AudioError::PayloadTooLarge);\n }\n if packet_id > MAX_PACKET_ID {\n return Err(AudioError::BadPacketId);\n }\n let frag_total = ((payload.len() + 3) / 4) as u8;\n let mut frames = Vec::with_capacity(1 + frag_total as usize);\n\n // frag_idx 0 — descriptor\n let desc_seq = packet_id << FRAG_BITS;\n let desc = [payload.len() as u8, frag_total, codec_id, flags];\n frames.push(Frame::new(1, FrameType::Ctrl, desc_seq, src, dst, desc, ts_us).encode());\n\n // frag_idx 1..frag_total — data, last chunk zero-padded\n for k in 1..=frag_total {\n let off = (k as usize - 1) * 4;\n let mut chunk = [0u8; 4];\n let end = (off + 4).min(payload.len());\n chunk[..end - off].copy_from_slice(&payload[off..end]);\n let seq = (packet_id << FRAG_BITS) | k as u16;\n frames.push(Frame::new(1, FrameType::Ctrl, seq, src, dst, chunk, ts_us).encode());\n }\n Ok(frames)\n}\n\n// ── L2: reassembler ─────────────────────────────────────────────────────────\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct AudioPacket {\n pub packet_id: u16,\n pub ts_us: u32,\n pub codec_id: u8,\n pub flags: u8,\n pub payload: Vec<u8>,\n}\n\n#[derive(Default)]\nstruct Slot {\n desc: Option<(u8, u8, u8, u8)>, // (payload_len, frag_total, codec_id, flags)\n ts_us: u32,\n frags: BTreeMap<u8, [u8; 4]>,\n}\n\n/// Stateful reassembler. `push` emits a completed packet as soon as its descriptor\n/// and every data fragment have arrived; duplicates are ignored; `finalize` reports\n/// any still-incomplete packet as lost. Mirrors the C and Python references.\n#[derive(Default)]\npub struct AudioReassembler {\n slots: BTreeMap<u16, Slot>,\n}\n\nimpl AudioReassembler {\n pub fn new() -> Self {\n Self::default()\n }\n\n pub fn push(&mut self, frame: &[u8; 17]) -> Option<AudioPacket> {\n let d = Frame::decode(&frame[..]).ok()?;\n if d.frame_type != FrameType::Ctrl {\n return None;\n }\n let packet_id = d.seq >> FRAG_BITS;\n let frag_idx = (d.seq & FRAG_MASK) as u8;\n let entry = self.slots.entry(packet_id).or_default();\n entry.ts_us = d.timestamp_us;\n if frag_idx == 0 {\n if entry.desc.is_none() {\n entry.desc = Some((d.payload[0], d.payload[1], d.payload[2], d.payload[3]));\n }\n } else {\n entry.frags.entry(frag_idx).or_insert(d.payload);\n }\n self.try_emit(packet_id)\n }\n\n fn try_emit(&mut self, packet_id: u16) -> Option<AudioPacket> {\n let entry = self.slots.get(&packet_id)?;\n let (payload_len, frag_total, codec_id, flags) = entry.desc?;\n if (1..=frag_total).any(|k| !entry.frags.contains_key(&k)) {\n return None;\n }\n let mut raw = Vec::with_capacity(frag_total as usize * 4);\n for k in 1..=frag_total {\n raw.extend_from_slice(&entry.frags[&k]);\n }\n raw.truncate(payload_len as usize);\n let ts_us = entry.ts_us;\n self.slots.remove(&packet_id);\n Some(AudioPacket { packet_id, ts_us, codec_id, flags, payload: raw })\n }\n\n /// Report every still-incomplete packet as lost (ascending packet_id), clearing state.\n pub fn finalize(&mut self) -> Vec<u16> {\n let lost: Vec<u16> = self.slots.keys().copied().collect(); // BTreeMap => sorted\n self.slots.clear();\n lost\n }\n}\n\n// ── L1: codec interface + registry ──────────────────────────────────────────\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub struct CodecProfile {\n pub codec_id: u8,\n pub sample_rate: u32,\n pub channels: u8,\n pub block_samples: u16,\n pub max_payload: u16,\n}\n\n/// One pluggable codec keyed by `codec_id`. Output is opaque to the L2 framing.\npub trait AudioCodec {\n fn profile(&self) -> CodecProfile;\n fn encode(&mut self, pcm: &[f32]) -> Vec<u8>;\n fn decode(&mut self, data: &[u8]) -> Vec<f32>;\n /// Packet-loss concealment: synthesise one block with no input.\n fn plc(&mut self) -> Vec<f32>;\n}\n\n/// Return a codec instance for `codec_id`, or `None` if not built in this config.\npub fn codec_for(codec_id: u8) -> Option<Box<dyn AudioCodec>> {\n match codec_id {\n CODEC_PCM_DIAG => Some(Box::new(PcmDiag)),\n #[cfg(feature = \"opus\")]\n CODEC_OPUS => opus_codec::OpusCodec::new().map(|c| Box::new(c) as Box<dyn AudioCodec>),\n #[cfg(feature = \"pm\")]\n CODEC_FAUST_PM => Some(Box::new(pm::FaustPmCodec::new())),\n _ => None,\n }\n}\n\n// ── L1: PCM-diagnostic codec (id 1) — 6 kHz 8-bit mono, 120 B/block ─────────\npub const PCM_DIAG_RATE: u32 = 6000;\npub const PCM_DIAG_BLOCK: usize = 120;\n\n/// float [-1,1] -> unsigned 8-bit (mid 128). decode∘encode is byte-lossless.\npub fn pcm_diag_encode(samples: &[f32]) -> Vec<u8> {\n samples\n .iter()\n .map(|&s| ((s * 128.0).round() as i32 + 128).clamp(0, 255) as u8)\n .collect()\n}\n/// unsigned 8-bit -> float [-1,1].\npub fn pcm_diag_decode(data: &[u8]) -> Vec<f32> {\n data.iter().map(|&b| (b as f32 - 128.0) / 128.0).collect()\n}\n\npub struct PcmDiag;\nimpl AudioCodec for PcmDiag {\n fn profile(&self) -> CodecProfile {\n CodecProfile {\n codec_id: CODEC_PCM_DIAG,\n sample_rate: PCM_DIAG_RATE,\n channels: 1,\n block_samples: PCM_DIAG_BLOCK as u16,\n max_payload: PCM_DIAG_BLOCK as u16,\n }\n }\n fn encode(&mut self, pcm: &[f32]) -> Vec<u8> {\n pcm_diag_encode(pcm)\n }\n fn decode(&mut self, data: &[u8]) -> Vec<f32> {\n pcm_diag_decode(data)\n }\n fn plc(&mut self) -> Vec<f32> {\n vec![0.0; PCM_DIAG_BLOCK]\n }\n}\n\n// ── L1: PM (Faust phase-mod, id 2) — 8-byte parameter layout (certified) ────\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]\npub struct PmParams {\n pub f0: u16,\n pub amp: u8,\n pub mod_index: u8,\n pub mod_ratio: u8,\n pub bright: u8,\n pub env: u8,\n pub flags: u8,\n}\n\npub fn pm_pack(p: &PmParams) -> [u8; 8] {\n [\n (p.f0 >> 8) as u8,\n p.f0 as u8,\n p.amp,\n p.mod_index,\n p.mod_ratio,\n p.bright,\n p.env,\n p.flags,\n ]\n}\npub fn pm_unpack(b: &[u8; 8]) -> PmParams {\n PmParams {\n f0: ((b[0] as u16) << 8) | b[1] as u16,\n amp: b[2],\n mod_index: b[3],\n mod_ratio: b[4],\n bright: b[5],\n env: b[6],\n flags: b[7],\n }\n}\n\n// ── L1: Opus codec (id 0) — feature \"opus\" ──────────────────────────────────\n#[cfg(feature = \"opus\")]\nmod opus_codec {\n use super::*;\n use opus::{Application, Channels, Decoder, Encoder};\n\n pub const OPUS_RATE: u32 = 48000;\n pub const OPUS_BLOCK: usize = 960; // 20 ms @ 48 kHz\n\n pub struct OpusCodec {\n enc: Encoder,\n dec: Decoder,\n }\n impl OpusCodec {\n pub fn new() -> Option<Self> {\n let mut enc = Encoder::new(OPUS_RATE, Channels::Mono, Application::Audio).ok()?;\n let _ = enc.set_bitrate(opus::Bitrate::Bits(24000));\n let dec = Decoder::new(OPUS_RATE, Channels::Mono).ok()?;\n Some(Self { enc, dec })\n }\n }\n impl AudioCodec for OpusCodec {\n fn profile(&self) -> CodecProfile {\n CodecProfile {\n codec_id: CODEC_OPUS,\n sample_rate: OPUS_RATE,\n channels: 1,\n block_samples: OPUS_BLOCK as u16,\n max_payload: MAX_PAYLOAD as u16,\n }\n }\n fn encode(&mut self, pcm: &[f32]) -> Vec<u8> {\n let mut out = vec![0u8; MAX_PAYLOAD];\n match self.enc.encode_float(pcm, &mut out) {\n Ok(n) => {\n out.truncate(n);\n out\n }\n Err(_) => Vec::new(),\n }\n }\n fn decode(&mut self, data: &[u8]) -> Vec<f32> {\n let mut pcm = vec![0.0f32; OPUS_BLOCK];\n match self.dec.decode_float(data, &mut pcm, false) {\n Ok(n) => {\n pcm.truncate(n);\n pcm\n }\n Err(_) => vec![0.0; OPUS_BLOCK],\n }\n }\n fn plc(&mut self) -> Vec<f32> {\n // An empty input slice tells libopus to conceal one lost packet.\n let mut pcm = vec![0.0f32; OPUS_BLOCK];\n let _ = self.dec.decode_float(&[], &mut pcm, false);\n pcm\n }\n }\n}\n\n// ── L1: Faust phase-mod synthesis (id 2) — feature \"pm\" ─────────────────────\n#[cfg(feature = \"pm\")]\nmod pm {\n use super::*;\n\n pub const PM_RATE: u32 = 48000;\n pub const PM_BLOCK: usize = 960;\n\n extern \"C\" {\n // Provided by codec/faust/dcf_pm_codec.gen.c (compiled by build.rs).\n // Synthesises `n` samples from the 8-byte param block.\n fn dcf_pm_synth_block_ffi(params8: *const u8, out: *mut f32, n: i32);\n }\n\n pub struct FaustPmCodec;\n impl FaustPmCodec {\n pub fn new() -> Self {\n FaustPmCodec\n }\n }\n impl AudioCodec for FaustPmCodec {\n fn profile(&self) -> CodecProfile {\n CodecProfile {\n codec_id: CODEC_FAUST_PM,\n sample_rate: PM_RATE,\n channels: 1,\n block_samples: PM_BLOCK as u16,\n max_payload: 8,\n }\n }\n fn encode(&mut self, _pcm: &[f32]) -> Vec<u8> {\n // Host analysis fills PmParams; here we emit a neutral block. Real analysis\n // lives in the demo/SDK and packs via pm_pack().\n pm_pack(&PmParams::default()).to_vec()\n }\n fn decode(&mut self, data: &[u8]) -> Vec<f32> {\n if data.len() != 8 {\n return vec![0.0; PM_BLOCK];\n }\n let mut out = vec![0.0f32; PM_BLOCK];\n unsafe { dcf_pm_synth_block_ffi(data.as_ptr(), out.as_mut_ptr(), PM_BLOCK as i32) };\n out\n }\n fn plc(&mut self) -> Vec<f32> {\n vec![0.0; PM_BLOCK]\n }\n }\n}\n"}]}
{"messages": [{"role": "system", "content": "You are an expert DCF adapter implementer. You build protocols (audio, game, text, SSTV, snake, mesh) as adapters over the 17-byte DeModFrame wire quantum.\n\nADAPTER PATTERN:\n- One adapter message → 1 + ceil(payload_len/4) DeModFrame packets\n- seq = packet_id[15:5] | frag_idx[4:0] (for audio/game); text uses frag_idx[9:0]\n- frag_idx 0 = descriptor: [len, frag_total, codec_id|msg_type_id, flags]\n- Payload ≤ 124 B/block (4 bytes per frame × 31 frames max)\n- L2 framing is codec/message-type-agnostic and byte-certified\n- Adding new codecs/message types never changes the wire vectors\n\nCONVENTIONS:\n- Encode: serialize payload → fragment into ≤124B chunks → wrap each in DeModFrame\n- Decode: collect frames by packet_id → reassemble payload → validate CRC per frame\n- Use CTRL (type 3) frames for audio; DATA (type 0) frames for game/text/SSTV\n- Descriptor flags: bit0 RELIABLE, bit1 ORDERED, bit2 END_TICK (adapter-specific)\n"}, {"role": "user", "content": "Implement the DCF audio adapter in Python.\n\nWire quantum spec (for context):\n# DCF Wire Quantum — Canonical Specification\n\n**Version 1** · DeMoD LLC · This document is normative.\n\nThe DCF wire protocol has exactly **one** wire format: the 17-byte `DeModFrame`\n(version nibble = 1). All other formats (the proto-message UDP header, the C\nSDK's `DCFMessageHeader`) are **adapters** over this quantum and must be\nexplicitly documented as such.\n\n## Wire Layout\n\n```\n Byte Field Width Description\n ──── ────────── ───── ─────────────────────────────────────────\n 0 sync 1 B Fixed 0xD3 (first validity gate)\n 1 flags 1 B [7:4] version (currently 1) | [3:0] type (0–3)\n 2–3 seq 2 B Big-endian u16, rolling sequence counter\n 4–5 src_id 2 B Big-endian u16, source node ID\n 6–7 dst_id 2 B Big-endian u16, destination (0xFFFF = broadcast)\n 8–11 payload 4 B Application data\n 12–14 timestamp 3 B 24-bit µs offset, big-endian, wraps ~16.7 s\n 15–16 crc16 2 B CRC-16/CCITT-FALSE over bytes [0..14]\n```\n\n**Total: 17 bytes = 136 bits.** The free field data is 108 bits (4+16+16+16+32+24).\n\n## Frame Types\n\n| Value | Name | Purpose |\n|-------|--------|-----------------------------|\n| 0 | DATA | Application payload |\n| 1 | ACK | Acknowledgement |\n| 2 | BEACON | Clock sync / broadcast |\n| 3 | CTRL | Control / fragmented audio |\n\n## Validity\n\nA frame is valid iff:\n1. `frame[0] == 0xD3` (sync byte)\n2. `(frame[1] >> 4) == 1` (version nibble)\n3. `CRC-16/CCITT-FALSE(frame[0..14]) == (frame[15] << 8) | frame[16]` (CRC)\n\nFor a random byte stream, the probability of a 17-byte window passing all three\nchecks is 2⁻²⁸ (Theorem 2.1, `wire_quanta_category.md`).\n\n## CRC-16/CCITT-FALSE\n\n- Polynomial: `0x1021`\n- Init: `0xFFFF`\n- RefIn/RefOut: `false`\n- XorOut: `0x0000`\n- Check value: `CRC(\"123456789\") = 0x29B1`\n\n## Anchors\n\n| Quantity | Value | Source |\n|----------|-------|--------|\n| CRC(\"123456789\") | `0x29B1` |...\n\nAdapter specification:\n# DCF-Audio — Collaborative Audio over the DeModFrame Wire\n\n**Version 1** · DeMoD LLC · This document is normative. Companion to\n[`WIRE_QUANTUM_SPEC.md`](WIRE_QUANTUM_SPEC.md).\n\nDCF-Audio carries real-time, collaborative audio (jamming, talkback) over the\nPunctim mesh. It introduces **no new wire format**: a 20 ms codec block is an\n**adapter** over the 17-byte `DeModFrame` quantum, serialised into a short burst of\nordinary `CTRL` (type 3) frames. The framing is **codec-agnostic and\nbyte-deterministic across C, Rust, and Python**, and is pinned by a finite\ncertificate exactly like the wire quantum itself.\n\n## Layered model\n\n| Layer | What | Certified? |\n|-------|------|-----------|\n| **L1** codec registry | Pluggable codecs keyed by `codec_id` (Opus / PCM-diag / Faust-PM). Output is opaque to L2. | PCM-diag bytes & PM param layout: **yes**. Opus & PM *audio*: no (float/version-dependent). |\n| **L2** framing | `packetize` / reassemble: codec bytes ⇄ `DeModFrame` CTRL frames. | **Yes** — `audio_vectors.json` |\n| **L3** jitter buffer + PLC | Reorder/dejitter on `packet_id`/`timestamp_us`; conceal lost packets. | Runtime (unit-tested) |\n| **L4** transport + demo | Send frames over the mesh; 2-peer loopback jam. | Demo |\n\nThe key invariant: **`codec_id` lives in the L2 descriptor, so adding codecs never\nchanges the L2 vectors.**\n\n## L2 framing (normative)\n\nOne 20 ms block → one *audio packet* → `1 + frag_total` frames. Every frame is a\nfully valid `DeModFrame` (version = 1, type = `CTRL` = 3) and still passes the\n246-vector wire certificate.\n\n```\n seq (u16) = packet_id[15:5] (11 bits) | frag_idx[4:0] (5 bits)\n frag_idx == 0 descriptor : payload = [payload_len, frag_total, codec_id, flags]\n frag_idx 1..N data : payload = bytes[(k-1)*4 .. +4] (last frame zero-padded)\n frag_total = ceil(payload_len / 4)\n timestamp_us = 24-bit block capture time, identical across a packet's frames\n src_id/dst_id = node ids (0xFFFF = broadcast for a jam)\n```\n\nBounds (from the 5-bit fragment field):\n\n| Quantity | Value |\n|----------|-------|\n| Max data fragments | 31 |\n| **Max codec payload** | **124 bytes / 20 ms** (≈ 49.6 kbps) |\n| Max `packet_id` | 2047 (wraps ≈ 41 s at 20 ms) |\n| Frames per packet | `1 + ceil(payload_len/4)` (1–32) |\n\nDescriptor flags: bit 0 = end-of-talkspurt, bit 1 = PM voice profile.\n\n**Reassembly.** Buffer by modular `packet_id`; gather the descriptor and all data\nfragments; concatenate and truncate to `payload_len`; emit\n`(packet_id, timestamp_us, codec_id, flags, payload)`. Duplicates are ignored. Any\nmissing fragment ⇒ the packet is **lost** ⇒ L3 runs the codec's PLC.\n\n## Codec registry\n\n| id | Name | Profile (20 ms blocks) | ≤124 B? | Byte-deterministic? |\n|----|------|------------------------|---------|---------------------|\n| 0 | Opus | 48 kHz mono, ~24 kbps (≈60 B) | yes | No (libopus-version dependent) |\n| 1 | PCM-diag | 6 kHz, 8-bit linear, mono = 120 B | yes | **Yes** (round-trip pinned) |\n| 2 | Faust-PM | phase-mod synthesis, **8-byte param block** | yes | Param layout: **yes**; synthesis audio: no |\n| 3 | *reserved* | (declined low-bitrate speech codec) | — | — |\n\nBecause a full-band PCM block cannot fit in 124 B, the diagnostic PCM codec runs at\n6 kHz / 8-bit (one byte per sample, 120 B). Opus fragments ≈ 16 frames; PM is just\n3 frames, so PM and Opus tolerate frame loss far better than PCM-diag.\n\n### Faust phase-mod codec (id 2) — 8-byte parameter block\n\nThe wire carries compact synthesis parameters; the decoder resynthesises timbre via\na ratio-locked phase-modulation oscillator (`codec/faust/dcf_pm_codec.dsp`, committed\nas Faust→C in `dcf_pm_codec.gen.c`). The **layout is certified**; the synthesised\naudio is not.\n\n```\n byte: 0 1 2 3 4 5 6 7\n field: f0_hi f0_lo amp mod_index mod_ratio bright env flags\n u16 (cents-mapped fundamental) u8 each (carrier:mod is u8 fixed-point)\n```\n\n## Certification\n\nMirrors the wire quantum: a Python reference emits golden vectors; C and Rust diff\nagainst them byte-for-byte.\n\n```sh\n# regenerate + verify the laws, then diff against the committed vectors\npython3 python/MCP/gen_audio_vectors.py /tmp/audio_vectors.json\ndiff /tmp/audio_vectors.json Documentation/audio_vectors.json\ndiff /tmp/pm_param_vectors.json Documentation/pm_param_vectors.json\ndiff /tmp/audio_vectors.gen.h codec/audio_vectors.gen.h\n\n# C (L2-only; no libopus / Faust needed)\ngcc -std=c11 -Wall -Wextra -I codec C_SDK/tests/test_audio_certify.c -lm -o /tmp/ac && /tmp/ac\n\n# Rust\ncd codec && cargo test --test certify_audio\n```\n\n| Artifact | Role |\n|----------|------|\n| `Documentation/audio_vectors.json` (= `python/MCP/`) | framing + reassembly + PCM round-trip vectors |\n| `Documentation/pm_param_vectors.json` (= `python/MCP/`) | PM 8-byte param round-trip |\n| `codec/audio_vectors.gen.h` | same vectors as a C header (dependency-free C test) |\n| `python/MCP/audiolab_core.py` | Python L2 reference (imports the certified wire codec) |\n| `python/MCP/gen_audio_vectors.py` | executable laws + vector generator |\n\n## Reference implementations\n\n| Language | File | Entry points |\n|----------|------|--------------|\n| C | `codec/demod_audio.h` | `dcf_audio_packetize`, `dcf_audio_reasm_*`, `dcf_codec_get`, `dcf_pm_pack/unpack` |\n| Rust | `codec/src/audio.rs` | `packetize`, `AudioReassembler`, `codec_for`, `pm_pack/unpack` |\n| Python | `python/MCP/audiolab_core.py` | `packetize`, `AudioReassembler`, `pm_pack/unpack` |\n| Lua | `lua/dcf_audio.lua` | self-certifying L2 codec (LGPL-3.0-only, dual-licensed — see `lua/LICENSING.md`) |\n\n## Latency budget (informative)\n\n~5 ms capture · 5–10 ms encode (Opus/PM) · 20 ms one-way network · 20–40 ms jitter\nbuffer (1–2 packets) · 5–10 ms decode · 5 ms playout → **< 100 ms** one-way for\ncollaborative jamming. The jitter window and PLC aggressiveness are tunable.\n\n## Transport & demo\n\n- **Rust SDK:** `DcfNode::send_audio_dcf(codec_id, encoded, packet_id, ts_us)`\n packetizes a block and ships each 17-byte frame as an unreliable `AUDIO` message;\n the receiver feeds payloads to `reassemble_audio_payload`.\n- **2-peer jam:** `cd codec && cargo run --example jam_loopback -- --codec pcm`\n (add `--loss 0.05` to exercise PLC; `--features opus|pm` + `--codec opus|pm`).\n\n## TERMINUS integration (informative)\n\n`unified-UI` (DeMoD TERMINUS) is a separate repo. The intended hookup feeds the\norchestrator's real-time audio through L1–L4 and surfaces peers via the existing\ndevice-bridge `/v1/mesh` endpoint — a future `demod-jam` app, not built here.\n\n\nGolden vectors (sample):\n{\n \"encode_basis\": [],\n \"syndrome_basis\": []\n}\n\nProvide the complete implementation with encode/decode functions."}, {"role": "assistant", "content": "# SPDX-License-Identifier: LGPL-3.0-only\n\"\"\"DCF AudioLab core — reference for the DCF-Audio L2 framing (the certified layer).\n\nAudio is an *adapter* over the 17-byte DeModFrame wire quantum: a 20 ms codec block\nis serialised into 1 + frag_total ordinary CTRL frames. This layer is codec-agnostic\nand byte-deterministic across C / Rust / Python — `codec_id` lives only in the\ndescriptor frame, so adding codecs never changes these vectors.\n\nL2 framing (all frames version=1, type=CTRL(3), big-endian; see WIRE_QUANTUM_SPEC.md):\n seq (u16) = packet_id[15:5] (11 bits) | frag_idx[4:0] (5 bits)\n frag_idx == 0 descriptor : payload = [payload_len, frag_total, codec_id, flags]\n frag_idx 1..N data : payload = bytes[(k-1)*4 .. +4], last frame zero-padded\n timestamp_us = 24-bit block capture time, identical across a packet's frames\n frag_total = ceil(payload_len / 4) (<= 31 => payload_len <= 124 bytes / 20 ms)\n\nThis module imports the certified wire codec from wirelab_core so the emitted bytes are\nprovably the same DeModFrame the 246-vector wire certificate already pins.\n\"\"\"\nimport os\nimport sys\n\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\nfrom wirelab_core import encode, decode # certified DeModFrame codec\n\n# ── L2 framing constants ──────────────────────────────────────────────────────\nFCTRL = 3 # frame type carrying fragmented audio\nFRAG_BITS = 5 # low bits of seq hold the fragment index\nFRAG_MASK = (1 << FRAG_BITS) - 1 # 0x1F\nMAX_FRAGS = FRAG_MASK # 31 data fragments\nMAX_PAYLOAD = MAX_FRAGS * 4 # 124 bytes / 20 ms block\nMAX_PACKET_ID = (1 << (16 - FRAG_BITS)) - 1 # 2047\n\n# ── Codec registry ids (profiles live in DCF_AUDIO_SPEC.md) ───────────────────\nCODEC_OPUS = 0 # 48 kHz mono ~24 kbps (not byte-deterministic)\nCODEC_PCM_DIAG = 1 # 6 kHz 8-bit mono, 120 B/block (byte-deterministic)\nCODEC_FAUST_PM = 2 # phase-mod synthesis, 8-byte param block\n# id 3 reserved (declined low-bitrate speech codec)\n\n# Descriptor flags\nFLAG_END_TALKSPURT = 0x01\nFLAG_PM_VOICE = 0x02\n\n\ndef _ceil_div(a, b):\n return (a + b - 1) // b\n\n\ndef packetize(codec_id, payload, packet_id, ts_us, src, dst, flags=0):\n \"\"\"Serialise one codec block (`payload`, 0..124 bytes) into a list of 17-byte\n DeModFrame CTRL frames. Returns [descriptor_frame, data_frame, ...].\"\"\"\n if not (0 <= codec_id < 256):\n raise ValueError(\"codec_id must be u8\")\n if not (0 <= flags < 256):\n raise ValueError(\"flags must be u8\")\n if not (0 <= packet_id <= MAX_PACKET_ID):\n raise ValueError(f\"packet_id must be 0..{MAX_PACKET_ID}\")\n if len(payload) > MAX_PAYLOAD:\n raise ValueError(f\"payload {len(payload)}B exceeds {MAX_PAYLOAD}B/block cap\")\n\n frag_total = _ceil_div(len(payload), 4) # data fragments after descriptor\n frames = []\n\n # frag_idx 0 — descriptor\n desc_seq = (packet_id << FRAG_BITS) | 0\n desc_payload = bytes([len(payload), frag_total, codec_id, flags])\n frames.append(encode(FCTRL, desc_seq, src, dst, desc_payload, ts_us))\n\n # frag_idx 1..frag_total — data, last chunk zero-padded to 4 bytes\n for k in range(1, frag_total + 1):\n chunk = payload[(k - 1) * 4:(k - 1) * 4 + 4]\n chunk = chunk + bytes(4 - len(chunk))\n seq = (packet_id << FRAG_BITS) | k\n frames.append(encode(FCTRL, seq, src, dst, chunk, ts_us))\n\n return frames\n\n\nclass AudioReassembler:\n \"\"\"Stateful reassembler. push() emits a completed packet as soon as its\n descriptor and every data fragment have arrived; duplicates are ignored;\n finalize() reports any packet still incomplete as lost.\n\n Emitted packet event: (\"packet\", packet_id, ts_us, codec_id, payload_bytes, flags)\n Lost event: (\"lost\", packet_id)\n \"\"\"\n\n def __init__(self):\n self._pkts = {} # packet_id -> dict(desc, ts, flags, frags{idx:4B})\n\n def push(self, frame):\n try:\n d = decode(frame)\n except ValueError:\n return []\n if d[\"frame_type\"] != FCTRL:\n return []\n seq = d[\"seq\"]\n packet_id = seq >> FRAG_BITS\n frag_idx = seq & FRAG_MASK\n body = bytes.fromhex(d[\"payload\"])\n\n entry = self._pkts.setdefault(packet_id, {\"desc\": None, \"ts\": d[\"ts_us\"],\n \"flags\": 0, \"frags\": {}})\n entry[\"ts\"] = d[\"ts_us\"]\n if frag_idx == 0:\n if entry[\"desc\"] is None:\n entry[\"desc\"] = {\"payload_len\": body[0], \"frag_total\": body[1],\n \"codec_id\": body[2], \"flags\": body[3]}\n elif frag_idx not in entry[\"frags\"]:\n entry[\"frags\"][frag_idx] = body\n\n return self._try_emit(packet_id)\n\n def _try_emit(self, packet_id):\n entry = self._pkts.get(packet_id)\n if entry is None or entry[\"desc\"] is None:\n return []\n desc = entry[\"desc\"]\n need = set(range(1, desc[\"frag_total\"] + 1))\n if not need.issubset(entry[\"frags\"].keys()):\n return []\n raw = b\"\".join(entry[\"frags\"][k] for k in range(1, desc[\"frag_total\"] + 1))\n payload = raw[:desc[\"payload_len\"]]\n del self._pkts[packet_id]\n return [(\"packet\", packet_id, entry[\"ts\"], desc[\"codec_id\"], payload, desc[\"flags\"])]\n\n def finalize(self):\n events = [(\"lost\", pid) for pid in sorted(self._pkts.keys())]\n self._pkts.clear()\n return events\n\n\n# ── PCM-diagnostic codec (id 1): 6 kHz, 8-bit linear, mono, 120 samples/block ──\nPCM_DIAG_RATE = 6000\nPCM_DIAG_BLOCK = 120 # samples == bytes per 20 ms block\n\n\ndef pcm_diag_encode(samples):\n \"\"\"float [-1,1] -> unsigned 8-bit (mid 128). Byte-deterministic.\"\"\"\n out = bytearray(len(samples))\n for i, s in enumerate(samples):\n v = int(round(s * 128.0)) + 128\n out[i] = 0 if v < 0 else 255 if v > 255 else v\n return bytes(out)\n\n\ndef pcm_diag_decode(data):\n \"\"\"unsigned 8-bit -> float [-1,1]. decode∘encode is byte-lossless.\"\"\"\n return [(b - 128) / 128.0 for b in data]\n\n\n# ── Faust phase-mod codec (id 2): 8-byte synthesis parameter block ────────────\n# Layout: [f0_hi, f0_lo, amp, mod_index, mod_ratio, bright, env, flags]\nPM_PARAM_FIELDS = (\"f0\", \"amp\", \"mod_index\", \"mod_ratio\", \"bright\", \"env\", \"flags\")\n\n\ndef pm_pack(params):\n \"\"\"dict -> 8 bytes. f0 is a u16 (quantised fundamental, cents-mapped);\n the rest are u8. Byte-deterministic — this layout is certified.\"\"\"\n f0 = int(params[\"f0\"]) & 0xFFFF\n b = bytes([\n (f0 >> 8) & 0xFF, f0 & 0xFF,\n int(params[\"amp\"]) & 0xFF,\n int(params[\"mod_index\"]) & 0xFF,\n int(params[\"mod_ratio\"]) & 0xFF,\n int(params[\"bright\"]) & 0xFF,\n int(params[\"env\"]) & 0xFF,\n int(params[\"flags\"]) & 0xFF,\n ])\n return b\n\n\ndef pm_unpack(data):\n \"\"\"8 bytes -> dict. unpack∘pack = id.\"\"\"\n if len(data) != 8:\n raise ValueError(\"PM param block must be exactly 8 bytes\")\n return {\n \"f0\": (data[0] << 8) | data[1],\n \"amp\": data[2],\n \"mod_index\": data[3],\n \"mod_ratio\": data[4],\n \"bright\": data[5],\n \"env\": data[6],\n \"flags\": data[7],\n }\n"}]}
{"messages": [{"role": "system", "content": "You are an expert DCF adapter implementer. You build protocols (audio, game, text, SSTV, snake, mesh) as adapters over the 17-byte DeModFrame wire quantum.\n\nADAPTER PATTERN:\n- One adapter message → 1 + ceil(payload_len/4) DeModFrame packets\n- seq = packet_id[15:5] | frag_idx[4:0] (for audio/game); text uses frag_idx[9:0]\n- frag_idx 0 = descriptor: [len, frag_total, codec_id|msg_type_id, flags]\n- Payload ≤ 124 B/block (4 bytes per frame × 31 frames max)\n- L2 framing is codec/message-type-agnostic and byte-certified\n- Adding new codecs/message types never changes the wire vectors\n\nCONVENTIONS:\n- Encode: serialize payload → fragment into ≤124B chunks → wrap each in DeModFrame\n- Decode: collect frames by packet_id → reassemble payload → validate CRC per frame\n- Use CTRL (type 3) frames for audio; DATA (type 0) frames for game/text/SSTV\n- Descriptor flags: bit0 RELIABLE, bit1 ORDERED, bit2 END_TICK (adapter-specific)\n"}, {"role": "user", "content": "Implement the DCF audio adapter in Lua.\n\nWire quantum spec (for context):\n# DCF Wire Quantum — Canonical Specification\n\n**Version 1** · DeMoD LLC · This document is normative.\n\nThe DCF wire protocol has exactly **one** wire format: the 17-byte `DeModFrame`\n(version nibble = 1). All other formats (the proto-message UDP header, the C\nSDK's `DCFMessageHeader`) are **adapters** over this quantum and must be\nexplicitly documented as such.\n\n## Wire Layout\n\n```\n Byte Field Width Description\n ──── ────────── ───── ─────────────────────────────────────────\n 0 sync 1 B Fixed 0xD3 (first validity gate)\n 1 flags 1 B [7:4] version (currently 1) | [3:0] type (0–3)\n 2–3 seq 2 B Big-endian u16, rolling sequence counter\n 4–5 src_id 2 B Big-endian u16, source node ID\n 6–7 dst_id 2 B Big-endian u16, destination (0xFFFF = broadcast)\n 8–11 payload 4 B Application data\n 12–14 timestamp 3 B 24-bit µs offset, big-endian, wraps ~16.7 s\n 15–16 crc16 2 B CRC-16/CCITT-FALSE over bytes [0..14]\n```\n\n**Total: 17 bytes = 136 bits.** The free field data is 108 bits (4+16+16+16+32+24).\n\n## Frame Types\n\n| Value | Name | Purpose |\n|-------|--------|-----------------------------|\n| 0 | DATA | Application payload |\n| 1 | ACK | Acknowledgement |\n| 2 | BEACON | Clock sync / broadcast |\n| 3 | CTRL | Control / fragmented audio |\n\n## Validity\n\nA frame is valid iff:\n1. `frame[0] == 0xD3` (sync byte)\n2. `(frame[1] >> 4) == 1` (version nibble)\n3. `CRC-16/CCITT-FALSE(frame[0..14]) == (frame[15] << 8) | frame[16]` (CRC)\n\nFor a random byte stream, the probability of a 17-byte window passing all three\nchecks is 2⁻²⁸ (Theorem 2.1, `wire_quanta_category.md`).\n\n## CRC-16/CCITT-FALSE\n\n- Polynomial: `0x1021`\n- Init: `0xFFFF`\n- RefIn/RefOut: `false`\n- XorOut: `0x0000`\n- Check value: `CRC(\"123456789\") = 0x29B1`\n\n## Anchors\n\n| Quantity | Value | Source |\n|----------|-------|--------|\n| CRC(\"123456789\") | `0x29B1` |...\n\nAdapter specification:\n# DCF-Audio — Collaborative Audio over the DeModFrame Wire\n\n**Version 1** · DeMoD LLC · This document is normative. Companion to\n[`WIRE_QUANTUM_SPEC.md`](WIRE_QUANTUM_SPEC.md).\n\nDCF-Audio carries real-time, collaborative audio (jamming, talkback) over the\nPunctim mesh. It introduces **no new wire format**: a 20 ms codec block is an\n**adapter** over the 17-byte `DeModFrame` quantum, serialised into a short burst of\nordinary `CTRL` (type 3) frames. The framing is **codec-agnostic and\nbyte-deterministic across C, Rust, and Python**, and is pinned by a finite\ncertificate exactly like the wire quantum itself.\n\n## Layered model\n\n| Layer | What | Certified? |\n|-------|------|-----------|\n| **L1** codec registry | Pluggable codecs keyed by `codec_id` (Opus / PCM-diag / Faust-PM). Output is opaque to L2. | PCM-diag bytes & PM param layout: **yes**. Opus & PM *audio*: no (float/version-dependent). |\n| **L2** framing | `packetize` / reassemble: codec bytes ⇄ `DeModFrame` CTRL frames. | **Yes** — `audio_vectors.json` |\n| **L3** jitter buffer + PLC | Reorder/dejitter on `packet_id`/`timestamp_us`; conceal lost packets. | Runtime (unit-tested) |\n| **L4** transport + demo | Send frames over the mesh; 2-peer loopback jam. | Demo |\n\nThe key invariant: **`codec_id` lives in the L2 descriptor, so adding codecs never\nchanges the L2 vectors.**\n\n## L2 framing (normative)\n\nOne 20 ms block → one *audio packet* → `1 + frag_total` frames. Every frame is a\nfully valid `DeModFrame` (version = 1, type = `CTRL` = 3) and still passes the\n246-vector wire certificate.\n\n```\n seq (u16) = packet_id[15:5] (11 bits) | frag_idx[4:0] (5 bits)\n frag_idx == 0 descriptor : payload = [payload_len, frag_total, codec_id, flags]\n frag_idx 1..N data : payload = bytes[(k-1)*4 .. +4] (last frame zero-padded)\n frag_total = ceil(payload_len / 4)\n timestamp_us = 24-bit block capture time, identical across a packet's frames\n src_id/dst_id = node ids (0xFFFF = broadcast for a jam)\n```\n\nBounds (from the 5-bit fragment field):\n\n| Quantity | Value |\n|----------|-------|\n| Max data fragments | 31 |\n| **Max codec payload** | **124 bytes / 20 ms** (≈ 49.6 kbps) |\n| Max `packet_id` | 2047 (wraps ≈ 41 s at 20 ms) |\n| Frames per packet | `1 + ceil(payload_len/4)` (1–32) |\n\nDescriptor flags: bit 0 = end-of-talkspurt, bit 1 = PM voice profile.\n\n**Reassembly.** Buffer by modular `packet_id`; gather the descriptor and all data\nfragments; concatenate and truncate to `payload_len`; emit\n`(packet_id, timestamp_us, codec_id, flags, payload)`. Duplicates are ignored. Any\nmissing fragment ⇒ the packet is **lost** ⇒ L3 runs the codec's PLC.\n\n## Codec registry\n\n| id | Name | Profile (20 ms blocks) | ≤124 B? | Byte-deterministic? |\n|----|------|------------------------|---------|---------------------|\n| 0 | Opus | 48 kHz mono, ~24 kbps (≈60 B) | yes | No (libopus-version dependent) |\n| 1 | PCM-diag | 6 kHz, 8-bit linear, mono = 120 B | yes | **Yes** (round-trip pinned) |\n| 2 | Faust-PM | phase-mod synthesis, **8-byte param block** | yes | Param layout: **yes**; synthesis audio: no |\n| 3 | *reserved* | (declined low-bitrate speech codec) | — | — |\n\nBecause a full-band PCM block cannot fit in 124 B, the diagnostic PCM codec runs at\n6 kHz / 8-bit (one byte per sample, 120 B). Opus fragments ≈ 16 frames; PM is just\n3 frames, so PM and Opus tolerate frame loss far better than PCM-diag.\n\n### Faust phase-mod codec (id 2) — 8-byte parameter block\n\nThe wire carries compact synthesis parameters; the decoder resynthesises timbre via\na ratio-locked phase-modulation oscillator (`codec/faust/dcf_pm_codec.dsp`, committed\nas Faust→C in `dcf_pm_codec.gen.c`). The **layout is certified**; the synthesised\naudio is not.\n\n```\n byte: 0 1 2 3 4 5 6 7\n field: f0_hi f0_lo amp mod_index mod_ratio bright env flags\n u16 (cents-mapped fundamental) u8 each (carrier:mod is u8 fixed-point)\n```\n\n## Certification\n\nMirrors the wire quantum: a Python reference emits golden vectors; C and Rust diff\nagainst them byte-for-byte.\n\n```sh\n# regenerate + verify the laws, then diff against the committed vectors\npython3 python/MCP/gen_audio_vectors.py /tmp/audio_vectors.json\ndiff /tmp/audio_vectors.json Documentation/audio_vectors.json\ndiff /tmp/pm_param_vectors.json Documentation/pm_param_vectors.json\ndiff /tmp/audio_vectors.gen.h codec/audio_vectors.gen.h\n\n# C (L2-only; no libopus / Faust needed)\ngcc -std=c11 -Wall -Wextra -I codec C_SDK/tests/test_audio_certify.c -lm -o /tmp/ac && /tmp/ac\n\n# Rust\ncd codec && cargo test --test certify_audio\n```\n\n| Artifact | Role |\n|----------|------|\n| `Documentation/audio_vectors.json` (= `python/MCP/`) | framing + reassembly + PCM round-trip vectors |\n| `Documentation/pm_param_vectors.json` (= `python/MCP/`) | PM 8-byte param round-trip |\n| `codec/audio_vectors.gen.h` | same vectors as a C header (dependency-free C test) |\n| `python/MCP/audiolab_core.py` | Python L2 reference (imports the certified wire codec) |\n| `python/MCP/gen_audio_vectors.py` | executable laws + vector generator |\n\n## Reference implementations\n\n| Language | File | Entry points |\n|----------|------|--------------|\n| C | `codec/demod_audio.h` | `dcf_audio_packetize`, `dcf_audio_reasm_*`, `dcf_codec_get`, `dcf_pm_pack/unpack` |\n| Rust | `codec/src/audio.rs` | `packetize`, `AudioReassembler`, `codec_for`, `pm_pack/unpack` |\n| Python | `python/MCP/audiolab_core.py` | `packetize`, `AudioReassembler`, `pm_pack/unpack` |\n| Lua | `lua/dcf_audio.lua` | self-certifying L2 codec (LGPL-3.0-only, dual-licensed — see `lua/LICENSING.md`) |\n\n## Latency budget (informative)\n\n~5 ms capture · 5–10 ms encode (Opus/PM) · 20 ms one-way network · 20–40 ms jitter\nbuffer (1–2 packets) · 5–10 ms decode · 5 ms playout → **< 100 ms** one-way for\ncollaborative jamming. The jitter window and PLC aggressiveness are tunable.\n\n## Transport & demo\n\n- **Rust SDK:** `DcfNode::send_audio_dcf(codec_id, encoded, packet_id, ts_us)`\n packetizes a block and ships each 17-byte frame as an unreliable `AUDIO` message;\n the receiver feeds payloads to `reassemble_audio_payload`.\n- **2-peer jam:** `cd codec && cargo run --example jam_loopback -- --codec pcm`\n (add `--loss 0.05` to exercise PLC; `--features opus|pm` + `--codec opus|pm`).\n\n## TERMINUS integration (informative)\n\n`unified-UI` (DeMoD TERMINUS) is a separate repo. The intended hookup feeds the\norchestrator's real-time audio through L1–L4 and surfaces peers via the existing\ndevice-bridge `/v1/mesh` endpoint — a future `demod-jam` app, not built here.\n\n\nGolden vectors (sample):\n{\n \"encode_basis\": [],\n \"syndrome_basis\": []\n}\n\nProvide the complete implementation with encode/decode functions."}, {"role": "assistant", "content": "-- SPDX-License-Identifier: LGPL-3.0-only\n-- Copyright (c) 2026 DeMoD LLC. A commercial license is available on request — see LICENSING.md.\n-- ============================================================================\n-- dcf_audio.lua — DCF-Audio L2 framing + frequency rendezvous (pure Lua).\n--\n-- The canonical, intentionally-open Lua port of the CERTIFIED DCF-Audio codec\n-- from this repo (python/MCP/audiolab_core.py + codec/{demod_audio.h,src/audio.rs});\n-- the 17-byte DeModFrame codec is byte-identical to GUI/wirelab.lua / wirelab_core.py\n-- / the C and Rust references. Audio is an *adapter* over the wire quantum: a 20 ms\n-- codec block is serialised into 1 + frag_total ordinary CTRL frames.\n--\n-- L2 layout (all frames version=1, type=CTRL(3); big-endian):\n-- seq (u16) = packet_id[15:5] (11 bits) | frag_idx[4:0] (5 bits)\n-- frag_idx 0 descriptor : payload = [payload_len, frag_total, codec_id, flags]\n-- frag_idx k data : payload = bytes[(k-1)*4 .. +4] (last frame zero-padded)\n-- frag_total = ceil(payload_len/4) (<= 31 => payload_len <= 124 bytes / 20 ms)\n--\n-- Frequency rendezvous (handshakeless): peers pre-agree a CHANNEL (the frame dst\n-- field). Tune to a numeric channel or derive one from a shared passphrase via the\n-- certified crc16. A node accepts a frame iff dst == its channel, or dst == 0xFFFF\n-- (broadcast). No wire change — ordinary, certified DeModFrames.\n--\n-- Self-certifies on load (M.CERTIFIED). Requires Lua 5.3+ (native bitwise ops),\n-- which the demod-ui host provides.\n-- ============================================================================\n\nlocal M = {}\n\n-- ── DeModFrame wire codec (byte-identical to GUI/wirelab.lua — certified) ───\nlocal SYNC, VERSION, FRAME_LEN = 0xD3, 1, 17\n\nlocal function crc16(bytes, n)\n local crc = 0xFFFF\n for i = 1, n do\n crc = (crc ~ ((bytes[i] & 0xFF) << 8)) & 0xFFFF\n for _ = 1, 8 do\n if (crc & 0x8000) ~= 0 then crc = ((crc << 1) ~ 0x1021) & 0xFFFF\n else crc = (crc << 1) & 0xFFFF end\n end\n end\n return crc\nend\n\nlocal function encode(ftype, seq, src, dst, payload, ts)\n local w = {}\n w[1] = SYNC\n w[2] = ((VERSION & 0xF) << 4) | (ftype & 0xF)\n w[3] = (seq >> 8) & 0xFF; w[4] = seq & 0xFF\n w[5] = (src >> 8) & 0xFF; w[6] = src & 0xFF\n w[7] = (dst >> 8) & 0xFF; w[8] = dst & 0xFF\n for i = 1, 4 do w[8 + i] = (payload[i] or 0) & 0xFF end\n w[13] = (ts >> 16) & 0xFF; w[14] = (ts >> 8) & 0xFF; w[15] = ts & 0xFF\n local c = crc16(w, 15)\n w[16] = (c >> 8) & 0xFF; w[17] = c & 0xFF\n return w\nend\n\nlocal function decode(w)\n if #w ~= FRAME_LEN then return nil, \"length\" end\n if w[1] ~= SYNC then return nil, \"bad sync\" end\n if (w[2] >> 4) ~= VERSION then return nil, \"bad version\" end\n if crc16(w, 15) ~= ((w[16] << 8) | w[17]) then return nil, \"crc\" end\n return {\n ftype = w[2] & 0xF,\n seq = (w[3] << 8) | w[4], src = (w[5] << 8) | w[6], dst = (w[7] << 8) | w[8],\n payload = { w[9], w[10], w[11], w[12] },\n ts = (w[13] << 16) | (w[14] << 8) | w[15],\n }\nend\n\nlocal function frame_to_hex(w)\n local t = {}\n for i = 1, #w do t[i] = (\"%02X\"):format(w[i]) end\n return table.concat(t)\nend\n\nM.crc16, M.encode, M.decode, M.frame_to_hex = crc16, encode, decode, frame_to_hex\n\n-- ── L2 constants ────────────────────────────────────────────────────────────\nM.FCTRL = 3\nM.FRAG_BITS = 5\nM.FRAG_MASK = 0x1F\nM.MAX_FRAGS = 31\nM.MAX_PAYLOAD = 124 -- bytes / 20 ms block\nM.MAX_PACKETID = 2047\nM.BROADCAST = 0xFFFF\nM.CODEC_OPUS, M.CODEC_PCM_DIAG, M.CODEC_FAUST_PM = 0, 1, 2 -- id 3 reserved\nM.FLAG_END_TALKSPURT, M.FLAG_PM_VOICE = 0x01, 0x02\n\n-- ── Frequency rendezvous (channel = frame dst) ──────────────────────────────\n-- Derive a u16 channel from a shared passphrase using the certified crc16, so two\n-- peers who agree on a word land on the same handshakeless channel. crc16 of the\n-- empty string is 0xFFFF (== broadcast) — callers should treat \"\" as \"no passphrase\".\nfunction M.channel_from_passphrase(s)\n local b = {}\n for i = 1, #s do b[i] = string.byte(s, i) end\n return crc16(b, #s)\nend\n\n-- A node tuned to my_channel accepts a frame addressed to its channel or broadcast.\nfunction M.accepts(frame_dst, my_channel)\n return frame_dst == my_channel or frame_dst == M.BROADCAST\nend\n\n-- ── L2: packetize ───────────────────────────────────────────────────────────\n-- payload: 1-indexed byte array (0..124). dst is the rendezvous channel (or\n-- BROADCAST). Returns { frame, ... }, descriptor first then data fragments.\nfunction M.packetize(codec_id, payload, packet_id, ts_us, src, dst, flags)\n local n = #payload\n assert(n <= M.MAX_PAYLOAD, \"payload exceeds 124 B/block\")\n assert(packet_id <= M.MAX_PACKETID, \"packet_id exceeds 11 bits\")\n flags = flags or 0\n local frag_total = (n + 3) // 4\n local frames = {}\n\n local desc_seq = (packet_id << M.FRAG_BITS)\n frames[1] = encode(M.FCTRL, desc_seq, src, dst, { n, frag_total, codec_id, flags }, ts_us)\n\n for k = 1, frag_total do\n local off = (k - 1) * 4\n local chunk = { payload[off + 1] or 0, payload[off + 2] or 0,\n payload[off + 3] or 0, payload[off + 4] or 0 }\n local seq = (packet_id << M.FRAG_BITS) | k\n frames[k + 1] = encode(M.FCTRL, seq, src, dst, chunk, ts_us)\n end\n return frames\nend\n\n-- ── L2: reassembler (channel-aware) ─────────────────────────────────────────\nlocal Reassembler = {}\nReassembler.__index = Reassembler\n\n-- channel: if given, frames not addressed to this channel (and not broadcast) are\n-- dropped (the handshakeless rendezvous filter). Pass nil to accept everything.\nfunction Reassembler.new(channel)\n return setmetatable({ slots = {}, channel = channel, rejected = 0 }, Reassembler)\nend\n\nfunction Reassembler:push(frame)\n local d = decode(frame)\n if not d or d.ftype ~= M.FCTRL then return nil end\n if self.channel ~= nil and not M.accepts(d.dst, self.channel) then\n self.rejected = self.rejected + 1\n return nil\n end\n local pid = d.seq >> M.FRAG_BITS\n local fidx = d.seq & M.FRAG_MASK\n local s = self.slots[pid]\n if not s then s = { desc = nil, ts = d.ts, frags = {} }; self.slots[pid] = s end\n s.ts = d.ts\n if fidx == 0 then\n if not s.desc then\n s.desc = { len = d.payload[1], total = d.payload[2], codec = d.payload[3], flags = d.payload[4] }\n end\n elseif not s.frags[fidx] then\n s.frags[fidx] = d.payload\n end\n if not s.desc then return nil end\n for k = 1, s.desc.total do if not s.frags[k] then return nil end end\n local bytes = {}\n for k = 1, s.desc.total do\n local p = s.frags[k]\n bytes[#bytes + 1] = p[1]; bytes[#bytes + 1] = p[2]\n bytes[#bytes + 1] = p[3]; bytes[#bytes + 1] = p[4]\n end\n for i = #bytes, s.desc.len + 1, -1 do bytes[i] = nil end\n self.slots[pid] = nil\n return { packet_id = pid, ts_us = s.ts, codec_id = s.desc.codec, flags = s.desc.flags, payload = bytes }\nend\n\nfunction Reassembler:finalize()\n local lost = {}\n for pid in pairs(self.slots) do lost[#lost + 1] = pid end\n table.sort(lost)\n self.slots = {}\n return lost\nend\n\nM.Reassembler = Reassembler\n\n-- ── L1: PCM-diagnostic codec (id 1) — 6 kHz 8-bit mono, 120 B/block ─────────\nM.PCM_DIAG_RATE, M.PCM_DIAG_BLOCK = 6000, 120\n\nfunction M.pcm_diag_encode(samples)\n local out = {}\n for i = 1, #samples do\n local v = math.floor(samples[i] * 128.0 + 0.5) + 128\n out[i] = (v < 0 and 0) or (v > 255 and 255) or v\n end\n return out\nend\n\nfunction M.pcm_diag_decode(bytes)\n local out = {}\n for i = 1, #bytes do out[i] = (bytes[i] - 128) / 128.0 end\n return out\nend\n\n-- ── L1: Faust phase-mod (id 2) — 8-byte parameter block (certified layout) ──\nfunction M.pm_pack(p)\n return {\n (p.f0 >> 8) & 0xFF, p.f0 & 0xFF,\n p.amp & 0xFF, p.mod_index & 0xFF, p.mod_ratio & 0xFF,\n p.bright & 0xFF, p.env & 0xFF, p.flags & 0xFF,\n }\nend\n\nfunction M.pm_unpack(b)\n return {\n f0 = (b[1] << 8) | b[2],\n amp = b[3], mod_index = b[4], mod_ratio = b[5],\n bright = b[6], env = b[7], flags = b[8],\n }\nend\n\n-- ── Self-certification (matches Documentation/audio_vectors.json anchors) ────\nlocal function selfcert()\n local a = {}\n for i = 1, 9 do a[i] = string.byte(\"123456789\", i) end\n if crc16(a, 9) ~= 0x29B1 then return false end\n -- the channel hash reuses the certified crc16, so the same anchor pins it:\n if M.channel_from_passphrase(\"123456789\") ~= 0x29B1 then return false end\n\n local frames = M.packetize(1, { 0xDE, 0xAD, 0xBE, 0xEF, 0x12, 0x34 },\n 0x10, 0x010203, 0x0001, 0xFFFF, 0)\n local want = {\n \"D31302000001FFFF06020100010203CAD7\",\n \"D31302010001FFFFDEADBEEF0102032489\",\n \"D31302020001FFFF1234000001020332BC\",\n }\n if #frames ~= 3 then return false end\n for i = 1, 3 do if frame_to_hex(frames[i]) ~= want[i] then return false end end\n\n local r = Reassembler.new()\n local got\n for i = 1, #frames do local p = r:push(frames[i]); if p then got = p end end\n if not got or got.codec_id ~= 1 or #got.payload ~= 6 then return false end\n if got.payload[1] ~= 0xDE or got.payload[6] ~= 0x34 then return false end\n\n -- channel filter: accept on match/broadcast, reject otherwise\n if not M.accepts(0x1234, 0x1234) or not M.accepts(M.BROADCAST, 0x1234) then return false end\n if M.accepts(0x1234, 0x5678) then return false end\n\n local pp = { f0 = 0x1234, amp = 17, mod_index = 99, mod_ratio = 5, bright = 33, env = 7, flags = 1 }\n local up = M.pm_unpack(M.pm_pack(pp))\n if up.f0 ~= pp.f0 or up.flags ~= pp.flags or up.mod_index ~= pp.mod_index then return false end\n\n return true\nend\n\nM.CERTIFIED = selfcert()\n\nreturn M\n"}]}
{"messages": [{"role": "system", "content": "You are an expert DCF adapter implementer. You build protocols (audio, game, text, SSTV, snake, mesh) as adapters over the 17-byte DeModFrame wire quantum.\n\nADAPTER PATTERN:\n- One adapter message → 1 + ceil(payload_len/4) DeModFrame packets\n- seq = packet_id[15:5] | frag_idx[4:0] (for audio/game); text uses frag_idx[9:0]\n- frag_idx 0 = descriptor: [len, frag_total, codec_id|msg_type_id, flags]\n- Payload ≤ 124 B/block (4 bytes per frame × 31 frames max)\n- L2 framing is codec/message-type-agnostic and byte-certified\n- Adding new codecs/message types never changes the wire vectors\n\nCONVENTIONS:\n- Encode: serialize payload → fragment into ≤124B chunks → wrap each in DeModFrame\n- Decode: collect frames by packet_id → reassemble payload → validate CRC per frame\n- Use CTRL (type 3) frames for audio; DATA (type 0) frames for game/text/SSTV\n- Descriptor flags: bit0 RELIABLE, bit1 ORDERED, bit2 END_TICK (adapter-specific)\n"}, {"role": "user", "content": "Implement the DCF game adapter in Rust.\n\nWire quantum spec (for context):\n# DCF Wire Quantum — Canonical Specification\n\n**Version 1** · DeMoD LLC · This document is normative.\n\nThe DCF wire protocol has exactly **one** wire format: the 17-byte `DeModFrame`\n(version nibble = 1). All other formats (the proto-message UDP header, the C\nSDK's `DCFMessageHeader`) are **adapters** over this quantum and must be\nexplicitly documented as such.\n\n## Wire Layout\n\n```\n Byte Field Width Description\n ──── ────────── ───── ─────────────────────────────────────────\n 0 sync 1 B Fixed 0xD3 (first validity gate)\n 1 flags 1 B [7:4] version (currently 1) | [3:0] type (0–3)\n 2–3 seq 2 B Big-endian u16, rolling sequence counter\n 4–5 src_id 2 B Big-endian u16, source node ID\n 6–7 dst_id 2 B Big-endian u16, destination (0xFFFF = broadcast)\n 8–11 payload 4 B Application data\n 12–14 timestamp 3 B 24-bit µs offset, big-endian, wraps ~16.7 s\n 15–16 crc16 2 B CRC-16/CCITT-FALSE over bytes [0..14]\n```\n\n**Total: 17 bytes = 136 bits.** The free field data is 108 bits (4+16+16+16+32+24).\n\n## Frame Types\n\n| Value | Name | Purpose |\n|-------|--------|-----------------------------|\n| 0 | DATA | Application payload |\n| 1 | ACK | Acknowledgement |\n| 2 | BEACON | Clock sync / broadcast |\n| 3 | CTRL | Control / fragmented audio |\n\n## Validity\n\nA frame is valid iff:\n1. `frame[0] == 0xD3` (sync byte)\n2. `(frame[1] >> 4) == 1` (version nibble)\n3. `CRC-16/CCITT-FALSE(frame[0..14]) == (frame[15] << 8) | frame[16]` (CRC)\n\nFor a random byte stream, the probability of a 17-byte window passing all three\nchecks is 2⁻²⁸ (Theorem 2.1, `wire_quanta_category.md`).\n\n## CRC-16/CCITT-FALSE\n\n- Polynomial: `0x1021`\n- Init: `0xFFFF`\n- RefIn/RefOut: `false`\n- XorOut: `0x0000`\n- Check value: `CRC(\"123456789\") = 0x29B1`\n\n## Anchors\n\n| Quantity | Value | Source |\n|----------|-------|--------|\n| CRC(\"123456789\") | `0x29B1` |...\n\nAdapter specification:\n# DCF-Game — Low-Latency Multiplayer over the DeModFrame Wire\n\n**Version 1** · DeMoD LLC · This document is normative. Companion to\n[`WIRE_QUANTUM_SPEC.md`](WIRE_QUANTUM_SPEC.md) and a sibling of\n[`DCF_AUDIO_SPEC.md`](DCF_AUDIO_SPEC.md).\n\nDCF-Game carries real-time multiplayer game traffic — player-state snapshots,\ninput frames, and discrete events — over the Punctim mesh. It introduces **no\nnew wire format**: one game message is an **adapter** over the 17-byte\n`DeModFrame` quantum, serialised into a short burst of ordinary `DATA` (type 0)\nframes. The framing is **message-type-agnostic and byte-deterministic across C,\nRust, and Python**, and is pinned by a finite certificate exactly like the wire\nquantum itself.\n\nAudio uses `CTRL` (type 3); game uses `DATA` (type 0), so the two adapters share\nthe same fragmentation scheme yet never collide on the wire.\n\n## Layered model\n\n| Layer | What | Certified? |\n|-------|------|-----------|\n| **L1** message-type registry | Pluggable message kinds keyed by `msg_type_id` (SNAPSHOT / INPUT / EVENT / JOIN). Body is opaque to L2. | SNAPSHOT/INPUT/JOIN byte layout: **yes**. EVENT body: no (opaque app bytes). |\n| **L2** framing | `packetize` / reassemble: message bytes ⇄ `DeModFrame` `DATA` frames. | **Yes** — `game_vectors.json` |\n| **L3** state buffer | Snapshots: latest-state-wins per `src_id` + dead-reckoning. Events: reliable, ordered. | Runtime (unit-tested) |\n| **L4** transport + demo | Send frames over the mesh; 2-peer game loopback. | Demo |\n\nThe key invariant: **`msg_type_id` lives in the L2 descriptor, so adding message\ntypes never changes the L2 vectors** — and neither do the reliable/ordered flags.\n\n## L2 framing (normative)\n\nOne game message → `1 + frag_total` frames. Every frame is a fully valid\n`DeModFrame` (version = 1, type = `DATA` = 0) and still passes the 246-vector wire\ncertificate.\n\n```\n seq (u16) = packet_id[15:5] (11 bits) | frag_idx[4:0] (5 bits)\n frag_idx == 0 descriptor : payload = [payload_len, frag_total, msg_type_id, flags]\n frag_idx 1..N data : payload = bytes[(k-1)*4 .. +4] (last frame zero-padded)\n frag_total = ceil(payload_len / 4)\n timestamp_us = 24-bit send time, identical across a message's frames\n src_id = player/node id ; dst_id = rendezvous channel (0xFFFF = broadcast to lobby)\n```\n\nBounds (from the 5-bit fragment field):\n\n| Quantity | Value |\n|----------|-------|\n| Max data fragments | 31 |\n| **Max message payload** | **124 bytes / message** |\n| Max `packet_id` | 2047 (wraps ≈ a few seconds of ticks) |\n| Frames per message | `1 + ceil(payload_len/4)` (1–32) |\n\n**Descriptor flags** (request transport behaviour; they do **not** change the L2\nbytes): bit 0 = `RELIABLE` (ask the transport for ARQ retransmit), bit 1 =\n`ORDERED` (deliver in `packet_id` order), bit 2 = `END_OF_TICK` (last message of a\nsimulation tick). Remaining bits reserved.\n\nBecause reliable vs. unreliable is only a flag, an unreliable SNAPSHOT and a\nreliable EVENT produce **byte-identical L2 framing** — only the flag bit and the\ntransport path differ. State larger than 124 B must be tiled across multiple\nmessages (the adapter never re-fragments beyond the 124 B quantum).\n\n**Reassembly.** Buffer by modular `packet_id`; gather the descriptor and all data\nfragments; concatenate and truncate to `payload_len`; emit\n`(packet_id, timestamp_us, msg_type_id, flags, payload)`. Duplicates are ignored.\nAny missing fragment ⇒ the message is **lost** ⇒ L3 decides (snapshots are\ndead-reckoned, reliable events are retransmitted by the transport).\n\n## Message-type registry\n\n| id | Name | Body | Reliable default | Byte-certified? |\n|----|------|------|------------------|-----------------|\n| 0 | SNAPSHOT | 14 B player state (pos/vel/yaw) | no | **Yes** |\n| 1 | INPUT | 6 B (tick u32 + buttons u16) | no | **Yes** |\n| 2 | EVENT | opaque app bytes (score, hit, chat) | yes | No (opaque) |\n| 3 | JOIN | player_id u16 + len-prefixed UTF-8 name | yes | **Yes** |\n| 4..255 | *reserved* | — | — | — |\n\n### SNAPSHOT body (msg_type 0) — 14 bytes, Q8.8 fixed-point\n\n```\n byte: 0 2 4 6 8 10 12\n field: x y z vx vy vz yaw\n i16 Q8.8 metres / metres-per-tick (6×2 B) u16 (0..65535 = 0..2π)\n```\n\nEach coordinate is a signed Q8.8 fixed-point value (`round(v·256)` clamped to\ni16); `yaw` is a raw `u16`. The **byte layout is certified** (`pack(unpack(b))==b`\nfor all 14-byte blocks); the float→Q8.8 quantisation of arbitrary positions is a\nruntime detail, not certified.\n\n### INPUT body (msg_type 1) — 6 bytes\n\n```\n byte: 0 4\n field: tick (u32) buttons (u16 bitfield)\n```\n\nFor server-authoritative variants: a monotonically increasing simulation tick and\na 16-bit button/axis bitfield. Byte layout certified.\n\n### JOIN body (msg_type 3) — variable\n\n```\n byte: 0 2 3..\n field: player_id name_len UTF-8 name (name_len bytes, ≤121)\n u16 u8\n```\n\nUsed for lobby membership. Byte layout certified for the Python and Rust\nreferences (JSON vectors); the fixed-size SNAPSHOT/INPUT bodies are additionally\ncertified in the dependency-free C test.\n\n## Certification\n\nMirrors the wire quantum and DCF-Audio: a Python reference emits golden vectors;\nC and Rust diff against them byte-for-byte.\n\n```sh\n# regenerate + verify the laws, then diff against the committed vectors\npython3 python/MCP/gen_game_vectors.py /tmp/game_vectors.json\ndiff /tmp/game_vectors.json Documentation/game_vectors.json # must be empty\n\ncd codec && cargo test --test certify_game # Rust\ngcc -std=c11 -I codec C_SDK/tests/test_game_certify.c -lm -o /tmp/gc && /tmp/gc # C\n```\n\n`Documentation/game_vectors.json` (with an identical `python/MCP/` copy) holds 7\nframing + 4 reassembly + SNAPSHOT/INPUT/JOIN body cases; `codec/game_vectors.gen.h`\nis the dependency-free C vector dump. CI runs all three certs and diffs regenerated\nvs. committed vectors (`.github/workflows/wire-certify.yml`, job `certify-game`).\n\n## Reference implementations\n\n| Lang | File | Entry points |\n|------|------|--------------|\n| C | `codec/demod_game.h` | `dcf_game_packetize` / `dcf_game_reasm_push`, `dcf_snapshot_*`, `dcf_input_*`, `dcf_join_*` |\n| Rust | `codec/src/game.rs` | `packetize` / `GameReassembler`, `snapshot_pack/unpack`, `input_pack/unpack`, `join_pack/unpack` |\n| Python | `python/MCP/gamelab_core.py` | `packetize` / `GameReassembler`, `snapshot_pack/unpack`, `input_pack/unpack`, `join_pack/unpack` |\n\n## Transport, addressing & topology\n\nGame frames ride the existing DCF transport. Peers rendezvous on a **channel**\n(the `DeModFrame` `dst_id`): a numeric channel or one derived from a shared room\npassphrase via the certified CRC-16 (`channel_from_passphrase`); `dst_id = 0xFFFF`\nis the open lobby. A node accepts a frame iff `dst_id == my_channel` or\n`dst_id == 0xFFFF`, and multiplexes opponents by `src_id`.\n\nThe initial topology is **direct peer-to-peer on a LAN** (mDNS discovery +\n`add_peer` over UDP, as DCF-Audio uses). Multi-hop **mesh forwarding** is a future\nextension: TTL-bounded flooding (re-broadcast frames whose `dst_id` ≠ self until a\nhop budget is exhausted) or a routing table. Either is additive — it does not\nchange the wire format or this certificate.\n\n## Theorem\n\nL2 framing is a fixed bit-placement adapter over the certified `DeModFrame`.\nMatching the framing + reassembly + SNAPSHOT/INPUT/JOIN body vectors pins the C\nand Rust implementations to the Python reference on the entire input space.\n`msg_type_id` and the reliable/ordered flags are opaque to L2, so the framing\nvectors are invariant to message-type and reliability choice — adding message\ntypes or transports never re-opens the certificate.\n\n\nGolden vectors (sample):\n{\n \"encode_basis\": [],\n \"syndrome_basis\": []\n}\n\nProvide the complete implementation with encode/decode functions."}, {"role": "assistant", "content": "// SPDX-License-Identifier: LGPL-3.0-only\n//! DCF-Game: low-latency multiplayer state/event transport over the DeModFrame wire.\n//!\n//! Game traffic is an *adapter* over the 17-byte DeModFrame quantum (`frame.rs`), not a\n//! new wire format — exactly like DCF-Audio (`audio.rs`). One game message (a state\n//! snapshot, an input frame, or an opaque event) is serialised into `1 + frag_total`\n//! ordinary DATA frames. This L2 framing is message-type-agnostic and byte-deterministic\n//! across C/Rust/Python — it is pinned by `Documentation/game_vectors.json`. See\n//! `Documentation/DCF_GAME_SPEC.md`.\n//!\n//! Layout (all frames version=1, type=DATA(0), big-endian):\n//! seq = packet_id[15:5] (11 bits) | frag_idx[4:0] (5 bits)\n//! frag_idx 0 descriptor : payload = [payload_len, frag_total, msg_type_id, flags]\n//! frag_idx k data : payload = bytes[(k-1)*4 .. +4] (last frame zero-padded)\n//! frag_total = ceil(payload_len/4) (<= 31 => payload_len <= 124 bytes / message)\n//!\n//! Audio uses CTRL(3); game uses DATA(0), so the two adapters never collide on the wire.\n\nuse crate::{Frame, FrameType};\nuse std::collections::BTreeMap;\n\n// ── L2 constants ────────────────────────────────────────────────────────────\npub const FRAG_BITS: u16 = 5;\npub const FRAG_MASK: u16 = 0x1F;\npub const MAX_FRAGS: usize = 31;\npub const MAX_PAYLOAD: usize = MAX_FRAGS * 4; // 124 bytes / message\npub const MAX_PACKET_ID: u16 = (1 << (16 - FRAG_BITS)) - 1; // 2047\n\n// Message-type registry ids (profiles in DCF_GAME_SPEC.md).\npub const GMSG_SNAPSHOT: u8 = 0; // packed player state (byte-deterministic)\npub const GMSG_INPUT: u8 = 1; // input bitfield + tick (byte-deterministic)\npub const GMSG_EVENT: u8 = 2; // opaque application bytes (not certified)\npub const GMSG_JOIN: u8 = 3; // lobby membership (byte-deterministic)\n// ids 4..=255 reserved.\n\n// Descriptor flag bits — request transport behaviour; do not change the L2 bytes.\npub const FLAG_RELIABLE: u8 = 0x01;\npub const FLAG_ORDERED: u8 = 0x02;\npub const FLAG_END_TICK: u8 = 0x04;\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum GameError {\n /// Message exceeds the 124-byte cap.\n PayloadTooLarge,\n /// packet_id exceeds the 11-bit field.\n BadPacketId,\n}\n\n// ── L2: packetize ───────────────────────────────────────────────────────────\n/// Serialise one game message into DeModFrame DATA frames (descriptor first, then\n/// data fragments in order). Each frame is a fully valid 17-byte DeModFrame.\npub fn packetize(\n msg_type_id: u8,\n payload: &[u8],\n packet_id: u16,\n ts_us: u32,\n src: u16,\n dst: u16,\n flags: u8,\n) -> Result<Vec<[u8; 17]>, GameError> {\n if payload.len() > MAX_PAYLOAD {\n return Err(GameError::PayloadTooLarge);\n }\n if packet_id > MAX_PACKET_ID {\n return Err(GameError::BadPacketId);\n }\n let frag_total = ((payload.len() + 3) / 4) as u8;\n let mut frames = Vec::with_capacity(1 + frag_total as usize);\n\n // frag_idx 0 — descriptor\n let desc_seq = packet_id << FRAG_BITS;\n let desc = [payload.len() as u8, frag_total, msg_type_id, flags];\n frames.push(Frame::new(1, FrameType::Data, desc_seq, src, dst, desc, ts_us).encode());\n\n // frag_idx 1..frag_total — data, last chunk zero-padded\n for k in 1..=frag_total {\n let off = (k as usize - 1) * 4;\n let mut chunk = [0u8; 4];\n let end = (off + 4).min(payload.len());\n chunk[..end - off].copy_from_slice(&payload[off..end]);\n let seq = (packet_id << FRAG_BITS) | k as u16;\n frames.push(Frame::new(1, FrameType::Data, seq, src, dst, chunk, ts_us).encode());\n }\n Ok(frames)\n}\n\n// ── L2: reassembler ─────────────────────────────────────────────────────────\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct GamePacket {\n pub packet_id: u16,\n pub ts_us: u32,\n pub msg_type_id: u8,\n pub flags: u8,\n pub payload: Vec<u8>,\n}\n\n#[derive(Default)]\nstruct Slot {\n desc: Option<(u8, u8, u8, u8)>, // (payload_len, frag_total, msg_type_id, flags)\n ts_us: u32,\n frags: BTreeMap<u8, [u8; 4]>,\n}\n\n/// Stateful reassembler. `push` emits a completed message as soon as its descriptor\n/// and every data fragment have arrived; duplicates are ignored; `finalize` reports\n/// any still-incomplete message as lost. Mirrors the C and Python references.\n#[derive(Default)]\npub struct GameReassembler {\n slots: BTreeMap<u16, Slot>,\n}\n\nimpl GameReassembler {\n pub fn new() -> Self {\n Self::default()\n }\n\n pub fn push(&mut self, frame: &[u8; 17]) -> Option<GamePacket> {\n let d = Frame::decode(&frame[..]).ok()?;\n if d.frame_type != FrameType::Data {\n return None;\n }\n let packet_id = d.seq >> FRAG_BITS;\n let frag_idx = (d.seq & FRAG_MASK) as u8;\n let entry = self.slots.entry(packet_id).or_default();\n entry.ts_us = d.timestamp_us;\n if frag_idx == 0 {\n if entry.desc.is_none() {\n entry.desc = Some((d.payload[0], d.payload[1], d.payload[2], d.payload[3]));\n }\n } else {\n entry.frags.entry(frag_idx).or_insert(d.payload);\n }\n self.try_emit(packet_id)\n }\n\n fn try_emit(&mut self, packet_id: u16) -> Option<GamePacket> {\n let entry = self.slots.get(&packet_id)?;\n let (payload_len, frag_total, msg_type_id, flags) = entry.desc?;\n if (1..=frag_total).any(|k| !entry.frags.contains_key(&k)) {\n return None;\n }\n let mut raw = Vec::with_capacity(frag_total as usize * 4);\n for k in 1..=frag_total {\n raw.extend_from_slice(&entry.frags[&k]);\n }\n raw.truncate(payload_len as usize);\n let ts_us = entry.ts_us;\n self.slots.remove(&packet_id);\n Some(GamePacket { packet_id, ts_us, msg_type_id, flags, payload: raw })\n }\n\n /// Report every still-incomplete message as lost (ascending packet_id), clearing state.\n pub fn finalize(&mut self) -> Vec<u16> {\n let lost: Vec<u16> = self.slots.keys().copied().collect(); // BTreeMap => sorted\n self.slots.clear();\n lost\n }\n}\n\n// ── L1: SNAPSHOT body (msg_type 0) — 14-byte player state, Q8.8 fixed-point ──\npub const SNAPSHOT_LEN: usize = 14;\n\n#[derive(Debug, Clone, Copy, PartialEq, Default)]\npub struct Snapshot {\n pub x: f32,\n pub y: f32,\n pub z: f32,\n pub vx: f32,\n pub vy: f32,\n pub vz: f32,\n pub yaw: u16,\n}\n\nfn q88(v: f32) -> [u8; 2] {\n let q = (v * 256.0).round().clamp(-32768.0, 32767.0) as i32 as i16;\n (q as u16).to_be_bytes()\n}\nfn unq88(b: &[u8]) -> f32 {\n (i16::from_be_bytes([b[0], b[1]]) as f32) / 256.0\n}\n\n/// Pack a snapshot into 14 deterministic bytes. The byte layout is certified.\npub fn snapshot_pack(s: &Snapshot) -> [u8; SNAPSHOT_LEN] {\n let mut out = [0u8; SNAPSHOT_LEN];\n for (i, v) in [s.x, s.y, s.z, s.vx, s.vy, s.vz].iter().enumerate() {\n out[i * 2..i * 2 + 2].copy_from_slice(&q88(*v));\n }\n out[12..14].copy_from_slice(&s.yaw.to_be_bytes());\n out\n}\n\n/// Unpack 14 bytes into a snapshot. `snapshot_pack(snapshot_unpack(b)) == b`.\npub fn snapshot_unpack(b: &[u8; SNAPSHOT_LEN]) -> Snapshot {\n Snapshot {\n x: unq88(&b[0..2]),\n y: unq88(&b[2..4]),\n z: unq88(&b[4..6]),\n vx: unq88(&b[6..8]),\n vy: unq88(&b[8..10]),\n vz: unq88(&b[10..12]),\n yaw: u16::from_be_bytes([b[12], b[13]]),\n }\n}\n\n// ── L1: INPUT body (msg_type 1) — 6 bytes: tick u32 + buttons u16 bitfield ───\npub const INPUT_LEN: usize = 6;\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]\npub struct Input {\n pub tick: u32,\n pub buttons: u16,\n}\n\npub fn input_pack(p: &Input) -> [u8; INPUT_LEN] {\n let mut out = [0u8; INPUT_LEN];\n out[0..4].copy_from_slice(&p.tick.to_be_bytes());\n out[4..6].copy_from_slice(&p.buttons.to_be_bytes());\n out\n}\npub fn input_unpack(b: &[u8; INPUT_LEN]) -> Input {\n Input {\n tick: u32::from_be_bytes([b[0], b[1], b[2], b[3]]),\n buttons: u16::from_be_bytes([b[4], b[5]]),\n }\n}\n\n// ── L1: JOIN body (msg_type 3) — player_id u16 + len-prefixed UTF-8 name ─────\npub fn join_pack(player_id: u16, name: &str) -> Vec<u8> {\n let nb = name.as_bytes();\n let n = nb.len().min(MAX_PAYLOAD - 3);\n let mut out = Vec::with_capacity(3 + n);\n out.extend_from_slice(&player_id.to_be_bytes());\n out.push(n as u8);\n out.extend_from_slice(&nb[..n]);\n out\n}\npub fn join_unpack(b: &[u8]) -> Option<(u16, String)> {\n if b.len() < 3 {\n return None;\n }\n let player_id = u16::from_be_bytes([b[0], b[1]]);\n let n = b[2] as usize;\n let name = String::from_utf8_lossy(&b[3..(3 + n).min(b.len())]).into_owned();\n Some((player_id, name))\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn packetize_reassemble_is_identity_under_reorder() {\n let body: Vec<u8> = (0..30u8).collect(); // spans 8 fragments\n let frames = packetize(GMSG_EVENT, &body, 42, 0x010203, 0x00A1, 0xFFFF, FLAG_RELIABLE).unwrap();\n assert_eq!(frames.len(), 1 + (body.len() + 3) / 4);\n // feed in reverse order; reassembly must still recover the exact message\n let mut r = GameReassembler::new();\n let mut got = None;\n for f in frames.iter().rev() {\n if let Some(p) = r.push(f) {\n got = Some(p);\n }\n }\n let p = got.expect(\"message reassembles regardless of frame order\");\n assert_eq!(p.packet_id, 42);\n assert_eq!(p.msg_type_id, GMSG_EVENT);\n assert_eq!(p.flags, FLAG_RELIABLE);\n assert_eq!(p.payload, body);\n assert!(r.finalize().is_empty());\n }\n\n #[test]\n fn dropped_fragment_is_reported_lost() {\n let body = [1u8, 2, 3, 4, 5, 6, 7];\n let frames = packetize(GMSG_SNAPSHOT, &body, 7, 0, 1, 1, 0).unwrap();\n let mut r = GameReassembler::new();\n for (i, f) in frames.iter().enumerate() {\n if i == 2 {\n continue; // drop one data fragment\n }\n assert!(r.push(f).is_none());\n }\n assert_eq!(r.finalize(), vec![7]); // incomplete => lost\n }\n\n #[test]\n fn snapshot_input_byte_roundtrip() {\n let s = Snapshot { x: 1.5, y: -2.25, z: 0.0, vx: 0.125, vy: -0.5, vz: 3.0, yaw: 40000 };\n assert_eq!(snapshot_unpack(&snapshot_pack(&s)), s); // Q8.8-representable values\n let i = Input { tick: 0xDEADBEEF, buttons: 0xBEEF };\n assert_eq!(input_unpack(&input_pack(&i)), i);\n let jb = join_pack(0xABCD, \"neo\");\n assert_eq!(join_unpack(&jb), Some((0xABCD, \"neo\".to_string())));\n }\n}\n"}]}
{"messages": [{"role": "system", "content": "You are an expert DCF adapter implementer. You build protocols (audio, game, text, SSTV, snake, mesh) as adapters over the 17-byte DeModFrame wire quantum.\n\nADAPTER PATTERN:\n- One adapter message → 1 + ceil(payload_len/4) DeModFrame packets\n- seq = packet_id[15:5] | frag_idx[4:0] (for audio/game); text uses frag_idx[9:0]\n- frag_idx 0 = descriptor: [len, frag_total, codec_id|msg_type_id, flags]\n- Payload ≤ 124 B/block (4 bytes per frame × 31 frames max)\n- L2 framing is codec/message-type-agnostic and byte-certified\n- Adding new codecs/message types never changes the wire vectors\n\nCONVENTIONS:\n- Encode: serialize payload → fragment into ≤124B chunks → wrap each in DeModFrame\n- Decode: collect frames by packet_id → reassemble payload → validate CRC per frame\n- Use CTRL (type 3) frames for audio; DATA (type 0) frames for game/text/SSTV\n- Descriptor flags: bit0 RELIABLE, bit1 ORDERED, bit2 END_TICK (adapter-specific)\n"}, {"role": "user", "content": "Implement the DCF game adapter in Python.\n\nWire quantum spec (for context):\n# DCF Wire Quantum — Canonical Specification\n\n**Version 1** · DeMoD LLC · This document is normative.\n\nThe DCF wire protocol has exactly **one** wire format: the 17-byte `DeModFrame`\n(version nibble = 1). All other formats (the proto-message UDP header, the C\nSDK's `DCFMessageHeader`) are **adapters** over this quantum and must be\nexplicitly documented as such.\n\n## Wire Layout\n\n```\n Byte Field Width Description\n ──── ────────── ───── ─────────────────────────────────────────\n 0 sync 1 B Fixed 0xD3 (first validity gate)\n 1 flags 1 B [7:4] version (currently 1) | [3:0] type (0–3)\n 2–3 seq 2 B Big-endian u16, rolling sequence counter\n 4–5 src_id 2 B Big-endian u16, source node ID\n 6–7 dst_id 2 B Big-endian u16, destination (0xFFFF = broadcast)\n 8–11 payload 4 B Application data\n 12–14 timestamp 3 B 24-bit µs offset, big-endian, wraps ~16.7 s\n 15–16 crc16 2 B CRC-16/CCITT-FALSE over bytes [0..14]\n```\n\n**Total: 17 bytes = 136 bits.** The free field data is 108 bits (4+16+16+16+32+24).\n\n## Frame Types\n\n| Value | Name | Purpose |\n|-------|--------|-----------------------------|\n| 0 | DATA | Application payload |\n| 1 | ACK | Acknowledgement |\n| 2 | BEACON | Clock sync / broadcast |\n| 3 | CTRL | Control / fragmented audio |\n\n## Validity\n\nA frame is valid iff:\n1. `frame[0] == 0xD3` (sync byte)\n2. `(frame[1] >> 4) == 1` (version nibble)\n3. `CRC-16/CCITT-FALSE(frame[0..14]) == (frame[15] << 8) | frame[16]` (CRC)\n\nFor a random byte stream, the probability of a 17-byte window passing all three\nchecks is 2⁻²⁸ (Theorem 2.1, `wire_quanta_category.md`).\n\n## CRC-16/CCITT-FALSE\n\n- Polynomial: `0x1021`\n- Init: `0xFFFF`\n- RefIn/RefOut: `false`\n- XorOut: `0x0000`\n- Check value: `CRC(\"123456789\") = 0x29B1`\n\n## Anchors\n\n| Quantity | Value | Source |\n|----------|-------|--------|\n| CRC(\"123456789\") | `0x29B1` |...\n\nAdapter specification:\n# DCF-Game — Low-Latency Multiplayer over the DeModFrame Wire\n\n**Version 1** · DeMoD LLC · This document is normative. Companion to\n[`WIRE_QUANTUM_SPEC.md`](WIRE_QUANTUM_SPEC.md) and a sibling of\n[`DCF_AUDIO_SPEC.md`](DCF_AUDIO_SPEC.md).\n\nDCF-Game carries real-time multiplayer game traffic — player-state snapshots,\ninput frames, and discrete events — over the Punctim mesh. It introduces **no\nnew wire format**: one game message is an **adapter** over the 17-byte\n`DeModFrame` quantum, serialised into a short burst of ordinary `DATA` (type 0)\nframes. The framing is **message-type-agnostic and byte-deterministic across C,\nRust, and Python**, and is pinned by a finite certificate exactly like the wire\nquantum itself.\n\nAudio uses `CTRL` (type 3); game uses `DATA` (type 0), so the two adapters share\nthe same fragmentation scheme yet never collide on the wire.\n\n## Layered model\n\n| Layer | What | Certified? |\n|-------|------|-----------|\n| **L1** message-type registry | Pluggable message kinds keyed by `msg_type_id` (SNAPSHOT / INPUT / EVENT / JOIN). Body is opaque to L2. | SNAPSHOT/INPUT/JOIN byte layout: **yes**. EVENT body: no (opaque app bytes). |\n| **L2** framing | `packetize` / reassemble: message bytes ⇄ `DeModFrame` `DATA` frames. | **Yes** — `game_vectors.json` |\n| **L3** state buffer | Snapshots: latest-state-wins per `src_id` + dead-reckoning. Events: reliable, ordered. | Runtime (unit-tested) |\n| **L4** transport + demo | Send frames over the mesh; 2-peer game loopback. | Demo |\n\nThe key invariant: **`msg_type_id` lives in the L2 descriptor, so adding message\ntypes never changes the L2 vectors** — and neither do the reliable/ordered flags.\n\n## L2 framing (normative)\n\nOne game message → `1 + frag_total` frames. Every frame is a fully valid\n`DeModFrame` (version = 1, type = `DATA` = 0) and still passes the 246-vector wire\ncertificate.\n\n```\n seq (u16) = packet_id[15:5] (11 bits) | frag_idx[4:0] (5 bits)\n frag_idx == 0 descriptor : payload = [payload_len, frag_total, msg_type_id, flags]\n frag_idx 1..N data : payload = bytes[(k-1)*4 .. +4] (last frame zero-padded)\n frag_total = ceil(payload_len / 4)\n timestamp_us = 24-bit send time, identical across a message's frames\n src_id = player/node id ; dst_id = rendezvous channel (0xFFFF = broadcast to lobby)\n```\n\nBounds (from the 5-bit fragment field):\n\n| Quantity | Value |\n|----------|-------|\n| Max data fragments | 31 |\n| **Max message payload** | **124 bytes / message** |\n| Max `packet_id` | 2047 (wraps ≈ a few seconds of ticks) |\n| Frames per message | `1 + ceil(payload_len/4)` (1–32) |\n\n**Descriptor flags** (request transport behaviour; they do **not** change the L2\nbytes): bit 0 = `RELIABLE` (ask the transport for ARQ retransmit), bit 1 =\n`ORDERED` (deliver in `packet_id` order), bit 2 = `END_OF_TICK` (last message of a\nsimulation tick). Remaining bits reserved.\n\nBecause reliable vs. unreliable is only a flag, an unreliable SNAPSHOT and a\nreliable EVENT produce **byte-identical L2 framing** — only the flag bit and the\ntransport path differ. State larger than 124 B must be tiled across multiple\nmessages (the adapter never re-fragments beyond the 124 B quantum).\n\n**Reassembly.** Buffer by modular `packet_id`; gather the descriptor and all data\nfragments; concatenate and truncate to `payload_len`; emit\n`(packet_id, timestamp_us, msg_type_id, flags, payload)`. Duplicates are ignored.\nAny missing fragment ⇒ the message is **lost** ⇒ L3 decides (snapshots are\ndead-reckoned, reliable events are retransmitted by the transport).\n\n## Message-type registry\n\n| id | Name | Body | Reliable default | Byte-certified? |\n|----|------|------|------------------|-----------------|\n| 0 | SNAPSHOT | 14 B player state (pos/vel/yaw) | no | **Yes** |\n| 1 | INPUT | 6 B (tick u32 + buttons u16) | no | **Yes** |\n| 2 | EVENT | opaque app bytes (score, hit, chat) | yes | No (opaque) |\n| 3 | JOIN | player_id u16 + len-prefixed UTF-8 name | yes | **Yes** |\n| 4..255 | *reserved* | — | — | — |\n\n### SNAPSHOT body (msg_type 0) — 14 bytes, Q8.8 fixed-point\n\n```\n byte: 0 2 4 6 8 10 12\n field: x y z vx vy vz yaw\n i16 Q8.8 metres / metres-per-tick (6×2 B) u16 (0..65535 = 0..2π)\n```\n\nEach coordinate is a signed Q8.8 fixed-point value (`round(v·256)` clamped to\ni16); `yaw` is a raw `u16`. The **byte layout is certified** (`pack(unpack(b))==b`\nfor all 14-byte blocks); the float→Q8.8 quantisation of arbitrary positions is a\nruntime detail, not certified.\n\n### INPUT body (msg_type 1) — 6 bytes\n\n```\n byte: 0 4\n field: tick (u32) buttons (u16 bitfield)\n```\n\nFor server-authoritative variants: a monotonically increasing simulation tick and\na 16-bit button/axis bitfield. Byte layout certified.\n\n### JOIN body (msg_type 3) — variable\n\n```\n byte: 0 2 3..\n field: player_id name_len UTF-8 name (name_len bytes, ≤121)\n u16 u8\n```\n\nUsed for lobby membership. Byte layout certified for the Python and Rust\nreferences (JSON vectors); the fixed-size SNAPSHOT/INPUT bodies are additionally\ncertified in the dependency-free C test.\n\n## Certification\n\nMirrors the wire quantum and DCF-Audio: a Python reference emits golden vectors;\nC and Rust diff against them byte-for-byte.\n\n```sh\n# regenerate + verify the laws, then diff against the committed vectors\npython3 python/MCP/gen_game_vectors.py /tmp/game_vectors.json\ndiff /tmp/game_vectors.json Documentation/game_vectors.json # must be empty\n\ncd codec && cargo test --test certify_game # Rust\ngcc -std=c11 -I codec C_SDK/tests/test_game_certify.c -lm -o /tmp/gc && /tmp/gc # C\n```\n\n`Documentation/game_vectors.json` (with an identical `python/MCP/` copy) holds 7\nframing + 4 reassembly + SNAPSHOT/INPUT/JOIN body cases; `codec/game_vectors.gen.h`\nis the dependency-free C vector dump. CI runs all three certs and diffs regenerated\nvs. committed vectors (`.github/workflows/wire-certify.yml`, job `certify-game`).\n\n## Reference implementations\n\n| Lang | File | Entry points |\n|------|------|--------------|\n| C | `codec/demod_game.h` | `dcf_game_packetize` / `dcf_game_reasm_push`, `dcf_snapshot_*`, `dcf_input_*`, `dcf_join_*` |\n| Rust | `codec/src/game.rs` | `packetize` / `GameReassembler`, `snapshot_pack/unpack`, `input_pack/unpack`, `join_pack/unpack` |\n| Python | `python/MCP/gamelab_core.py` | `packetize` / `GameReassembler`, `snapshot_pack/unpack`, `input_pack/unpack`, `join_pack/unpack` |\n\n## Transport, addressing & topology\n\nGame frames ride the existing DCF transport. Peers rendezvous on a **channel**\n(the `DeModFrame` `dst_id`): a numeric channel or one derived from a shared room\npassphrase via the certified CRC-16 (`channel_from_passphrase`); `dst_id = 0xFFFF`\nis the open lobby. A node accepts a frame iff `dst_id == my_channel` or\n`dst_id == 0xFFFF`, and multiplexes opponents by `src_id`.\n\nThe initial topology is **direct peer-to-peer on a LAN** (mDNS discovery +\n`add_peer` over UDP, as DCF-Audio uses). Multi-hop **mesh forwarding** is a future\nextension: TTL-bounded flooding (re-broadcast frames whose `dst_id` ≠ self until a\nhop budget is exhausted) or a routing table. Either is additive — it does not\nchange the wire format or this certificate.\n\n## Theorem\n\nL2 framing is a fixed bit-placement adapter over the certified `DeModFrame`.\nMatching the framing + reassembly + SNAPSHOT/INPUT/JOIN body vectors pins the C\nand Rust implementations to the Python reference on the entire input space.\n`msg_type_id` and the reliable/ordered flags are opaque to L2, so the framing\nvectors are invariant to message-type and reliability choice — adding message\ntypes or transports never re-opens the certificate.\n\n\nGolden vectors (sample):\n{\n \"encode_basis\": [],\n \"syndrome_basis\": []\n}\n\nProvide the complete implementation with encode/decode functions."}, {"role": "assistant", "content": "# SPDX-License-Identifier: LGPL-3.0-only\n\"\"\"DCF GameLab core — reference for the DCF-Game L2 framing (the certified layer).\n\nDCF-Game is an *adapter* over the 17-byte DeModFrame wire quantum, exactly like\nDCF-Audio: one game message (a state snapshot, an input frame, or an opaque event)\nis serialised into 1 + frag_total ordinary DATA frames. This layer is message-type\nagnostic and byte-deterministic across C / Rust / Python — `msg_type_id` lives only\nin the descriptor frame, so adding message types never changes these vectors.\n\nL2 framing (all frames version=1, type=DATA(0), big-endian; see WIRE_QUANTUM_SPEC.md):\n seq (u16) = packet_id[15:5] (11 bits) | frag_idx[4:0] (5 bits)\n frag_idx == 0 descriptor : payload = [payload_len, frag_total, msg_type_id, flags]\n frag_idx 1..N data : payload = bytes[(k-1)*4 .. +4], last frame zero-padded\n timestamp_us = 24-bit send time, identical across a message's frames\n frag_total = ceil(payload_len / 4) (<= 31 => payload_len <= 124 bytes / message)\n\nAudio uses CTRL(3); game uses DATA(0) so the two adapters never collide on the wire.\nThis module imports the certified wire codec from wirelab_core so the emitted bytes\nare provably the same DeModFrame the 246-vector wire certificate already pins.\n\"\"\"\nimport os\nimport sys\n\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\nfrom wirelab_core import encode, decode # certified DeModFrame codec\n\n# ── L2 framing constants ──────────────────────────────────────────────────────\nFDATA = 0 # frame type carrying fragmented game messages\nFRAG_BITS = 5 # low bits of seq hold the fragment index\nFRAG_MASK = (1 << FRAG_BITS) - 1 # 0x1F\nMAX_FRAGS = FRAG_MASK # 31 data fragments\nMAX_PAYLOAD = MAX_FRAGS * 4 # 124 bytes / message\nMAX_PACKET_ID = (1 << (16 - FRAG_BITS)) - 1 # 2047\n\n# ── Message-type registry ids (profiles live in DCF_GAME_SPEC.md) ─────────────\nGMSG_SNAPSHOT = 0 # packed player state (pos/vel/yaw) — byte-deterministic\nGMSG_INPUT = 1 # input bitfield + tick — byte-deterministic\nGMSG_EVENT = 2 # opaque application bytes (score, hit, chat) — not certified\nGMSG_JOIN = 3 # lobby membership (player id + name) — byte-deterministic\n# ids 4..255 reserved for future message types\n\n# Descriptor flags — request transport behaviour; do not change the L2 bytes.\nFLAG_RELIABLE = 0x01 # ask the transport for ARQ retransmit\nFLAG_ORDERED = 0x02 # deliver in packet_id order\nFLAG_END_TICK = 0x04 # last message of a simulation tick\n\n\ndef _ceil_div(a, b):\n return (a + b - 1) // b\n\n\ndef packetize(msg_type_id, payload, packet_id, ts_us, src, dst, flags=0):\n \"\"\"Serialise one game message (`payload`, 0..124 bytes) into a list of 17-byte\n DeModFrame DATA frames. Returns [descriptor_frame, data_frame, ...].\"\"\"\n if not (0 <= msg_type_id < 256):\n raise ValueError(\"msg_type_id must be u8\")\n if not (0 <= flags < 256):\n raise ValueError(\"flags must be u8\")\n if not (0 <= packet_id <= MAX_PACKET_ID):\n raise ValueError(f\"packet_id must be 0..{MAX_PACKET_ID}\")\n if len(payload) > MAX_PAYLOAD:\n raise ValueError(f\"payload {len(payload)}B exceeds {MAX_PAYLOAD}B/message cap\")\n\n frag_total = _ceil_div(len(payload), 4) # data fragments after descriptor\n frames = []\n\n # frag_idx 0 — descriptor\n desc_seq = (packet_id << FRAG_BITS) | 0\n desc_payload = bytes([len(payload), frag_total, msg_type_id, flags])\n frames.append(encode(FDATA, desc_seq, src, dst, desc_payload, ts_us))\n\n # frag_idx 1..frag_total — data, last chunk zero-padded to 4 bytes\n for k in range(1, frag_total + 1):\n chunk = payload[(k - 1) * 4:(k - 1) * 4 + 4]\n chunk = chunk + bytes(4 - len(chunk))\n seq = (packet_id << FRAG_BITS) | k\n frames.append(encode(FDATA, seq, src, dst, chunk, ts_us))\n\n return frames\n\n\nclass GameReassembler:\n \"\"\"Stateful reassembler. push() emits a completed message as soon as its\n descriptor and every data fragment have arrived; duplicates are ignored;\n finalize() reports any message still incomplete as lost.\n\n Emitted packet event: (\"packet\", packet_id, ts_us, msg_type_id, payload_bytes, flags)\n Lost event: (\"lost\", packet_id)\n \"\"\"\n\n def __init__(self):\n self._pkts = {} # packet_id -> dict(desc, ts, frags{idx:4B})\n\n def push(self, frame):\n try:\n d = decode(frame)\n except ValueError:\n return []\n if d[\"frame_type\"] != FDATA:\n return []\n seq = d[\"seq\"]\n packet_id = seq >> FRAG_BITS\n frag_idx = seq & FRAG_MASK\n body = bytes.fromhex(d[\"payload\"])\n\n entry = self._pkts.setdefault(packet_id, {\"desc\": None, \"ts\": d[\"ts_us\"],\n \"frags\": {}})\n entry[\"ts\"] = d[\"ts_us\"]\n if frag_idx == 0:\n if entry[\"desc\"] is None:\n entry[\"desc\"] = {\"payload_len\": body[0], \"frag_total\": body[1],\n \"msg_type_id\": body[2], \"flags\": body[3]}\n elif frag_idx not in entry[\"frags\"]:\n entry[\"frags\"][frag_idx] = body\n\n return self._try_emit(packet_id)\n\n def _try_emit(self, packet_id):\n entry = self._pkts.get(packet_id)\n if entry is None or entry[\"desc\"] is None:\n return []\n desc = entry[\"desc\"]\n need = set(range(1, desc[\"frag_total\"] + 1))\n if not need.issubset(entry[\"frags\"].keys()):\n return []\n raw = b\"\".join(entry[\"frags\"][k] for k in range(1, desc[\"frag_total\"] + 1))\n payload = raw[:desc[\"payload_len\"]]\n del self._pkts[packet_id]\n return [(\"packet\", packet_id, entry[\"ts\"], desc[\"msg_type_id\"], payload, desc[\"flags\"])]\n\n def finalize(self):\n events = [(\"lost\", pid) for pid in sorted(self._pkts.keys())]\n self._pkts.clear()\n return events\n\n\n# ── SNAPSHOT body (msg_type 0): 14-byte player state, Q8.8 fixed-point ────────\n# Layout (big-endian): x y z vx vy vz as i16 Q8.8 metres / metres-per-tick, then\n# yaw as u16 mapping 0..2π over 0..65535. Byte-deterministic — this layout is\n# certified (unlike the opaque EVENT body).\nSNAPSHOT_LEN = 14\nSNAPSHOT_FIELDS = (\"x\", \"y\", \"z\", \"vx\", \"vy\", \"vz\", \"yaw\")\n\n\ndef _q88(v):\n \"\"\"float metres -> i16 Q8.8, clamped.\"\"\"\n q = int(round(v * 256.0))\n return max(-32768, min(32767, q)) & 0xFFFF\n\n\ndef _unq88(u):\n \"\"\"u16 two's-complement Q8.8 -> float metres.\"\"\"\n return (u - 0x10000 if u >= 0x8000 else u) / 256.0\n\n\ndef snapshot_pack(s):\n \"\"\"dict{x,y,z,vx,vy,vz,yaw} -> 14 bytes. yaw is an integer u16.\"\"\"\n out = bytearray()\n for f in (\"x\", \"y\", \"z\", \"vx\", \"vy\", \"vz\"):\n out += _q88(float(s[f])).to_bytes(2, \"big\")\n out += (int(s[\"yaw\"]) & 0xFFFF).to_bytes(2, \"big\")\n return bytes(out)\n\n\ndef snapshot_unpack(b):\n \"\"\"14 bytes -> dict. unpack∘pack is exact for Q8.8-representable inputs.\"\"\"\n if len(b) != SNAPSHOT_LEN:\n raise ValueError(\"snapshot body must be exactly 14 bytes\")\n vals = [int.from_bytes(b[i:i + 2], \"big\") for i in range(0, 12, 2)]\n return {\n \"x\": _unq88(vals[0]), \"y\": _unq88(vals[1]), \"z\": _unq88(vals[2]),\n \"vx\": _unq88(vals[3]), \"vy\": _unq88(vals[4]), \"vz\": _unq88(vals[5]),\n \"yaw\": int.from_bytes(b[12:14], \"big\"),\n }\n\n\n# ── INPUT body (msg_type 1): 6 bytes — tick u32 + buttons u16 bitfield ────────\nINPUT_LEN = 6\n\n\ndef input_pack(p):\n \"\"\"dict{tick,buttons} -> 6 bytes. Byte-deterministic.\"\"\"\n return ((int(p[\"tick\"]) & 0xFFFFFFFF).to_bytes(4, \"big\") +\n (int(p[\"buttons\"]) & 0xFFFF).to_bytes(2, \"big\"))\n\n\ndef input_unpack(b):\n if len(b) != INPUT_LEN:\n raise ValueError(\"input body must be exactly 6 bytes\")\n return {\"tick\": int.from_bytes(b[0:4], \"big\"),\n \"buttons\": int.from_bytes(b[4:6], \"big\")}\n\n\n# ── JOIN body (msg_type 3): player_id u16 + len-prefixed UTF-8 name ───────────\ndef join_pack(j):\n \"\"\"dict{player_id,name} -> 3 + len(name) bytes (name truncated to 121).\"\"\"\n name = j[\"name\"].encode(\"utf-8\")[:MAX_PAYLOAD - 3]\n return (int(j[\"player_id\"]) & 0xFFFF).to_bytes(2, \"big\") + bytes([len(name)]) + name\n\n\ndef join_unpack(b):\n if len(b) < 3:\n raise ValueError(\"join body must be at least 3 bytes\")\n n = b[2]\n return {\"player_id\": int.from_bytes(b[0:2], \"big\"),\n \"name\": b[3:3 + n].decode(\"utf-8\", \"replace\")}\n"}]}
{"messages": [{"role": "system", "content": "You are an expert DCF adapter implementer. You build protocols (audio, game, text, SSTV, snake, mesh) as adapters over the 17-byte DeModFrame wire quantum.\n\nADAPTER PATTERN:\n- One adapter message → 1 + ceil(payload_len/4) DeModFrame packets\n- seq = packet_id[15:5] | frag_idx[4:0] (for audio/game); text uses frag_idx[9:0]\n- frag_idx 0 = descriptor: [len, frag_total, codec_id|msg_type_id, flags]\n- Payload ≤ 124 B/block (4 bytes per frame × 31 frames max)\n- L2 framing is codec/message-type-agnostic and byte-certified\n- Adding new codecs/message types never changes the wire vectors\n\nCONVENTIONS:\n- Encode: serialize payload → fragment into ≤124B chunks → wrap each in DeModFrame\n- Decode: collect frames by packet_id → reassemble payload → validate CRC per frame\n- Use CTRL (type 3) frames for audio; DATA (type 0) frames for game/text/SSTV\n- Descriptor flags: bit0 RELIABLE, bit1 ORDERED, bit2 END_TICK (adapter-specific)\n"}, {"role": "user", "content": "Implement the DCF text adapter in Python.\n\nWire quantum spec (for context):\n# DCF Wire Quantum — Canonical Specification\n\n**Version 1** · DeMoD LLC · This document is normative.\n\nThe DCF wire protocol has exactly **one** wire format: the 17-byte `DeModFrame`\n(version nibble = 1). All other formats (the proto-message UDP header, the C\nSDK's `DCFMessageHeader`) are **adapters** over this quantum and must be\nexplicitly documented as such.\n\n## Wire Layout\n\n```\n Byte Field Width Description\n ──── ────────── ───── ─────────────────────────────────────────\n 0 sync 1 B Fixed 0xD3 (first validity gate)\n 1 flags 1 B [7:4] version (currently 1) | [3:0] type (0–3)\n 2–3 seq 2 B Big-endian u16, rolling sequence counter\n 4–5 src_id 2 B Big-endian u16, source node ID\n 6–7 dst_id 2 B Big-endian u16, destination (0xFFFF = broadcast)\n 8–11 payload 4 B Application data\n 12–14 timestamp 3 B 24-bit µs offset, big-endian, wraps ~16.7 s\n 15–16 crc16 2 B CRC-16/CCITT-FALSE over bytes [0..14]\n```\n\n**Total: 17 bytes = 136 bits.** The free field data is 108 bits (4+16+16+16+32+24).\n\n## Frame Types\n\n| Value | Name | Purpose |\n|-------|--------|-----------------------------|\n| 0 | DATA | Application payload |\n| 1 | ACK | Acknowledgement |\n| 2 | BEACON | Clock sync / broadcast |\n| 3 | CTRL | Control / fragmented audio |\n\n## Validity\n\nA frame is valid iff:\n1. `frame[0] == 0xD3` (sync byte)\n2. `(frame[1] >> 4) == 1` (version nibble)\n3. `CRC-16/CCITT-FALSE(frame[0..14]) == (frame[15] << 8) | frame[16]` (CRC)\n\nFor a random byte stream, the probability of a 17-byte window passing all three\nchecks is 2⁻²⁸ (Theorem 2.1, `wire_quanta_category.md`).\n\n## CRC-16/CCITT-FALSE\n\n- Polynomial: `0x1021`\n- Init: `0xFFFF`\n- RefIn/RefOut: `false`\n- XorOut: `0x0000`\n- Check value: `CRC(\"123456789\") = 0x29B1`\n\n## Anchors\n\n| Quantity | Value | Source |\n|----------|-------|--------|\n| CRC(\"123456789\") | `0x29B1` |...\n\nAdapter specification:\n# DCF-Text — Chat / Agent-to-Agent Text over the DeModFrame Wire\n\n**Version 1** · DeMoD LLC · This document is normative. Companion to\n[`WIRE_QUANTUM_SPEC.md`](WIRE_QUANTUM_SPEC.md) and a sibling of\n[`DCF_GAME_SPEC.md`](DCF_GAME_SPEC.md) and [`DCF_AUDIO_SPEC.md`](DCF_AUDIO_SPEC.md).\n\nDCF-Text carries UTF-8 text — human chat and **agent-to-agent** messages — over the\nPunctim mesh. It introduces **no new wire format**: one message is an **adapter**\nover the 17-byte `DeModFrame` quantum, serialised into a short burst of ordinary\n`DATA` (type 0) frames. The framing is **content-agnostic and byte-deterministic\nacross C, Rust, Python, and Go**, pinned by a finite certificate exactly like the\nwire quantum itself.\n\nAudio rides `CTRL` (type 3); **text and game both ride `DATA` (type 0)** but split\nthe `seq` field differently — text gives the fragment index **10 bits** (messages\nare larger and lower-rate than 20 ms audio blocks or per-tick game state), game\ngives it 5. See [Relationship to DCF-Game](#relationship-to-dcf-game).\n\n## Layered model\n\n| Layer | What | Certified? |\n|-------|------|-----------|\n| **L1** message | One UTF-8 string (≤ 4092 B) plus opaque descriptor flags. | n/a (opaque app bytes) |\n| **L2** framing | `packetize` / reassemble: message bytes ⇄ `DeModFrame` `DATA` frames. | **Yes** — `text_vectors.json` |\n| **L3** delivery | Reassembly with dup-suppression + out-of-order tolerance; optional ARQ via the `RELIABLE` flag. | Runtime (unit-tested) |\n| **L4** transport + apps | Frames over the mesh; matrix-bridge agent-to-agent, `DcfTextNode`. | App |\n\nThe key invariant: **the descriptor flags are opaque to L2, so they never change\nthe L2 vectors** — the framing bytes are invariant to the agent/more/reliable\nchoice.\n\n## L2 framing (normative)\n\nOne message → `1 + frag_total` frames. Every frame is a fully valid `DeModFrame`\n(version = 1, type = `DATA` = 0) and still passes the 246-vector wire certificate.\n\n```\n seq (u16) = packet_id[15:10] (6 bits, 0..63) | frag_idx[9:0] (10 bits, 0..1023)\n frag_idx == 0 descriptor : payload = [len_hi, len_lo, flags, 0] (length big-endian u16)\n frag_idx 1..N data : payload = bytes[(k-1)*4 .. +4] (last frame zero-padded)\n frag_total = ceil(len / 4)\n timestamp_us = 24-bit send time, identical across a message's frames\n src_id = local node id ; dst_id = rendezvous channel (0xFFFF = broadcast)\n```\n\nBounds (from the 10-bit fragment field):\n\n| Quantity | Value |\n|----------|-------|\n| Max data fragments | 1023 |\n| **Max message payload** | **4092 bytes / message** |\n| Max `packet_id` | 63 (6-bit; wraps modulo 64) |\n| Frames per message | `1 + ceil(len/4)` (1–1024) |\n\nThe descriptor carries the **true byte length** (big-endian u16) so the receiver\ncan strip the zero-padding on the final data fragment. Messages larger than 4092 B\nmust be split by the application (the `MORE` flag marks continuation chunks); the\nadapter never re-fragments beyond the 4092 B quantum.\n\n**Descriptor flags** (`payload[2]` of the descriptor) request delivery behaviour;\nthey do **not** change the L2 bytes:\n\n| Bit | Name | Meaning |\n|-----|------|---------|\n| 0 | `AGENT` (0x01) | Message is to/from an LLM agent (agent-to-agent traffic). |\n| 1 | `MORE` (0x02) | This message is one chunk of a longer logical message/event. |\n| 2 | `RELIABLE` (0x04) | Ask the receiver to ACK (the DCF-Text reliability layer). |\n\nRemaining bits reserved. Because the flags are opaque to L2, an `AGENT|RELIABLE`\nmessage and a bare message of the same text produce **byte-identical** framing —\nonly the flag byte and the delivery path differ.\n\n**Reassembly.** Buffer by modular `packet_id`; gather the descriptor and every data\nfragment `1..frag_total`; concatenate and truncate to `len`; decode UTF-8 and emit\n`(packet_id, timestamp_us, src, dst, text, flags)`. Duplicate fragments are ignored\n(idempotent — replaying a fragment never double-emits). Out-of-order delivery is\ntolerated. A `finalize` call reports every still-incomplete message as **lost**\n(ascending `packet_id`) and clears state. Invalid UTF-8 is decoded with replacement\nrather than dropped.\n\n## Relationship to DCF-Game\n\nText and game **share `DATA` (type 0)** and the same descriptor-then-data\nfragmentation scheme. There is **no in-band tag** that distinguishes a text `DATA`\nframe from a game one — they differ only by `seq` split (text 6+10, game 11+5),\nwhich is not self-describing. They are therefore separated by **deployment, not by\nthe bytes**: a node feeds a channel's frames to the reassembler for the one service\nit runs on that channel (a text node runs a `TextReassembler`; a game node runs a\n`GameReassembler`). Do not multiplex text and game on the same `dst` channel with a\nsingle node expecting to demultiplex them in-band. Audio is unambiguous against both\n(it rides `CTRL`).\n\n## Addressing & channel rendezvous\n\nText frames ride the existing DCF transport. Peers rendezvous on a **channel** (the\n`DeModFrame` `dst_id`): `dst_id = 0xFFFF` is broadcast; otherwise the channel is\nderived from a shared passphrase via the certified CRC-16\n(`channel_id(name) = crc16(name)`, the same hash the rest of the repo uses). A node\naccepts a frame iff `dst_id == my_channel` or `dst_id == 0xFFFF`, and multiplexes\ncorrespondents by `src_id`.\n\n## Certification\n\nMirrors the wire quantum and DCF-Game: a Python reference emits golden vectors; the\nports diff against them byte-for-byte.\n\n```sh\n# regenerate + verify the laws, then diff against the committed vectors\npython3 python/MCP/gen_text_vectors.py /tmp/text_vectors.json\ndiff /tmp/text_vectors.json Documentation/text_vectors.json # must be empty\ndiff /tmp/text_vectors.json python/MCP/text_vectors.json # must be empty\n\ncd codec && cargo test --test certify_text # Rust\ngcc -std=c11 -I codec C_SDK/tests/test_text_certify.c -lm -o /tmp/tc && /tmp/tc # C\ncd go && go test ./text/ # Go\n```\n\n`Documentation/text_vectors.json` (with an identical `python/MCP/` copy) holds **9\nframing + 4 reassembly** cases (the reassembly cases exercise in-order, reordered,\nduplicate, and fragment-drop-lost delivery); `codec/text_vectors.gen.h` is the\ndependency-free C vector dump. CI runs the Python/C/Rust certs and diffs regenerated\nvs. committed vectors (`.github/workflows/wire-certify.yml`, job `certify-text`);\nthe Go cert runs under `certify-go`.\n\n**Anchor.** `exampleTextMessage`: `text = \"agent ⇄ agent 🚀\"` (20 UTF-8 bytes),\n`packet_id = 10`, `ts_us = 66051`, `src = 0x00A1`, `dst = 0xEED7`, `flags = 0x05`\n→ 1 descriptor + 5 data frames, first frame\n`d310280000a1eed7001405000102035490`.\n\n## Reference implementations\n\n| Lang | File | Entry points |\n|------|------|--------------|\n| C | `codec/demod_text.h` | `dcf_text_packetize` / `dcf_text_reasm_push`, `dcf_text_channel_id` |\n| Rust | `codec/src/text.rs` | `packetize` / `TextReassembler`, `channel_id` |\n| Python | `python/MCP/textlab_core.py` | `packetize` / `TextReassembler`, `channel_id` |\n| Go | `go/text/text.go` | `Packetize` / `TextReassembler`, `ChannelID` |\n\n`python/MCP/textlab_core.py` is the canonical source of truth (the generator\n`gen_text_vectors.py` emits the laws + vectors from it). An additional interoperable\nNode.js port — `JS/nodejs/src/text.js`, exposed as `DcfTextNode` in\n`JS/nodejs/src/node.js` — speaks the same on-air dialect.\n\n## Consumers\n\n- **Agent-to-agent (matrix-bridge).** The `a2a` tooling and `DcfTextNode`\n (`matrix-bridge/dcf_node.py`, with `matrix-bridge/dcf_text.py` re-exporting the\n canonical `textlab_core`) let two MCP-capable agents talk directly over the mesh —\n no Matrix server, no human relay (`matrix-bridge/AGENT_TO_AGENT.md`). The `AGENT`\n flag marks that traffic.\n- **Node.js `DcfTextNode`** — a stdlib-`dgram` text node speaking the same dialect\n as the Python node.\n\n## Theorem\n\nL2 framing is a fixed bit-placement adapter over the certified `DeModFrame`.\nMatching the framing + reassembly vectors pins the C, Rust, and Go implementations\nto the Python reference on the entire input space. The descriptor flags are opaque\nto L2, so the framing vectors are invariant to the agent/more/reliable choice —\nadding flags or transports never re-opens the certificate, and the 246-vector wire\ncertificate is untouched.\n\n## Security & export posture\n\nLike the rest of DCF, the text wire is **plaintext** — message contents, `src`/`dst`,\nand timing are fully readable by any on-path observer (the protocol is deliberately\nencryption-free for export compliance). Deploy behind WireGuard or operator-supplied,\nexport-compliant crypto **beneath** the UDP socket; never add encryption to the codec.\nSee [`DCF_SECURITY_EXPOSURE.md`](DCF_SECURITY_EXPOSURE.md).\n\n\nGolden vectors (sample):\n{\n \"encode_basis\": [],\n \"syndrome_basis\": []\n}\n\nProvide the complete implementation with encode/decode functions."}, {"role": "assistant", "content": "# SPDX-License-Identifier: LGPL-3.0-only\n\"\"\"DCF TextLab core — reference for the DCF-Text L2 framing (the certified layer).\n\nDCF-Text is an *adapter* over the 17-byte DeModFrame wire quantum, exactly like\nDCF-Audio and DCF-Game: one UTF-8 message is fragmented into ordinary DeModFrame\nDATA frames whose 4-byte payloads carry the bytes, with the fragment bookkeeping\npacked into the `seq` field. The frames are byte-for-byte valid DeModFrames\n(sync 0xD3, version nibble 1, CRC-16/CCITT), so they satisfy the same wire\ninvariant the 246-vector certificate pins — this module reuses the certified codec\nin ``wirelab_core.py`` so nothing about the quantum is reinvented here, and the L2\nbytes are pinned cross-language by ``Documentation/text_vectors.json``.\n\nWhy a separate adapter from audio/game:\n * Audio rides on CTRL frames (type 3). Text and game ride on DATA frames\n (type 0); a text fragment is told apart from a game fragment by its seq split\n (text uses a 10-bit fragment index, game a 5-bit one) — the descriptor that\n starts every message disambiguates which adapter owns the packet_id.\n * Chat/agent messages are larger and lower-rate than 20 ms audio blocks, so the\n seq split gives the fragment index more room (10 bits) than audio's 5.\n\nL2 framing (all frames version=1, type=DATA(0), big-endian; see WIRE_QUANTUM_SPEC.md):\n seq (u16) = packet_id[15:10] (6 bits, 0..63) | frag_idx[9:0] (10 bits, 0..1023)\n frag_idx == 0 descriptor : payload = [len_hi, len_lo, flags, reserved]\n frag_idx 1..N data : payload = utf8[(k-1)*4 .. +4], last frame zero-padded\n frag_total = ceil(len / 4) (<= 1023 => len <= 4092 bytes / message)\n src = local node id (u16)\n dst = rendezvous channel (u16): 0xFFFF broadcast, else crc16(passphrase)\n ts_us = 24-bit microsecond timestamp, identical across a message's frames\n\nThis is the canonical source of truth; ``matrix-bridge/dcf_text.py`` re-exports it.\n\"\"\"\nimport os\nimport sys\n\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\nfrom wirelab_core import encode, decode, crc16_ccitt # certified DeModFrame codec\n\n# ── L2 framing constants ──────────────────────────────────────────────────────\nFDATA = 0 # frame type carrying fragmented chat text\nFRAG_BITS = 10 # low bits of seq hold the fragment index\nFRAG_MASK = (1 << FRAG_BITS) - 1 # 0x3FF\nMAX_FRAGS = FRAG_MASK # 1023 data fragments\nMAX_PAYLOAD = MAX_FRAGS * 4 # 4092 bytes / message\nMAX_PACKET_ID = (1 << (16 - FRAG_BITS)) - 1 # 63\n\nBROADCAST = 0xFFFF # dst meaning \"every node on the wire\"\n\n# Descriptor flags — opaque to L2 (they do not change the framing bytes).\nFLAG_AGENT = 0x01 # message is addressed to / from the LLM agent\nFLAG_MORE = 0x02 # this message is one chunk of a longer Matrix event\nFLAG_RELIABLE = 0x04 # ask the receiver to ACK (DCF-Text reliability layer)\n\n\ndef _ceil_div(a, b):\n return (a + b - 1) // b\n\n\ndef channel_id(name):\n \"\"\"Map a human channel/passphrase to a 16-bit rendezvous ``dst`` (the same\n frequency-channel trick the rest of the repo uses). ``None``/\"\" => broadcast.\"\"\"\n if not name:\n return BROADCAST\n return crc16_ccitt(name.encode(\"utf-8\"))\n\n\ndef packetize(text, packet_id, ts_us, src, dst, flags=0):\n \"\"\"Serialise one UTF-8 message (<= 4092 bytes) into a list of 17-byte\n DeModFrame DATA frames. Returns [descriptor_frame, data_frame, ...].\"\"\"\n payload = text.encode(\"utf-8\") if isinstance(text, str) else bytes(text)\n if not (0 <= flags < 256):\n raise ValueError(\"flags must be u8\")\n if not (0 <= packet_id <= MAX_PACKET_ID):\n raise ValueError(f\"packet_id must be 0..{MAX_PACKET_ID}\")\n if len(payload) > MAX_PAYLOAD:\n raise ValueError(f\"message {len(payload)}B exceeds {MAX_PAYLOAD}B cap \"\n \"(split into multiple messages)\")\n\n frag_total = _ceil_div(len(payload), 4) # data fragments after descriptor\n frames = []\n\n # frag_idx 0 — descriptor (carries the true byte length so we can unpad)\n desc_seq = (packet_id << FRAG_BITS) | 0\n desc_payload = bytes([(len(payload) >> 8) & 0xFF, len(payload) & 0xFF, flags, 0])\n frames.append(encode(FDATA, desc_seq, src, dst, desc_payload, ts_us))\n\n # frag_idx 1..frag_total — data, last chunk zero-padded to 4 bytes\n for k in range(1, frag_total + 1):\n chunk = payload[(k - 1) * 4:(k - 1) * 4 + 4]\n chunk = chunk + bytes(4 - len(chunk))\n seq = (packet_id << FRAG_BITS) | k\n frames.append(encode(FDATA, seq, src, dst, chunk, ts_us))\n\n return frames\n\n\nclass TextReassembler:\n \"\"\"Stateful reassembler. push() emits a completed message as soon as its\n descriptor and every data fragment have arrived; duplicates are ignored;\n finalize() reports any message still incomplete as lost.\n\n Emitted message event: (\"message\", packet_id, ts_us, src, dst, text, flags)\n Lost event (finalize): (\"lost\", packet_id)\n \"\"\"\n\n def __init__(self, accept_dst=None):\n # accept_dst: None => accept every channel; else only this dst + BROADCAST\n self._accept = accept_dst\n self._pkts = {} # packet_id -> dict(desc, ts, src, dst, frags{idx:4B})\n\n def push(self, frame):\n try:\n d = decode(frame)\n except ValueError:\n return []\n if d[\"frame_type\"] != FDATA:\n return []\n if self._accept is not None and d[\"dst\"] not in (self._accept, BROADCAST):\n return []\n seq = d[\"seq\"]\n packet_id = seq >> FRAG_BITS\n frag_idx = seq & FRAG_MASK\n body = bytes.fromhex(d[\"payload\"])\n\n entry = self._pkts.setdefault(packet_id, {\"desc\": None, \"ts\": d[\"ts_us\"],\n \"src\": d[\"src\"], \"dst\": d[\"dst\"],\n \"frags\": {}})\n entry[\"ts\"] = d[\"ts_us\"]\n entry[\"src\"] = d[\"src\"]\n entry[\"dst\"] = d[\"dst\"]\n if frag_idx == 0:\n if entry[\"desc\"] is None:\n length = (body[0] << 8) | body[1]\n entry[\"desc\"] = {\"len\": length, \"flags\": body[2],\n \"frag_total\": _ceil_div(length, 4)}\n elif frag_idx not in entry[\"frags\"]:\n entry[\"frags\"][frag_idx] = body\n\n return self._try_emit(packet_id)\n\n def _try_emit(self, packet_id):\n entry = self._pkts.get(packet_id)\n if entry is None or entry[\"desc\"] is None:\n return []\n desc = entry[\"desc\"]\n need = set(range(1, desc[\"frag_total\"] + 1))\n if not need.issubset(entry[\"frags\"].keys()):\n return []\n raw = b\"\".join(entry[\"frags\"][k] for k in range(1, desc[\"frag_total\"] + 1))\n payload = raw[:desc[\"len\"]]\n del self._pkts[packet_id]\n try:\n text = payload.decode(\"utf-8\")\n except UnicodeDecodeError:\n text = payload.decode(\"utf-8\", \"replace\")\n return [(\"message\", packet_id, entry[\"ts\"], entry[\"src\"], entry[\"dst\"],\n text, desc[\"flags\"])]\n\n def finalize(self):\n \"\"\"Report every still-incomplete message as lost (ascending packet_id),\n clearing state. Mirrors GameReassembler.finalize / the C and Rust refs.\"\"\"\n events = [(\"lost\", pid) for pid in sorted(self._pkts.keys())]\n self._pkts.clear()\n return events\n\n\n# ── self-test (round trips a few messages through encode -> wire -> decode) ────\ndef _selftest():\n cases = [\"hi\", \"DeModFrame over Matrix\",\n \"unicode ❤️ \\U0001f680\" * 5,\n \"x\" * MAX_PAYLOAD]\n for pid, msg in enumerate(cases):\n frames = packetize(msg, pid % (MAX_PACKET_ID + 1), 123456, src=7, dst=42)\n for f in frames:\n assert len(f) == 17 and f[0] == 0xD3 and (f[1] >> 4) == 1\n r = TextReassembler()\n out = []\n for f in frames: # in-order delivery\n out += r.push(f)\n assert len(out) == 1, f\"expected 1 message, got {len(out)}\"\n _, _, _, src, dst, text, _ = out[0]\n assert text == msg and src == 7 and dst == 42, (text, msg)\n # replaying any already-consumed data fragment must not double-emit the message\n assert r.push(frames[-1]) == []\n\n # out-of-order + duplicate delivery still reassembles exactly once\n frames = packetize(\"reorder me please\", 3, 1, src=1, dst=2)\n r = TextReassembler()\n out = []\n for f in [frames[2], frames[2], frames[0], frames[1]] + frames[3:]:\n out += r.push(f)\n assert len(out) == 1 and out[0][5] == \"reorder me please\"\n\n # a dropped data fragment leaves the message incomplete -> finalize reports it lost\n frames = packetize(\"dropme over the wire\", 9, 1, src=1, dst=2)\n r = TextReassembler()\n for j, f in enumerate(frames):\n if j == 2:\n continue\n assert r.push(f) == []\n assert r.finalize() == [(\"lost\", 9)]\n print(\"textlab_core selftest: CERTIFIED\")\n\n\nif __name__ == \"__main__\":\n _selftest()\n"}]}
{"messages": [{"role": "system", "content": "You are an expert DCF adapter implementer. You build protocols (audio, game, text, SSTV, snake, mesh) as adapters over the 17-byte DeModFrame wire quantum.\n\nADAPTER PATTERN:\n- One adapter message → 1 + ceil(payload_len/4) DeModFrame packets\n- seq = packet_id[15:5] | frag_idx[4:0] (for audio/game); text uses frag_idx[9:0]\n- frag_idx 0 = descriptor: [len, frag_total, codec_id|msg_type_id, flags]\n- Payload ≤ 124 B/block (4 bytes per frame × 31 frames max)\n- L2 framing is codec/message-type-agnostic and byte-certified\n- Adding new codecs/message types never changes the wire vectors\n\nCONVENTIONS:\n- Encode: serialize payload → fragment into ≤124B chunks → wrap each in DeModFrame\n- Decode: collect frames by packet_id → reassemble payload → validate CRC per frame\n- Use CTRL (type 3) frames for audio; DATA (type 0) frames for game/text/SSTV\n- Descriptor flags: bit0 RELIABLE, bit1 ORDERED, bit2 END_TICK (adapter-specific)\n"}, {"role": "user", "content": "Implement the DCF text adapter in Rust.\n\nWire quantum spec (for context):\n# DCF Wire Quantum — Canonical Specification\n\n**Version 1** · DeMoD LLC · This document is normative.\n\nThe DCF wire protocol has exactly **one** wire format: the 17-byte `DeModFrame`\n(version nibble = 1). All other formats (the proto-message UDP header, the C\nSDK's `DCFMessageHeader`) are **adapters** over this quantum and must be\nexplicitly documented as such.\n\n## Wire Layout\n\n```\n Byte Field Width Description\n ──── ────────── ───── ─────────────────────────────────────────\n 0 sync 1 B Fixed 0xD3 (first validity gate)\n 1 flags 1 B [7:4] version (currently 1) | [3:0] type (0–3)\n 2–3 seq 2 B Big-endian u16, rolling sequence counter\n 4–5 src_id 2 B Big-endian u16, source node ID\n 6–7 dst_id 2 B Big-endian u16, destination (0xFFFF = broadcast)\n 8–11 payload 4 B Application data\n 12–14 timestamp 3 B 24-bit µs offset, big-endian, wraps ~16.7 s\n 15–16 crc16 2 B CRC-16/CCITT-FALSE over bytes [0..14]\n```\n\n**Total: 17 bytes = 136 bits.** The free field data is 108 bits (4+16+16+16+32+24).\n\n## Frame Types\n\n| Value | Name | Purpose |\n|-------|--------|-----------------------------|\n| 0 | DATA | Application payload |\n| 1 | ACK | Acknowledgement |\n| 2 | BEACON | Clock sync / broadcast |\n| 3 | CTRL | Control / fragmented audio |\n\n## Validity\n\nA frame is valid iff:\n1. `frame[0] == 0xD3` (sync byte)\n2. `(frame[1] >> 4) == 1` (version nibble)\n3. `CRC-16/CCITT-FALSE(frame[0..14]) == (frame[15] << 8) | frame[16]` (CRC)\n\nFor a random byte stream, the probability of a 17-byte window passing all three\nchecks is 2⁻²⁸ (Theorem 2.1, `wire_quanta_category.md`).\n\n## CRC-16/CCITT-FALSE\n\n- Polynomial: `0x1021`\n- Init: `0xFFFF`\n- RefIn/RefOut: `false`\n- XorOut: `0x0000`\n- Check value: `CRC(\"123456789\") = 0x29B1`\n\n## Anchors\n\n| Quantity | Value | Source |\n|----------|-------|--------|\n| CRC(\"123456789\") | `0x29B1` |...\n\nAdapter specification:\n# DCF-Text — Chat / Agent-to-Agent Text over the DeModFrame Wire\n\n**Version 1** · DeMoD LLC · This document is normative. Companion to\n[`WIRE_QUANTUM_SPEC.md`](WIRE_QUANTUM_SPEC.md) and a sibling of\n[`DCF_GAME_SPEC.md`](DCF_GAME_SPEC.md) and [`DCF_AUDIO_SPEC.md`](DCF_AUDIO_SPEC.md).\n\nDCF-Text carries UTF-8 text — human chat and **agent-to-agent** messages — over the\nPunctim mesh. It introduces **no new wire format**: one message is an **adapter**\nover the 17-byte `DeModFrame` quantum, serialised into a short burst of ordinary\n`DATA` (type 0) frames. The framing is **content-agnostic and byte-deterministic\nacross C, Rust, Python, and Go**, pinned by a finite certificate exactly like the\nwire quantum itself.\n\nAudio rides `CTRL` (type 3); **text and game both ride `DATA` (type 0)** but split\nthe `seq` field differently — text gives the fragment index **10 bits** (messages\nare larger and lower-rate than 20 ms audio blocks or per-tick game state), game\ngives it 5. See [Relationship to DCF-Game](#relationship-to-dcf-game).\n\n## Layered model\n\n| Layer | What | Certified? |\n|-------|------|-----------|\n| **L1** message | One UTF-8 string (≤ 4092 B) plus opaque descriptor flags. | n/a (opaque app bytes) |\n| **L2** framing | `packetize` / reassemble: message bytes ⇄ `DeModFrame` `DATA` frames. | **Yes** — `text_vectors.json` |\n| **L3** delivery | Reassembly with dup-suppression + out-of-order tolerance; optional ARQ via the `RELIABLE` flag. | Runtime (unit-tested) |\n| **L4** transport + apps | Frames over the mesh; matrix-bridge agent-to-agent, `DcfTextNode`. | App |\n\nThe key invariant: **the descriptor flags are opaque to L2, so they never change\nthe L2 vectors** — the framing bytes are invariant to the agent/more/reliable\nchoice.\n\n## L2 framing (normative)\n\nOne message → `1 + frag_total` frames. Every frame is a fully valid `DeModFrame`\n(version = 1, type = `DATA` = 0) and still passes the 246-vector wire certificate.\n\n```\n seq (u16) = packet_id[15:10] (6 bits, 0..63) | frag_idx[9:0] (10 bits, 0..1023)\n frag_idx == 0 descriptor : payload = [len_hi, len_lo, flags, 0] (length big-endian u16)\n frag_idx 1..N data : payload = bytes[(k-1)*4 .. +4] (last frame zero-padded)\n frag_total = ceil(len / 4)\n timestamp_us = 24-bit send time, identical across a message's frames\n src_id = local node id ; dst_id = rendezvous channel (0xFFFF = broadcast)\n```\n\nBounds (from the 10-bit fragment field):\n\n| Quantity | Value |\n|----------|-------|\n| Max data fragments | 1023 |\n| **Max message payload** | **4092 bytes / message** |\n| Max `packet_id` | 63 (6-bit; wraps modulo 64) |\n| Frames per message | `1 + ceil(len/4)` (1–1024) |\n\nThe descriptor carries the **true byte length** (big-endian u16) so the receiver\ncan strip the zero-padding on the final data fragment. Messages larger than 4092 B\nmust be split by the application (the `MORE` flag marks continuation chunks); the\nadapter never re-fragments beyond the 4092 B quantum.\n\n**Descriptor flags** (`payload[2]` of the descriptor) request delivery behaviour;\nthey do **not** change the L2 bytes:\n\n| Bit | Name | Meaning |\n|-----|------|---------|\n| 0 | `AGENT` (0x01) | Message is to/from an LLM agent (agent-to-agent traffic). |\n| 1 | `MORE` (0x02) | This message is one chunk of a longer logical message/event. |\n| 2 | `RELIABLE` (0x04) | Ask the receiver to ACK (the DCF-Text reliability layer). |\n\nRemaining bits reserved. Because the flags are opaque to L2, an `AGENT|RELIABLE`\nmessage and a bare message of the same text produce **byte-identical** framing —\nonly the flag byte and the delivery path differ.\n\n**Reassembly.** Buffer by modular `packet_id`; gather the descriptor and every data\nfragment `1..frag_total`; concatenate and truncate to `len`; decode UTF-8 and emit\n`(packet_id, timestamp_us, src, dst, text, flags)`. Duplicate fragments are ignored\n(idempotent — replaying a fragment never double-emits). Out-of-order delivery is\ntolerated. A `finalize` call reports every still-incomplete message as **lost**\n(ascending `packet_id`) and clears state. Invalid UTF-8 is decoded with replacement\nrather than dropped.\n\n## Relationship to DCF-Game\n\nText and game **share `DATA` (type 0)** and the same descriptor-then-data\nfragmentation scheme. There is **no in-band tag** that distinguishes a text `DATA`\nframe from a game one — they differ only by `seq` split (text 6+10, game 11+5),\nwhich is not self-describing. They are therefore separated by **deployment, not by\nthe bytes**: a node feeds a channel's frames to the reassembler for the one service\nit runs on that channel (a text node runs a `TextReassembler`; a game node runs a\n`GameReassembler`). Do not multiplex text and game on the same `dst` channel with a\nsingle node expecting to demultiplex them in-band. Audio is unambiguous against both\n(it rides `CTRL`).\n\n## Addressing & channel rendezvous\n\nText frames ride the existing DCF transport. Peers rendezvous on a **channel** (the\n`DeModFrame` `dst_id`): `dst_id = 0xFFFF` is broadcast; otherwise the channel is\nderived from a shared passphrase via the certified CRC-16\n(`channel_id(name) = crc16(name)`, the same hash the rest of the repo uses). A node\naccepts a frame iff `dst_id == my_channel` or `dst_id == 0xFFFF`, and multiplexes\ncorrespondents by `src_id`.\n\n## Certification\n\nMirrors the wire quantum and DCF-Game: a Python reference emits golden vectors; the\nports diff against them byte-for-byte.\n\n```sh\n# regenerate + verify the laws, then diff against the committed vectors\npython3 python/MCP/gen_text_vectors.py /tmp/text_vectors.json\ndiff /tmp/text_vectors.json Documentation/text_vectors.json # must be empty\ndiff /tmp/text_vectors.json python/MCP/text_vectors.json # must be empty\n\ncd codec && cargo test --test certify_text # Rust\ngcc -std=c11 -I codec C_SDK/tests/test_text_certify.c -lm -o /tmp/tc && /tmp/tc # C\ncd go && go test ./text/ # Go\n```\n\n`Documentation/text_vectors.json` (with an identical `python/MCP/` copy) holds **9\nframing + 4 reassembly** cases (the reassembly cases exercise in-order, reordered,\nduplicate, and fragment-drop-lost delivery); `codec/text_vectors.gen.h` is the\ndependency-free C vector dump. CI runs the Python/C/Rust certs and diffs regenerated\nvs. committed vectors (`.github/workflows/wire-certify.yml`, job `certify-text`);\nthe Go cert runs under `certify-go`.\n\n**Anchor.** `exampleTextMessage`: `text = \"agent ⇄ agent 🚀\"` (20 UTF-8 bytes),\n`packet_id = 10`, `ts_us = 66051`, `src = 0x00A1`, `dst = 0xEED7`, `flags = 0x05`\n→ 1 descriptor + 5 data frames, first frame\n`d310280000a1eed7001405000102035490`.\n\n## Reference implementations\n\n| Lang | File | Entry points |\n|------|------|--------------|\n| C | `codec/demod_text.h` | `dcf_text_packetize` / `dcf_text_reasm_push`, `dcf_text_channel_id` |\n| Rust | `codec/src/text.rs` | `packetize` / `TextReassembler`, `channel_id` |\n| Python | `python/MCP/textlab_core.py` | `packetize` / `TextReassembler`, `channel_id` |\n| Go | `go/text/text.go` | `Packetize` / `TextReassembler`, `ChannelID` |\n\n`python/MCP/textlab_core.py` is the canonical source of truth (the generator\n`gen_text_vectors.py` emits the laws + vectors from it). An additional interoperable\nNode.js port — `JS/nodejs/src/text.js`, exposed as `DcfTextNode` in\n`JS/nodejs/src/node.js` — speaks the same on-air dialect.\n\n## Consumers\n\n- **Agent-to-agent (matrix-bridge).** The `a2a` tooling and `DcfTextNode`\n (`matrix-bridge/dcf_node.py`, with `matrix-bridge/dcf_text.py` re-exporting the\n canonical `textlab_core`) let two MCP-capable agents talk directly over the mesh —\n no Matrix server, no human relay (`matrix-bridge/AGENT_TO_AGENT.md`). The `AGENT`\n flag marks that traffic.\n- **Node.js `DcfTextNode`** — a stdlib-`dgram` text node speaking the same dialect\n as the Python node.\n\n## Theorem\n\nL2 framing is a fixed bit-placement adapter over the certified `DeModFrame`.\nMatching the framing + reassembly vectors pins the C, Rust, and Go implementations\nto the Python reference on the entire input space. The descriptor flags are opaque\nto L2, so the framing vectors are invariant to the agent/more/reliable choice —\nadding flags or transports never re-opens the certificate, and the 246-vector wire\ncertificate is untouched.\n\n## Security & export posture\n\nLike the rest of DCF, the text wire is **plaintext** — message contents, `src`/`dst`,\nand timing are fully readable by any on-path observer (the protocol is deliberately\nencryption-free for export compliance). Deploy behind WireGuard or operator-supplied,\nexport-compliant crypto **beneath** the UDP socket; never add encryption to the codec.\nSee [`DCF_SECURITY_EXPOSURE.md`](DCF_SECURITY_EXPOSURE.md).\n\n\nGolden vectors (sample):\n{\n \"encode_basis\": [],\n \"syndrome_basis\": []\n}\n\nProvide the complete implementation with encode/decode functions."}, {"role": "assistant", "content": "// SPDX-License-Identifier: LGPL-3.0-only\n//! DCF-Text: chat / agent-to-agent text transport over the DeModFrame wire.\n//!\n//! Text traffic is an *adapter* over the 17-byte DeModFrame quantum (`frame.rs`), not a\n//! new wire format — exactly like DCF-Audio (`audio.rs`) and DCF-Game (`game.rs`). One\n//! UTF-8 message is fragmented into `1 + frag_total` ordinary DATA frames whose 4-byte\n//! payloads carry the bytes. This L2 framing is byte-deterministic across C/Rust/Python —\n//! it is pinned by `Documentation/text_vectors.json`. See `Documentation/DCF_TEXT_SPEC.md`.\n//!\n//! Layout (all frames version=1, type=DATA(0), big-endian):\n//! seq = packet_id[15:10] (6 bits, 0..63) | frag_idx[9:0] (10 bits, 0..1023)\n//! frag_idx 0 descriptor : payload = [len_hi, len_lo, flags, 0]\n//! frag_idx k data : payload = bytes[(k-1)*4 .. +4] (last frame zero-padded)\n//! frag_total = ceil(len/4) (<= 1023 => len <= 4092 bytes / message)\n//!\n//! Audio uses CTRL(3); text and game ride DATA(0) but use different seq splits (text's\n//! 10-bit fragment index vs game's 5-bit), and the descriptor that opens every message\n//! tells a receiver which adapter owns the packet_id.\n\nuse crate::{crc16_ccitt, Frame, FrameType};\nuse std::collections::BTreeMap;\n\n// ── L2 constants ────────────────────────────────────────────────────────────\npub const FRAG_BITS: u16 = 10;\npub const FRAG_MASK: u16 = 0x3FF;\npub const MAX_FRAGS: usize = 1023;\npub const MAX_PAYLOAD: usize = MAX_FRAGS * 4; // 4092 bytes / message\npub const MAX_PACKET_ID: u16 = (1 << (16 - FRAG_BITS)) - 1; // 63\npub const BROADCAST: u16 = 0xFFFF;\n\n// Descriptor flag bits — opaque to L2 (they do not change the framing bytes).\npub const FLAG_AGENT: u8 = 0x01;\npub const FLAG_MORE: u8 = 0x02;\npub const FLAG_RELIABLE: u8 = 0x04;\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum TextError {\n /// Message exceeds the 4092-byte cap.\n PayloadTooLarge,\n /// packet_id exceeds the 6-bit field.\n BadPacketId,\n}\n\n/// Map a human channel/passphrase to a 16-bit rendezvous `dst` (the same frequency-channel\n/// hash the rest of the repo uses). Empty => broadcast.\npub fn channel_id(name: &str) -> u16 {\n if name.is_empty() {\n BROADCAST\n } else {\n crc16_ccitt(name.as_bytes())\n }\n}\n\n// ── L2: packetize ───────────────────────────────────────────────────────────\n/// Serialise one UTF-8 message (its raw bytes) into DeModFrame DATA frames (descriptor\n/// first, then data fragments in order). Each frame is a fully valid 17-byte DeModFrame.\npub fn packetize(\n payload: &[u8],\n packet_id: u16,\n ts_us: u32,\n src: u16,\n dst: u16,\n flags: u8,\n) -> Result<Vec<[u8; 17]>, TextError> {\n if payload.len() > MAX_PAYLOAD {\n return Err(TextError::PayloadTooLarge);\n }\n if packet_id > MAX_PACKET_ID {\n return Err(TextError::BadPacketId);\n }\n let frag_total = ((payload.len() + 3) / 4) as u16;\n let mut frames = Vec::with_capacity(1 + frag_total as usize);\n\n // frag_idx 0 — descriptor (2-byte big-endian length so the last fragment can be un-padded)\n let desc_seq = packet_id << FRAG_BITS;\n let len = payload.len() as u16;\n let desc = [(len >> 8) as u8, (len & 0xFF) as u8, flags, 0];\n frames.push(Frame::new(1, FrameType::Data, desc_seq, src, dst, desc, ts_us).encode());\n\n // frag_idx 1..frag_total — data, last chunk zero-padded\n for k in 1..=frag_total {\n let off = (k as usize - 1) * 4;\n let mut chunk = [0u8; 4];\n let end = (off + 4).min(payload.len());\n chunk[..end - off].copy_from_slice(&payload[off..end]);\n let seq = (packet_id << FRAG_BITS) | k;\n frames.push(Frame::new(1, FrameType::Data, seq, src, dst, chunk, ts_us).encode());\n }\n Ok(frames)\n}\n\n// ── L2: reassembler ─────────────────────────────────────────────────────────\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct TextPacket {\n pub packet_id: u16,\n pub ts_us: u32,\n pub src: u16,\n pub dst: u16,\n pub flags: u8,\n pub text: String,\n}\n\n#[derive(Default)]\nstruct Slot {\n desc: Option<(u16, u8)>, // (payload_len, flags)\n ts_us: u32,\n src: u16,\n dst: u16,\n frags: BTreeMap<u16, [u8; 4]>,\n}\n\n/// Stateful reassembler. `push` emits a completed message as soon as its descriptor and\n/// every data fragment have arrived; duplicates are ignored; `finalize` reports any\n/// still-incomplete message as lost. Mirrors the C and Python references. The reassembler\n/// is channel-agnostic — channel filtering, if any, is the caller's job (as in the SDK).\n#[derive(Default)]\npub struct TextReassembler {\n slots: BTreeMap<u16, Slot>,\n}\n\nimpl TextReassembler {\n pub fn new() -> Self {\n Self::default()\n }\n\n pub fn push(&mut self, frame: &[u8; 17]) -> Option<TextPacket> {\n let d = Frame::decode(&frame[..]).ok()?;\n if d.frame_type != FrameType::Data {\n return None;\n }\n let packet_id = d.seq >> FRAG_BITS;\n let frag_idx = d.seq & FRAG_MASK;\n let entry = self.slots.entry(packet_id).or_default();\n entry.ts_us = d.timestamp_us;\n entry.src = d.src_id;\n entry.dst = d.dst_id;\n if frag_idx == 0 {\n if entry.desc.is_none() {\n let len = ((d.payload[0] as u16) << 8) | d.payload[1] as u16;\n entry.desc = Some((len, d.payload[2]));\n }\n } else {\n entry.frags.entry(frag_idx).or_insert(d.payload);\n }\n self.try_emit(packet_id)\n }\n\n fn try_emit(&mut self, packet_id: u16) -> Option<TextPacket> {\n let entry = self.slots.get(&packet_id)?;\n let (len, flags) = entry.desc?;\n let frag_total = ((len as usize + 3) / 4) as u16;\n if (1..=frag_total).any(|k| !entry.frags.contains_key(&k)) {\n return None;\n }\n let mut raw = Vec::with_capacity(frag_total as usize * 4);\n for k in 1..=frag_total {\n raw.extend_from_slice(&entry.frags[&k]);\n }\n raw.truncate(len as usize);\n let (ts_us, src, dst) = (entry.ts_us, entry.src, entry.dst);\n self.slots.remove(&packet_id);\n Some(TextPacket {\n packet_id,\n ts_us,\n src,\n dst,\n flags,\n text: String::from_utf8_lossy(&raw).into_owned(),\n })\n }\n\n /// Report every still-incomplete message as lost (ascending packet_id), clearing state.\n pub fn finalize(&mut self) -> Vec<u16> {\n let lost: Vec<u16> = self.slots.keys().copied().collect(); // BTreeMap => sorted\n self.slots.clear();\n lost\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn packetize_reassemble_is_identity_under_reorder() {\n let msg = \"hello over DeModFrame — fragment me\"; // spans many fragments\n let frames = packetize(msg.as_bytes(), 5, 0x010203, 0x00A1, 0xFFFF, FLAG_AGENT).unwrap();\n assert_eq!(frames.len(), 1 + (msg.len() + 3) / 4);\n // feed in reverse order; reassembly must still recover the exact message\n let mut r = TextReassembler::new();\n let mut got = None;\n for f in frames.iter().rev() {\n if let Some(p) = r.push(f) {\n got = Some(p);\n }\n }\n let p = got.expect(\"message reassembles regardless of frame order\");\n assert_eq!(p.packet_id, 5);\n assert_eq!(p.src, 0x00A1);\n assert_eq!(p.flags, FLAG_AGENT);\n assert_eq!(p.text, msg);\n assert!(r.finalize().is_empty());\n }\n\n #[test]\n fn dropped_fragment_is_reported_lost() {\n let frames = packetize(b\"dropme over the wire\", 9, 0, 1, 2, 0).unwrap();\n let mut r = TextReassembler::new();\n for (i, f) in frames.iter().enumerate() {\n if i == 2 {\n continue; // drop one data fragment\n }\n assert!(r.push(f).is_none());\n }\n assert_eq!(r.finalize(), vec![9]); // incomplete => lost\n }\n\n #[test]\n fn empty_and_unicode_roundtrip() {\n for msg in [\"\", \"agent ⇄ agent 🚀\"] {\n let frames = packetize(msg.as_bytes(), 0, 0, 1, 2, 0).unwrap();\n let mut r = TextReassembler::new();\n let mut got = None;\n for f in &frames {\n if let Some(p) = r.push(f) {\n got = Some(p);\n }\n }\n assert_eq!(got.unwrap().text, msg);\n }\n assert_eq!(channel_id(\"123456789\"), 0x29B1);\n assert_eq!(channel_id(\"\"), BROADCAST);\n }\n}\n"}]}