From f765fc2b2702ef3233a3279370ef1200349fd512 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sun, 28 Jun 2026 13:06:37 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20LEARNING.md=20+=20fuzz/=20placeholder?= =?UTF-8?q?=20+=20README=20dev-loop=20(spec=20=C2=A77,=20=C2=A76.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LEARNING.md indexes ten concept-to-file pointers (the spec floor was five) — the newtype pattern, exhaustive enum match, sans-IO, trait extension seams, thiserror + hot-path match-and-continue, Arc vs Arc, DashMap, str0m's single-mutation invariant, graceful shutdown, include_str!. fuzz/README.md pre-paves the layout (no hostile-bytes surface in slice 1; harnesses land at step 5 per the out-of-scope table). README's new dev-loop section documents the libopus FFI prerequisite and the manual e2e steps. --- LEARNING.md | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 42 ++++++++++++++++++++++++++++ fuzz/README.md | 21 ++++++++++++++ 3 files changed, 137 insertions(+) create mode 100644 LEARNING.md create mode 100644 fuzz/README.md diff --git a/LEARNING.md b/LEARNING.md new file mode 100644 index 0000000..4f53050 --- /dev/null +++ b/LEARNING.md @@ -0,0 +1,74 @@ +# LEARNING.md — to learn concept X, read file Y + +This index maps a Rust concept you might be learning to the file where +slice 1 makes the concept concrete. Each entry is a worked example you +can read in `cargo doc --open` plus the source file itself. + +## Concepts + pointers + +- **Newtype pattern (type-safety via single-field wrappers)** → + `crates/rutster-call-model/src/lib.rs` — `ChannelId(Uuid)`. The newtype + stops us from mixing up a `ChannelId` with some future `SessionId` at + the type-system level. Compile-enforced where a comment could only ask. + +- **`enum` for closed state sets + exhaustive `match`** → + `crates/rutster-call-model/src/lib.rs` — `ChannelState` (New → + Connecting → Connected → Closing → Closed). Exhaustiveness checking + forces every `match` to consider each state; adding a state later + surfaces every site that needs handling. + +- **Sans-IO pattern (no I/O inside the library; input via method calls, + output via return values)** → `crates/rutster-media/src/loop_driver.rs` + — the str0m poll loop. `Rtc::handle_input` takes a network packet as a + struct argument, not from a socket the library owns; `poll_output` + returns `Transmit` packets the caller sends. Fully testable without a + network — str0m integration tests use this property to drive faster + than realtime. + +- **Trait design for extension points (a futures-compatible seam)** → + `crates/rutster-media/src/pcm.rs` — the `AudioSource` / `AudioSink` + traits. Slice 1 wires an `EchoAudioPipe` between them; step 2 swaps + that for a real WSS tap client without touching `RtcSession`. The + traits describe *what* the splice point does, not *how* it's filled. + +- **Error enums with `thiserror` + hot-path match-and-continue** → + `crates/rutster-media/src/lib.rs` (`MediaError`) and + `crates/rutster-media/src/opus_codec.rs` (`OpusDecoder::decode` returns + `Option`). Cold path: `thiserror`-derived enum + `?`. Hot + path: match-and-continue, never `?`, never panic — "drop + observe, + don't crash" (spec §3.8). + +- **`Arc>` vs `Arc>` — when each is right** → + `crates/rutster/src/session_map.rs`. The `RtcSession` lives behind + `Arc>` because every access mutates it (str0m's `&mut self` + contract) — `RwLock`'s read-mode would be useless. Comment on the + struct explains the trade-off. + +- **`DashMap` for sharded concurrent maps** → + `crates/rutster/src/session_map.rs`. `DashMap` shards its inner map so + two handlers operating on different `ChannelId`s don't contend; + `HashMap` wrapped in a single `Mutex` would serialize every access. + +- **str0m 0.21's single-mutation invariant** → + `crates/rutster-media/src/loop_driver.rs`. Mutate (handle_input / + Writer::write) → drain `poll_output` to `Output::Timeout` → next + mutate. Violating this leaves str0m in an inconsistent state. + +- **tokio graceful shutdown via signal handlers** → + `crates/rutster/src/main.rs` (`shutdown_signal`). Ctrl-C / SIGTERM + drops the AppState; the AppState drops the DashMap; the DashMap drops + every RtcSession. No in-flight call preservation in slice 1. + +- **`include_str!` for embedding static assets** → + `crates/rutster/src/routes.rs` (`include_str!("../static/index.html")`). + The HTML test client is compiled into the binary at build time — no + separate file to ship, no disk IO to serve it. + +## How to read + +1. `cargo doc --open` — every module has a `//!` doc comment; the doc + tree is the high-level map. +2. Pick a concept above; open the named file. The first occurrence of + each non-obvious pattern has a `//` comment explaining *why*. +3. Cross-ref back to the spec sections cited inline (`spec §3.8`, + `ADR-0002`, etc.) for the architecture-level rationale. diff --git a/README.md b/README.md index 1fdce9b..a6267dc 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,48 @@ or its architecture. > technical builder uses to stand up a contact center* — and re-aims it at a category AI is > actively disrupting, instead of a PBX category UCaaS already ate. +## Slice 1 dev loop (WebRTC media loopback) + +> Build prerequisite: install libopus (the `opus` crate links it via FFI): +> ```bash +> sudo apt-get install -y libopus-dev # Debian/Ubuntu +> # Fedora: sudo dnf install -y opus-devel +> # macOS: brew install opus +> ``` +> This is the one system dependency in slice 1. Opus is FFI per PORT_PLAN +> §7's "🦀 Core (FFI)" disposition — the codec surface Rust doesn't need +> to re-implement. + +Run the server: + +```bash +cargo run +# listening on http://0.0.0.0:8080 +``` + +Open a browser to `http://localhost:8080/`, click "Start call", grant +microphone permission. Speak — you should hear yourself back within +~200 ms (no perceptible delay). Click "Hang up" to tear down; server +logs `Closing → Closed`. + +Verbose tracing: + +```bash +RUST_LOG=rutster=debug cargo run +``` + +### Slice 1 "done" checklist (spec §6.5) + +On a clean checkout: +1. `cargo test --all` passes. +2. `cargo fmt --check` passes. +3. `cargo clippy -- -D warnings` passes. +4. `cargo deny check` passes. +5. `cargo run` + browser manual e2e: speak → hear echo within ~200 ms. +6. Hang-up button triggers `Closing → Closed` in server logs. +7. Every stub crate compiles; its doc-comment names its scheduled step. +8. `LEARNING.md` indexes at least 5 "to learn X, read Y" pointers. + ## Why it exists Asterisk won because contact centers were **built on it** (Vicidial, GOautodial, a thousand diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 0000000..ef3a0b2 --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,21 @@ +# fuzz/ — cargo-fuzz harness directory (placeholder) + +**Status:** placeholder. Not yet a cargo-fuzz project — just the directory. +Fuzz harnesses land at spearhead step 5 (PSTN trunk) alongside the +SIP/SDP/RTP wire parsers (PORT_PLAN §10 mandates continuous fuzzing of +every wire parser). Slice 1 has no hostile-bytes surface (the browser is +trusted), so no harnesses here yet — the `fuzz/` dir pre-paves the +layout. Populating this directory with a real `cargo-fuzz` project +(`fuzz/Cargo.toml` + `fuzz/fuzz_targets/*.rs`) happens at step 5. + +If you're at step 5, replace this README with that structure: +- `fuzz/Cargo.toml` — cargo-fuzz manifest. +- `fuzz/fuzz_targets/sip_parser.rs` — fuzz the SIP parser. +- `fuzz/fuzz_targets/sdp_parser.rs` — fuzz the SDP parser. +- `fuzz/fuzz_targets/rtp_packet.rs` — fuzz the RTP packet parser. +- CI job running a short fuzz burst on each PR (the cargo-fuzz integration + lands in `.github/workflows/` at that point). + +The hot-path "drop + observe, don't crash" policy (spec §3.8) is what the +future harnesses assert against: throw arbitrary bytes at the parser, +assert it returns an error or drops silently — never panics.