Files
rutster/docs/DEVELOPMENT.md
adlee-was-taken 4c0898cd49 docs: QUICKSTART + DEVELOPMENT + CONTRIBUTING, polish README index
Builds out the user-facing docs tree alongside the slice-1 build target.
Kept the implementer's planned Task 7 'Slice 1 dev loop' README section
untouched — these docs are the canonical destination for that pointer.

- docs/QUICKSTART.md: 5-min path to 'hear the echo' (libopus install,
  cargo run, browser steps, troubleshooting, what's happening under the
  hood).
- docs/DEVELOPMENT.md: dev loop — workspace layout, per-crate iteration,
  running tests, the 20 ms loop / 'drop + observe' rule, slice-1
  boundaries (what NOT to add yet).
- CONTRIBUTING.md (at repo root, conventional): trunk-based dev,
  CI gates, commit message style, atomic commits, code style +
  learner-facing documentation policy, terminology policy, PR workflow
  + review checklist, GPL-3.0-or-later license.
- README.md: add a Quickstart pointer at the top, a Documentation table
  linking to every doc, and the slice-1 build-target status block.
2026-06-28 12:32:12 -04:00

7.2 KiB

Development

The dev loop: how to iterate on the Rust workspace, run tests, and find your way around the codebase.

Status: Slice 1 is the active build target. The workspace is landing task-by-task on the slice-1-webrtc-loopback branch. Once it merges to main, everything below applies directly. Until then, git checkout slice-1-webrtc-loopback to follow along. Implementation plan: docs/superpowers/plans/2026-06-28-slice-1-webrtc-loopback.md.


Workspace layout

Cargo workspace at the repo root, shaped to ADR-0002's fused per-call vertical. One binary crate + five library crates.

rutster/
├── Cargo.toml                 # [workspace] + [workspace.dependencies]
├── deny.toml                  # cargo-deny config (licenses/bans/sources)
├── rust-toolchain.toml         # pinned stable
├── LEARNING.md                # index of "to learn concept X, read file Y"
├── crates/
│   ├── rutster/               # binary: axum signaling server + media driver
│   │   ├── src/main.rs
│   │   ├── src/session_map.rs    # DashMap<ChannelId, RtcSession>
│   │   ├── src/routes.rs         # HTTP routes
│   │   └── static/index.html     # browser test client
│   ├── rutster-media/         # str0m WebRTC + Opus⇄PCM codec boundary
│   │   ├── src/pcm.rs            # PcmFrame, AudioSource/Sink traits, EchoAudioPipe
│   │   ├── src/opus_codec.rs     # OpusDecoder / OpusEncoder wrappers
│   │   ├── src/rtc_session.rs    # RtcSession (per-peer owner)
│   │   └── src/loop_driver.rs    # str0m poll loop
│   ├── rutster-call-model/    # the Channel/Leg object embryo
│   ├── rutster-signaling-sip/ # stub until spearhead step 5
│   ├── rutster-tap/           # stub until spearhead step 2
│   └── rutster-spend/         # stub until spearhead step 6
└── fuzz/                      # placeholder cargo-fuzz dir (real harnesses: step 5)

The three stub crates (rutster-signaling-sip, rutster-tap, rutster-spend) exist to lock the ADR-0002 boundary shape without anticipating code. Each is a lib.rs with a //! module doc comment describing what will land there and when. Don't fill them in early — see AGENTS.md "Slice-1 boundaries — what NOT to add."

Dependency direction

  • rutster (binary) → rutster-media, rutster-call-model
  • rutster-mediarutster-call-model
  • rutster-call-model is a leaf (depends on nothing in the workspace)
  • The three stubs depend on nothing in slice 1

Build / test / lint commands

The four CI gates — all must pass before merge. Run them locally before pushing:

cargo fmt --check                  # formatting check
cargo clippy -- -D warnings       # lints (warnings = failures)
cargo test --all                   # all unit + integration tests
cargo deny check                    # licenses, advisories, bans, sources

To auto-fix formatting + lint warnings:

cargo fmt                # apply formatting
cargo clippy --fix       # apply lint suggestions
cargo clippy --fix --all-features   # apply across all features

Per-crate iteration

When working on one crate, skip the rest for faster cycles:

cargo test -p rutster-media                  # one crate's tests
cargo test -p rutster-media -- --nocapture   # see println! output
cargo run -p rutster                         # run the binary
cargo build -p rutster-call-model             # type-check one crate
cargo doc -p rutster-media --open             # render one crate's docs

Render full API docs

cargo doc --no-deps --open

Every module has //! module docs explaining what it does + why it exists in the architecture. cargo doc is genuinely useful here, not just ceremony — the user is learning Rust from this codebase and slice 1 carries thorough educational comments per spec §7.


Running the binary

cargo run -p rutster
# listening on http://0.0.0.0:8080

Open http://localhost:8080/ in a browser → click "Start call" → grant mic → hear yourself echo. See QUICKSTART.md for the full walkthrough + troubleshooting.

Verbose tracing:

RUST_LOG=rutster=debug cargo run -p rutster

Filter to one module:

RUST_LOG=rutster_media::loop_driver=trace,rutster=info cargo run -p rutster

The 20 ms media loop & the "drop + observe" rule

The hot path is the str0m poll loop in crates/rutster-media/src/loop_driver.rs. It runs every ~10 ms (tokio interval, slice-1 deviation per spec §3.4 — step 4 lands a dedicated timing thread).

Error policy on the hot path: never ?-propagate. Match-and-continue. A dropped packet must not terminate the peer. Policy: "drop + observe (log + counter), don't crash." The eventual fuzz harness (step 5) will test against this exact posture.

Cold path (signaling, setup, request handlers) uses thiserror-derived error enums + ? propagation, converted to HTTP status codes at the axum boundary.

See AGENTS.md "Error handling" for the full policy.


Shipped vs. deferred

Slice 1 proves the media core only: WebRTC termination + the Opus⇄PCM codec boundary. The tap, the brain, barge-in, the trunk, and the spend cap are all explicitly deferred — each lands in a subsequent spearhead step.

The full "what's deferred + when it returns" table is in docs/superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md §1.2.

The short version — don't add these in slice 1:

  • Dedicated timing thread (step 4)
  • TLS on the HTTP signaling surface (step 5)
  • Authn / authz / multi-tenancy (step 6)
  • Trickle ICE (when NATs demand it)
  • The tap itself (step 2 — slice 1 only pre-paves the seam)
  • The brain / STT / LLM / TTS (step 3)
  • Barge-in / VAD (step 4)
  • PSTN trunk / SIP (step 5)
  • Spend cap (step 6)
  • Event bus / Valkey / CDR emission (step 5)
  • Transfer / park / pickup / barge features (escalation rung 2)
  • Browser automation e2e tests (post-slice-1)
  • Docker / compose (later rung)

If you find yourself reaching for any of these, the right answer is "no, see slice-1 spec §1.2."


Dev loop gotchas

  • libopus system dependency. The opus crate links system libopus via FFI. If you see error: linking with cc failed: exit code, install libopus-dev (Debian/Ubuntu) / opus-devel (Fedora) / brew install opus (macOS). See QUICKSTART.md for platform-specific commands.
  • cargo deny first-time setup. Run cargo install cargo-deny --locked once. cargo deny check then validates licenses, advisories, and duplicate-version bans.
  • Editor IDE. rust-analyzer is recommended — it's what the plan was written against. Open the repo root in your editor; rust-analyzer picks up rust-toolchain.toml and the workspace manifest automatically.
  • Slow first build. str0m + axum + tokio compile fresh on first build (~2 minutes). Incremental builds are fast. Use cargo check -p <crate> instead of cargo build for type-check-only.