diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 22d23e24..66e63b39 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -50,25 +50,28 @@ allow).
manta/
├── Cargo.toml # workspace
├── crates/
-│ ├── manta-input # IQ sources: SoapySDR, KiwiSDR client, file, audio
+│ ├── manta-input # IQ sources: SoapySDR, HPSDR/Hermes, KiwiSDR client, file, audio
│ ├── manta-dsp # PFB channelizer, noise-floor estimation, envelope
│ ├── manta-decode # CW keying state machine, timing, Morse decode
│ ├── manta-spot # callsign validation, CQ/DE parse, dedupe, scoring
│ ├── manta-server # telnet cluster server + JSON/WebSocket stream
│ ├── manta-engine # orchestration: track lifecycle, decoder pool
│ ├── manta-testkit # synthetic CW generator, golden-IQ harness
-│ └── manta-cli # `manta` binary: daemon + subcommands
+│ ├── manta-cli # `manta` binary: daemon + subcommands
+│ └── manta-soak-harness # 24h soak measurement harness (ROADMAP M2 gate),
+│ # not shipped in the manta binary
```
Dependency graph (arrows = depends on):
```
manta-cli ──▶ manta-engine ──▶ manta-input ──▶ manta-dsp
- │ ├──▶ manta-dsp ──────▶ coppa-dsp
- │ ├──▶ manta-decode
- │ └──▶ manta-spot ──────▶ manta-decode
- └──▶ manta-server
+ │ │ ├──▶ manta-dsp ──────▶ coppa-dsp
+ │ │ ├──▶ manta-decode
+ │ │ └──▶ manta-spot ──────▶ manta-decode
+ └──────────────────▶ manta-server
manta-testkit ──▶ manta-dsp, manta-decode, coppa-channel
+manta-soak-harness ──▶ manta-dsp, manta-input, manta-engine, manta-testkit
```
M1 added `manta-input → manta-dsp` (the shared Hilbert transformer, used
@@ -318,7 +321,7 @@ validation (MAN-28). Dedupe (step 5) still applies.
own yet (decode-pipeline internals, not the network-facing surface
MAN-59 scoped to), and `manta --status` hitting a local control socket
for live stats is similarly not yet implemented. Prometheus text
- endpoint (feature `metrics`): input overruns, active tracks, evictions,
+ endpoint (compiled in unconditionally, no feature flag): input overruns, active tracks, evictions,
decode rate, spots/min, per-stage queue depths, spot confidence
histogram — also aspirational for several of these fields; the
currently-implemented subset is `manta_spots_total`,
diff --git a/CLAUDE.md b/CLAUDE.md
index a7661231..fe5f8093 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -25,7 +25,7 @@ reachable from this environment.
## Documents (read in this order)
- `README.md` — goals and non-goals
-- `ARCHITECTURE.md` — 8-crate workspace, data flow, channelizer/decoder/
+- `ARCHITECTURE.md` — 9-crate workspace, data flow, channelizer/decoder/
validation/output design
- `docs/SPEC-decode-core.md` — implementation-level algorithm spec: exact
channelizer constants, noise-floor estimator, track state machine, decoder
diff --git a/README.md b/README.md
index 286978ab..2edac56c 100644
--- a/README.md
+++ b/README.md
@@ -20,14 +20,22 @@
`manta` is a headless daemon written in Rust. It takes wideband IQ from a
-commodity SDR, a KiwiSDR over the network, or a WAV file, channelizes the
-whole passband with a polyphase filterbank, runs an independent CW decoder on
-every signal it finds, validates the callsigns, and emits spots. Output is the
-standard `DX de` cluster format over telnet plus a JSON Lines stream, so
-existing aggregators (including the Reverse Beacon Network) and modern
-consumers (such as [cqdx](https://cqdx.app)) can ingest it without changes.
-
-No GUI. CLI, a config file, and metrics.
+commodity SDR, an OpenHPSDR/Hermes device, a KiwiSDR over the network, or a
+WAV file, channelizes the whole passband with a polyphase filterbank, runs an
+independent CW decoder on every signal it finds, validates the callsigns, and
+emits spots. Output is the standard `DX de` cluster format over telnet plus a
+JSON Lines / WebSocket stream, so existing aggregators (including the Reverse
+Beacon Network) and modern consumers (such as [cqdx](https://cqdx.app)) can
+ingest it without changes.
+
+No GUI. CLI, a TOML config file, and Prometheus metrics.
+
+```console
+$ telnet manta.example.org 7300
+login: W1XYZ
+de W5AU-# >
+DX de W5AU-#: 14000.7 W1AW CW 7 dB 20 WPM CQ 1533Z
+```
## Why
@@ -40,81 +48,108 @@ with documented, testable algorithms.
## Installation
-No Rust toolchain, no cloning the source. Pick one:
+```sh
+cargo install --path crates/manta-cli --features hpsdr # Rust 1.85+
+```
-**Download a prebuilt binary** from the [Releases
-page](https://github.com/HagaleTechnologies/manta/releases) — macOS
-(Intel or Apple Silicon), Windows, or Linux (x86_64 or arm64, including
-Raspberry Pi OS 64-bit). Unpack the archive and run the `manta` binary
-inside it. **Linux binaries need `libasound2` installed** (audio input is
-an unconditional dependency, even if you only ever use file, KiwiSDR, or
-HPSDR input) — `sudo apt install libasound2` on Debian/Ubuntu/Raspberry Pi
-OS, or the equivalent ALSA runtime package on other distros; without it
-the binary fails to start.
+That puts a `manta` binary in Cargo's bin directory (`~/.cargo/bin`
+unless you moved `CARGO_HOME`), which a standard Rust install already has
+on `PATH` — every command below assumes a bare `manta` resolves. To build
+without installing, `cargo build --release -p manta-cli` leaves the
+binary at `target/release/manta`; run that path instead.
-**Or run the Docker image** (works anywhere Docker does, `linux/amd64` and
-`linux/arm64`):
+No tagged release yet, so there is no prebuilt binary or Docker image to
+pull — build from source for now. Both publish automatically, for every
+platform, from the first tag:
```sh
+# once a release exists:
docker run --rm ghcr.io/hagaletechnologies/manta:latest --help
```
-When running as a long-lived server (not `--help`), stop it with
-`docker stop -t 30 ` — Docker's own default 10-second grace
-period before SIGKILL is shorter than manta's supported drain window for
-a slow client's final write (up to 25s), so the default can cut a
-graceful shutdown off mid-drain.
-
-Both are built by [`.github/workflows/release-publish.yml`](.github/workflows/release-publish.yml)
-directly from each tagged release's commit — every published binary
-traces to a specific, auditable source revision. (If the image above
-returns an authorization error, the GHCR package needs its one-time
-"make public" step in GitHub's package settings after the first real
-release — see MAN-65. Windows binaries need the [Visual C++
-Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist)
-installed if it isn't already.)
+**Notes:**
+- Linux binaries need `libasound2` installed (audio input is an
+ unconditional dependency, even if you only ever use file, KiwiSDR, or
+ HPSDR input) — `sudo apt install libasound2` on Debian/Ubuntu/Raspberry
+ Pi OS, or the equivalent ALSA runtime package elsewhere.
+- Stop a long-running container with `docker stop -t 30 ` —
+ Docker's default 10-second grace period is shorter than manta's drain
+ window for a slow client's final write (up to 25 s), so the default
+ can cut a graceful shutdown off mid-drain.
+- Input backends are cargo features, and a build that did not ask for
+ one has no flags for it — `--hpsdr-host` / `--soapy-driver` fail with
+ `error: unexpected argument` on a build without them. `hpsdr`
+ (OpenHPSDR/Hermes) has no native dependency, which is why the install
+ line above turns it on. `soapy` (RTL-SDR, Airspy, SDRplay, HackRF via
+ SoapySDR) needs the native SoapySDR system library installed first;
+ once you have it, add it: `--features hpsdr,soapy`.
+- Windows binaries need the [Visual C++
+ Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist)
+ installed if it isn't already.
+
+## 60-second demo
+
+No SDR, no radio. Install, generate a synthetic golden vector, decode it:
-**Building from source** (if you're developing manta itself, or need a
-platform/feature combination the release matrix doesn't cover — the
-`soapy` feature below, for instance, isn't in the official release
-binaries since it needs the SoapySDR system library) still works exactly
-as before, and is what the rest of this Quickstart assumes:
-
-## Quickstart
+```sh
+cargo install --path crates/manta-cli --features hpsdr # puts `manta` on PATH
+manta gen v1 --out /tmp/v1 # 120 s of synthetic CW, 20 WPM, +20 dB
+manta decode /tmp/v1/v1.wav
+# CQ DE W1AW W1AW K CQ CQ DE W1AW W1AW K CQ CQ DE W1AW W1AW K CQ …
+```
-Requires Rust 1.85 or newer.
+Then point it at a real signal — a public KiwiSDR needs no hardware of
+your own:
```sh
-# Build
-cargo build --release -p manta-cli
+manta listen --kiwi-host kiwi.example.org --kiwi-freq 7030000
+```
-# Decode a synthetic golden vector from a file (deterministic, no hardware)
-manta gen v1 --out /tmp/v1
-manta decode /tmp/v1/v1.wav
+File replay (`listen --source`) currently accepts only 48 kHz mono audio
+and runs faster than realtime, so the hardware-free path above stops at
+`decode`; a paced replay that can drive the servers below is being
+worked on.
-# Copy live CW from a public KiwiSDR on 40 m
-manta listen --kiwi-host kiwi.example.org --kiwi-freq 7030000
+`manta --help` lists every subcommand and flag.
-# Copy from a local SDR via SoapySDR (build with --features soapy)
-manta listen --soapy-driver driver=rtlsdr --soapy-freq 7030000 --soapy-rate 240000
+## Run it as a node
-# Copy from the default audio input (rig audio passband, 48 kHz)
-manta listen
+One flag starts the DX cluster telnet server, the JSON/WebSocket stream,
+and the metrics endpoint alongside the decoder:
-# Any of the above as JSON Lines instead of text
-manta listen --json --kiwi-host kiwi.example.org --kiwi-freq 7030000
+```toml
+# server.toml
+[server]
+station_callsign = "W5AU" # your call; becomes `DX de W5AU-#:` and JSON `deCall`
+bind_addr = "127.0.0.1" # 0.0.0.0 to accept remote clients
+telnet_port = 7300
+json_port = 7301
+metrics_port = 7302
```
-`manta --help` lists every subcommand and flag.
+```sh
+manta listen --kiwi-host kiwi.example.org --kiwi-freq 7030000 --server-config server.toml
+telnet localhost 7300 # DX de … lines
+nc localhost 7301 # one JSON object per spot
+curl -s localhost:7302/metrics # Prometheus text
+```
+
+Forwarding to an upstream RBN-style collector is a `[[rbn_uplink]]`
+block. **Set `dry_run = true` first** — the default is `false`, so an
+uplink block starts transmitting as soon as you add it.
+
+Before exposing any port beyond loopback, read
+[docs/RUNBOOKS/network-exposure.md](docs/RUNBOOKS/network-exposure.md).
## Inputs
| Source | How | Status |
| --- | --- | --- |
-| IQ / audio WAV file | `decode`, `listen --source` | Working |
+| IQ / audio WAV file | `decode`, `listen --source` | Working (`decode` takes IQ; `listen --source` takes 48 kHz mono audio) |
| Sound card (rig audio passband) | `listen --device` | Working, 48 kHz input only |
| KiwiSDR over the network | `listen --kiwi-host` | Working |
-| RTL-SDR, Airspy, SDRplay, HackRF, and anything else SoapySDR drives | `listen --soapy-driver`, feature `soapy` | Working, needs hardware soak |
+| OpenHPSDR / Hermes (Hermes-Lite 2, Red Pitaya, QMTech) | `listen --hpsdr-host`, feature `hpsdr` — on in the install line above, no native dependency | Working; protocol verified against reference sources, not yet against hardware |
+| RTL-SDR, Airspy, SDRplay, HackRF, anything SoapySDR drives | `listen --soapy-driver`, feature `soapy` — **not** in the install line above; needs the SoapySDR system library, then `--features hpsdr,soapy` | Working, needs hardware soak |
Targets Linux (x86-64 and ARM, Raspberry Pi 4 class), macOS, and Windows.
The CPU budget is a full 192 kS/s passband inside one Raspberry Pi 4 core,
@@ -122,10 +157,27 @@ enforced by criterion benches.
## Outputs
-- Decoded text or JSON Lines on stdout today.
-- RBN-format `DX de` spots over the DX cluster telnet protocol (port 7300)
- and a JSON Lines / WebSocket stream (port 7301): in progress, see
- [ROADMAP.md](ROADMAP.md) milestone M3.
+All four ship today; `manta listen --server-config ` starts the
+first three together.
+
+- **DX cluster telnet server** (`:7300`) — standard login prompt and
+ RBN-format `DX de` lines, with enough command grammar (`sh/dx`,
+ `set/dx/filter`) for stock clients. This is the RBN/aggregator
+ compatibility surface.
+- **JSON Lines / WebSocket stream** (`:7301`) — one full-fidelity spot
+ object per line; a raw TCP client and a WebSocket client share the
+ port. This is the [cqdx](https://cqdx.app) ingest surface.
+- **Outbound RBN uplink** — forwards validated spots to an upstream
+ collector in the same `DX de` wire format, with reconnect and multiple
+ simultaneous targets. Working against a collector; **not yet validated
+ against RBN's live ingest**.
+- **Prometheus metrics** (`:7302/metrics`) — spot, client, uplink and
+ source-health counters. Some gauges are still placeholders;
+ ARCHITECTURE §8 says which.
+
+`decode` and `listen` also print decoded text or `--json` events on
+stdout. That output is a debugging aid, not a stable interface — the
+servers above are.
The decode path is deterministic: the same file in produces byte-identical
spot logs out. That is a hard requirement, and CI enforces it with golden
@@ -133,21 +185,24 @@ test vectors.
## Status
-Pre-1.0. What exists and what does not:
-
-- **Done:** single-signal decode from files and live audio (M1); the full
- wideband pipeline of polyphase channelizer, detector, track manager, and
- decoder pool, with SoapySDR and KiwiSDR inputs (M2 sub-projects); callsign
- validation, CQ/DE parsing, cty.dat and SCP cross-checks, dedupe, wired into
- the engine (M3, in part).
-- **Open acceptance gates:** the Raspberry Pi 4 CPU-budget measurement and a
- 24 h live-SDR soak both need physical hardware.
-- **Next:** the telnet and JSON spot servers, TOML config, metrics, and an RBN
- parity benchmark on recorded contest IQ.
-- **Known limits:** the classical decoder loses copy under heavy HF fading on
- a few golden vectors (issues #25 and #28). Closing that gap is the M4 ML
- fusion stage, gated on beating the classical baseline under simulated
- fading.
+Pre-1.0, and pre-first-release. What is true today:
+
+- **Shipped:** the full wideband pipeline — polyphase channelizer,
+ noise-floor detector, track manager, decoder pool — with five input
+ sources, callsign validation (cty.dat, SCP, CQ/DE parsing, dedupe), and
+ all four output surfaces above.
+- **Not yet measured:** the RBN parity benchmark (recall vs. RBN on
+ recorded contest IQ), the Raspberry Pi 4 CPU budget, and a 24-hour
+ live-SDR soak. The first needs reference data; the other two need
+ physical hardware. The CPU budget's desktop leg *has* been measured and
+ currently reads as a **fail** — ≈0.53x–0.58x realtime against a <0.5x
+ budget, pending a clean rerun on a quiet machine ([ROADMAP.md](ROADMAP.md) M2).
+- **Known limits:** the classical decoder loses copy under heavy HF
+ fading on several golden vectors, and at low SNR the validator still
+ admits occasional bogus callsigns from noise. Closing the fading gap is
+ classical-DSP work in flight, with ML fusion behind it at M4.
+- **No tagged release yet**, so the container image above is empty until
+ the first tag.
[ROADMAP.md](ROADMAP.md) has the milestone breakdown with acceptance
criteria.
@@ -173,16 +228,21 @@ criteria.
## Documentation
-- [ARCHITECTURE.md](ARCHITECTURE.md): the seven-crate workspace, data flow,
- and the channelizer, decoder, validation, and output design.
-- [docs/SPEC-decode-core.md](docs/SPEC-decode-core.md): the
- implementation-level algorithm spec. Channelizer constants, noise-floor
- estimator, track state machine, decoder equations, confidence formulas,
- determinism rules, golden vectors, and the config-key table.
-- [ROADMAP.md](ROADMAP.md): milestones M0 to M4 with acceptance criteria.
-- [docs/DECISIONS/](docs/DECISIONS/): dated design decisions and
- implementation pins.
-- [wiki/INDEX.md](wiki/INDEX.md): accumulated gotchas and cross-references.
+**Running a node:** [docs/RUNBOOKS/network-exposure.md](docs/RUNBOOKS/network-exposure.md)
+(exposing the servers safely) · the "Run it as a node" section above ·
+`manta --help` for every flag.
+
+**Contributing:** [ARCHITECTURE.md](ARCHITECTURE.md) — the nine-crate
+workspace, data flow, and the channelizer, decoder, validation and output
+design · [docs/DECISIONS/](docs/DECISIONS/) — dated design decisions and
+implementation pins · [wiki/INDEX.md](wiki/INDEX.md) — accumulated
+gotchas.
+
+**Algorithms and research:** [docs/SPEC-decode-core.md](docs/SPEC-decode-core.md) —
+channelizer constants, noise-floor estimator, track state machine,
+decoder equations, confidence formulas, determinism rules, golden
+vectors, config-key table · [ROADMAP.md](ROADMAP.md) — milestones M0 to
+M4 with acceptance criteria.
## Related projects
diff --git a/ROADMAP.md b/ROADMAP.md
index b0c8e3c9..a609048a 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -112,11 +112,18 @@ repetition gate, dedupe) is complete as a standalone crate -- see
-decode-core.md §7.1 (V11-V15). It is now wired into `manta-engine`'s
batch (`decode_samples`/`decode_wav`) and streaming (`listen`) pipelines,
both emitting real `Spot`s -- see
-`docs/superpowers/specs/2026-07-26-m3-engine-wiring-design.md`. Remaining
-M3 sub-projects: `manta-server` (telnet + JSON/WebSocket output, TOML
-config, metrics), and the RBN parity benchmark (needs ≥ 2 h of recorded
-contest-weekend IQ with RBN reference spots -- a data dependency not yet
-resolved).
+`docs/superpowers/specs/2026-07-26-m3-engine-wiring-design.md`.
+`manta-server` has also landed: the telnet cluster server, the
+JSON/WebSocket stream, TOML config, the metrics endpoint, and the
+outbound RBN uplink are all shipped and tested. Remaining M3 sub-projects:
+the RBN parity benchmark (needs ≥ 2 h of recorded contest-weekend IQ with
+RBN reference spots -- a data dependency not yet resolved) and the
+7-day unattended soak below.
+
+**RBN admission** is pre-1.0 work, not deferred: the near-term path is a
+Skimmer-Server-compatible handshake so existing aggregators can accept a
+manta node, with direct-uplink admission negotiated from a position of
+having live nodes.
## M4 — ML decoder stage (research-dependent)
@@ -135,6 +142,5 @@ confidence weighting. ONNX/candle inference, feature-gated.
- RTTY/FT4-adjacent modes on the same channelizer.
- Multi-SDR single-daemon orchestration.
-- Upstream conversation with RBN operators about accepting manta nodes.
- Spot quality feedback loop: cqdx-side confirmation (same call spotted by other
nodes) fed back to tune validation thresholds.
diff --git a/crates/manta-cli/tests/docs_consistency.rs b/crates/manta-cli/tests/docs_consistency.rs
new file mode 100644
index 00000000..3a5660c8
--- /dev/null
+++ b/crates/manta-cli/tests/docs_consistency.rs
@@ -0,0 +1,210 @@
+//! The public docs must match the shipped binary. MAN-145.
+//!
+//! These assertions exist because README.md, ARCHITECTURE.md, CLAUDE.md and
+//! wiki/pages/overview.md independently drifted to three different crate
+//! counts and a two-milestone-stale description of the output layer. Each
+//! test below pins one claim a first-time visitor reads.
+
+use std::fs;
+use std::path::{Path, PathBuf};
+
+fn repo_root() -> PathBuf {
+ Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("../..")
+ .canonicalize()
+ .expect("repo root")
+}
+
+fn doc(rel: &str) -> String {
+ let p = repo_root().join(rel);
+ fs::read_to_string(&p).unwrap_or_else(|e| panic!("reading {}: {e}", p.display()))
+}
+
+/// Every run of whitespace collapsed to a single space, so a phrase that a
+/// Markdown reflow split across two lines still matches.
+fn squash_whitespace(s: &str) -> String {
+ s.split_whitespace().collect::>().join(" ")
+}
+
+/// Text between a `## ` and the next `## ` heading.
+fn section<'a>(md: &'a str, heading: &str) -> &'a str {
+ let start = md
+ .find(&format!("\n## {heading}"))
+ .unwrap_or_else(|| panic!("no `## {heading}` section"));
+ let rest = &md[start + 1..];
+ let end = rest[3..].find("\n## ").map(|i| i + 4).unwrap_or(rest.len());
+ &rest[..end]
+}
+
+fn workspace_members() -> Vec {
+ let manifest: toml::Value = toml::from_str(&doc("Cargo.toml")).expect("parse Cargo.toml");
+ manifest["workspace"]["members"]
+ .as_array()
+ .expect("members array")
+ .iter()
+ .map(|m| {
+ m.as_str()
+ .expect("member string")
+ .rsplit('/')
+ .next()
+ .expect("crate name")
+ .to_string()
+ })
+ .collect()
+}
+
+// ---- Scenario 3: the crate count is accurate everywhere ----
+
+/// Every crate in the workspace must appear in ARCHITECTURE.md's tree.
+/// `manta-soak-harness` was missing for the whole of M2/M3.
+#[test]
+fn architecture_lists_every_workspace_crate() {
+ let arch = doc("ARCHITECTURE.md");
+ for krate in workspace_members() {
+ assert!(
+ arch.contains(&krate),
+ "ARCHITECTURE.md never mentions workspace member `{krate}`"
+ );
+ }
+}
+
+/// README, CLAUDE.md and the wiki overview must all state the same crate
+/// count, and it must be the real one. ARCHITECTURE.md is deliberately not
+/// in the list: it states no count in words, and is covered for crate
+/// *names* by `architecture_lists_every_workspace_crate` above. Guard-
+/// listing the stale spellings keeps the failure message actionable when a
+/// crate is added.
+#[test]
+fn every_doc_states_the_real_crate_count() {
+ let n = workspace_members().len();
+ assert_eq!(
+ n, 9,
+ "crate count changed to {n}: update the docs below and this test"
+ );
+
+ let stale = [
+ "seven-crate",
+ "eight-crate",
+ "8-crate",
+ "7-crate",
+ "six-crate",
+ ];
+ let fresh = ["nine-crate", "9-crate"];
+
+ for file in ["README.md", "CLAUDE.md", "wiki/pages/overview.md"] {
+ let text = doc(file);
+ for s in stale {
+ assert!(
+ !text.contains(s),
+ "{file} still says `{s}` (workspace has {n} crates)"
+ );
+ }
+ assert!(
+ fresh.iter().any(|f| text.contains(f)),
+ "{file} states no crate count; expected one of {fresh:?}"
+ );
+ }
+}
+
+// ---- Scenario 2: the Inputs table lists every shipped input ----
+
+/// Every input the CLI can be built with needs a row. HPSDR/Hermes shipped
+/// (crates/manta-input/src/hpsdr.rs, feature `hpsdr`, which the README's
+/// install line turns on) and had no row at all.
+#[test]
+fn readme_inputs_table_covers_every_shipped_input() {
+ let readme = doc("README.md");
+ let inputs = section(&readme, "Inputs");
+ for needle in [
+ "WAV file",
+ "--device",
+ "--kiwi-host",
+ "--soapy-driver",
+ "--hpsdr-host",
+ ] {
+ assert!(
+ inputs.contains(needle),
+ "README Inputs table has no row for `{needle}`"
+ );
+ }
+}
+
+// ---- Scenario 1: Outputs and Status describe shipped capability ----
+
+/// The telnet server, JSON/WebSocket stream, RBN uplink and Prometheus
+/// endpoint have all shipped and are covered by manta-server's test suite.
+#[test]
+fn readme_outputs_describes_shipped_servers() {
+ let readme = doc("README.md");
+ let outputs = section(&readme, "Outputs");
+ assert!(
+ !outputs.contains("in progress"),
+ "README Outputs still calls a shipped server \"in progress\""
+ );
+ for needle in ["7300", "7301", "7302", "uplink", "--server-config"] {
+ assert!(
+ outputs.contains(needle),
+ "README Outputs never mentions `{needle}`"
+ );
+ }
+}
+
+/// The Status block must not list shipped work as upcoming.
+#[test]
+fn readme_status_does_not_promise_shipped_work() {
+ let readme = doc("README.md");
+ let status = section(&readme, "Status");
+ for stale in ["the telnet and JSON spot servers", "TOML config, metrics"] {
+ assert!(
+ !status.contains(stale),
+ "README Status still lists `{stale}` as future work"
+ );
+ }
+}
+
+/// Every command in the Quickstart must run on a build the reader was told
+/// to make. `--soapy-driver` needs `--features soapy`, which is in neither
+/// the default build nor any release binary.
+#[test]
+fn readme_quickstart_only_uses_default_build_flags() {
+ let readme = doc("README.md");
+ let quickstart = section(&readme, "60-second demo");
+ for gated in ["--soapy-driver", "--soapy-freq", "--soapy-rate"] {
+ assert!(
+ !quickstart.contains(gated),
+ "60-second demo uses feature-gated flag `{gated}`; it fails with \
+ `error: unexpected argument` on a default or release build"
+ );
+ }
+}
+
+// ---- Scenario 4: ROADMAP's RBN-admission item ----
+
+/// RBN admission is active P0 work (MAN-40; decision D1, 2026-09-06), not a
+/// deferred post-1.0 idea.
+#[test]
+fn roadmap_does_not_defer_rbn_admission() {
+ let roadmap = doc("ROADMAP.md");
+ let post = section(&roadmap, "Post-1.0 candidates");
+ assert!(
+ !post.contains("RBN operators"),
+ "ROADMAP still defers RBN-operator admission to post-1.0"
+ );
+}
+
+/// M3's own prose must not list the shipped manta-server as remaining.
+#[test]
+fn roadmap_m3_does_not_list_shipped_server_as_remaining() {
+ let roadmap = doc("ROADMAP.md");
+ // Collapse runs of whitespace so the search survives a paragraph reflow:
+ // the phrase is line-wrapped in the source Markdown, and matching the raw
+ // text would panic on `.expect()` instead of reporting a real drift.
+ let m3 = squash_whitespace(section(&roadmap, "M3"));
+ let idx = m3
+ .find("Remaining M3 sub-projects")
+ .expect("M3 remaining-work sentence");
+ assert!(
+ !m3[idx..].contains("manta-server"),
+ "ROADMAP M3 still lists `manta-server` as a remaining sub-project"
+ );
+}
diff --git a/wiki/pages/overview.md b/wiki/pages/overview.md
index 27ea1dcd..f27b4e4d 100644
--- a/wiki/pages/overview.md
+++ b/wiki/pages/overview.md
@@ -17,9 +17,9 @@ links:
- decode-chain
- coppa-reuse
---
-manta is an open-source, cross-platform, wideband multi-signal CW skimmer (Rust) that consumes wideband IQ from commodity SDRs, decodes every CW signal in the passband concurrently, validates callsigns, and emits RBN-compatible spots — an open replacement for the single closed-source Windows program the Reverse Beacon Network depends on. It is **design-phase**: ARCHITECTURE, ROADMAP, and `docs/SPEC-decode-core.md` are frozen; no implementation has started. Read the specs first — the design decisions are already made.
+manta is an open-source, cross-platform, wideband multi-signal CW skimmer (Rust) that consumes wideband IQ from commodity SDRs, decodes every CW signal in the passband concurrently, validates callsigns, and emits RBN-compatible spots — an open replacement for the single closed-source Windows program the Reverse Beacon Network depends on. Implementation is well underway; see README.md's Status section for what has shipped. Read the specs first — the design decisions are already made.
-## Where things live (planned 8-crate workspace)
+## Where things live (nine-crate workspace)
The workspace layout and dependency graph are normative in ARCHITECTURE §2 — do not restate the crate table here; the pointers below map crates to wiki pages.
@@ -29,6 +29,7 @@ The workspace layout and dependency graph are normative in ARCHITECTURE §2 —
- `crates/manta-server/` — telnet cluster + JSON/WebSocket. See [[spot-output-contract]].
- `crates/manta-engine/` — track lifecycle, decoder pool orchestration.
- `crates/manta-input/`, `manta-testkit/`, `manta-cli/` — IQ sources, synthetic/golden harness, binary.
+- `crates/manta-soak-harness/` — 24h soak measurement harness (ROADMAP M2 gate), not shipped in the manta binary.
- `docs/SPEC-decode-core.md` — normative constants, equations, config keys, golden vectors. The wiki points here, never restates.
## Start here