The Three Axes.
How an open-source project travels from a business sentence to requirements, design, real users, support — and back into new requirements. MRRC Modern as the worked example, with SDD, Harness and FDE as the method, and evidence at every station.
Most engineering write-ups start halfway: a requirement appears, a design follows, tests pass. This one starts where projects actually start — with a business sentence a person said out loud — and follows MRRC Modern for its first 75 days (2026-07-06 → 2026-09-19): one remote-control server for amateur radio that grew from a single-radio experiment into a three-platform, ten-model-key product with a public support chain. The method on display is the same three axes used across this ecosystem — a living SDD (the contract), an engineering harness (the machinery that enforces it), and FDE (the field loops that keep correcting both). Every station below is anchored to a commit, an SDD row, a test, a release artifact, or a public answer card.
TL;DR — the journey in six findings
- FactOne sentence became a contract on day one. The Echo output — "a browser-based FT-710 remote control that replaces SCU-LAN10 with zero additional hardware, one USB cable" — drove SDD V1.0: all 15 chapters, written concurrently with the first commit.
- FactThe field wrote the roadmap. 12 git-verifiable FDE cycles in 16 days, each triggered by exactly one real field signal — 16 kHz TX crackling → AD-011; a ~20 Hz frequency drift → the
DN;ruling and constraint; filter races → stale-read discard. - FactKnowledge became machinery. 22 machine-readable constraints (block / warn / info) distilled from real incidents, checked before edits (exit code 2); the test suite grew 262 → 1,299;
release_checkholds offline release rules; PROJECT_MAP names the single source of truth per topic. - InferenceSupport became the sensor network. Built as four slices in three days: bundle → triage + answers → update check → board + unattended implement. Classification is the decision; the loop now ends at a reviewed branch, never at
main. - InferenceNew requirements arrive through the same door as old ones. Recording silence (field log), macOS "damaged" and silent RX (release week), PTT release latency (field feel) — each became an SDD row, a guard, a release, and a public answer card.
- ThesisThe method transfers; the counts do not. Definition before implementation, every incident becomes a rule, instrument the field, automate the doing and never the verdict. §07 turns this into eight rules and a three-stage adoption ladder.
One project, two ways the chain breaks
Business intent → requirements → design → field → support → new requirements. Every project walks it; the failures are what differ.
The chain looks linear on a slide and behaves like a loop in reality: a requirement is only real when a field signal confirms it mattered, and a field signal is only useful when it can change the design. When the loop closes, the project compounds. It breaks in two recognizable ways instead.
The vision never lands
A business sentence is repeated in pitch decks while requirements drift into feature lists. Nobody can point at the artifact where the sentence became a measurable obligation — so every argument restarts from opinion.
The field never returns
Iteration is fast, but what the field taught lives in chat logs and a maintainer's memory. The same incident is rediscovered each quarter; a version number tells you what shipped but not what it learned.
MRRC Modern was built to make both failures expensive to commit. It began on 2026-07-06 with the initial commit of a single-radio experiment (FT-710 over its own USB cable), and by 2026-09-19 had produced 435 commits, 67 SDD version rows, 22 architecture decisions, 22 machine-readable constraints, 37 changelog entries, releases from v1.0.0 to v1.18.1 across Windows, macOS and a Raspberry Pi image — and a public support chain with answers, a board and an unattended implementer. None of the numbers are the point; what follows is the machinery that produced them.
The three axes are one system. The SDD settles judgment into a contract; the harness makes forgetting structurally impossible; FDE keeps letting the field correct both. Remove any one and you can name the failure that follows — a beautiful document nobody obeys, a fast loop with no memory, or a contract that has stopped listening. It is the umbrella's thesis — execution can be delegated, judgment cannot — applied to a whole lifecycle; the chain that page promises (field incident → SDD ruling → constraint → blocked re-introduction) is exactly what §02–§05 walk (thesis).
The contract that outlives the conversation
A living design document in three harness levels: business (what), technical (how), product (deliverable).
2.1 · From a sentence to measurable requirements
The project's Echo phase produced one paragraph before any architecture:
"A browser-based FT-710 remote control that replaces SCU-LAN10 with zero additional hardware. One USB cable. Real-time waterfall via FT4222 SPI. Bidirectional 48 kHz Opus audio. Mobile-first UI. Four WebSocket channels (control, audio RX, audio TX, spectrum)."
Echo inputs were concrete: daily operation by the author across devices; an evaluation of the existing open-source FT-710 remote (F4HTB) that found no SPI scope, no bidirectional Opus, no mobile UI; the gaps documented against Hamlib; and — the unfair advantage — reusable patterns inherited from the older MRRC project (PTT safety, multi-instance architecture, the SDD template itself). The paragraph became the baseline: all 15 SDD chapters in one burst, including a numbered NFR table with targets and verification methods. Requirements in this project are written as obligations a field test can settle:
| Requirement | Target | Verification method |
|---|---|---|
| NFR-001 · RX audio latency (radio speaker → browser speaker) | < 500 ms end-to-end | Listening test |
| NFR-002 · Control response | UI command ack within 200 ms on LAN | WebSocket round-trip observation |
| NFR-008 · PTT response | < 100 ms from touch to TX command | Timing logs |
| SC8 · Success criterion | PTT cannot stick | Safety architecture + watchdog tests |
The pattern to copy is small and strict: a requirement without a target and a verification method is an opinion. "PTT cannot stick" survives because a chapter (SDD 15, PTT safety architecture) and a watchdog exist; it is not a slogan.
2.2 · From requirements to design decisions
Design decisions in this project are numbered, dated, and traceable: AD-001 … AD-022, one decision register. Every one of them has an Echo behind it, and several have field incidents behind them:
| Decision | What it settled | What forced it |
|---|---|---|
| AD-002 | Direct CAT via pyserial instead of Hamlib for the FT-710 path | Hamlib gap analysis; the in-cable protocol was simpler and gave real spectrum |
| AD-011 | 48 kHz codec domain with a frame-aligned 44.1 kHz bridge | Field signal: "16 kHz TX crackling" |
| AD-014 | DN; is never polled — it steps the VFO down ~20 Hz per call | A live frequency-drift incident |
| AD-017 | Recording writer is per-session and idempotently rebuilt; a full queue drops the oldest block | The 2026-09-12 silent-MP3 field report |
| AD-019 | Unverified radio models refuse transmit unless explicitly allowed | New model families arriving faster than hardware acceptance |
| AD-021 / AD-022 | Support bundle, triage and the provable update channel | Support itself: first-round emails only ever asked for evidence |
2.3 · The version row is the atom of learning
The SDD's version history is not a changelog duplicate; it is the project's learning ledger. One row per change episode: the field signal or requirement, the evidence, the decision, the boundary that remains. 67 rows in 75 days — some spanning a one-line fix, some an entire support chain. Row V2.45 reads like a field report with a conclusion: the log signature of the silent recording ("Recording writer is falling behind — dropping audio" appearing 6 ms after start, 950+ drops in 22 s), the ffmpeg measurement of the output (-91 dB = digital silence, duration correct), the root cause, the fix, the three regression tests, and the boundary — "long recordings and Raspberry Pi SD-card writes remain unverified".
Lifecycle states are explicit — planned, implemented, tested, bench-verified, field-verified, released, deferred, known-issue — and nothing advances by itself. A released subsystem cannot lend its maturity to another client or radio model; unverified Icom and Yaesu profiles ship behind a transmit gate and say so.
Constraints that don't need remembering
If a lesson only lives in a document, it will be skipped. The harness is what makes skipping it fail loudly.
3.1 · Two layers, one job: make failure visible
The harness has an outer layer (business, technical and product constraints — the three SDD groups) and an inner execution layer (human + agents, repository, tools, tests + review, deployment, field telemetry). This article is about the inner layer's sharpest instrument: the constraint registry.
3.2 · The constraint registry: 22 lessons as machinery
.agents/skills/sdd-guardian/harness/constraints.json holds 22 rules distilled from the SDD and from real incidents, each with a severity, a rationale, and match patterns. An agent (or human) asking for a brief gets them at session start; a pre-edit check blocks violations with exit code 2. The registry's origin stories are the point — these are not style-guide preferences:
| Constraint | Severity | What it encodes |
|---|---|---|
cat-no-dn | block | DN; steps the active VFO DOWN ~20 Hz per call — a live frequency-drift incident, now impossible to re-introduce by accident |
cat-direct-serial-io | block | Serial I/O only inside radio controllers — keeps the asyncio transport from being touched from everywhere |
audio-16k-rate | block | Audio rates are 44.1 kHz device / 48 kHz codec — never 16 kHz in the audio path (the crackling incident); 16 kHz is a storage domain |
poll-stale-guard | info | Poll loops discard stale in-flight reads (the "filter switch works 50% of the time" race) |
ptt-release-no-verify | info | TX0 is fire-and-forget — verify loops on release are how PTT gets stuck |
password-constant-time | warn | Password checks use hmac.compare_digest |
The conversion rule: an incident is only fixed when it becomes a guard. Repair is link one of three; the ruling in the SDD is link two; the machine-readable constraint that blocks the next attempt is link three. This is why the registry is small and boring — every row cost something real.
3.3 · Gates and single sources of truth
Around the registry sits the rest of the machinery: the unittest suite (262 tests at the 12th cycle in July → 1,299 by the FDE-board commits in September), offline release_check rules (reported as 29 checks / 0 failing on release rows), a release-artifacts registry holding per-file version, size and SHA-256, and tests that enforce document consistency — the SDD version in the version-history first row must match the README, generated pages and landing page. For humans, docs/PROJECT_MAP.md answers one question — "if I change X, what must move with it?" — with a table per topic:
| Topic | Single source of truth | Enforced by |
|---|---|---|
| App version | CHANGELOG.md top entry | Build scripts parse it; release_check |
| SDD version | SDD/14-version-history.md first row | test_sdd_docs_consistency.py |
| Design decisions | SDD/08-architecture-decisions.md (AD-001…AD-022) | release_check AD index + the guardian registry |
| Engineering constraints | constraints.json | sdd_context.py check — block violations exit 2 |
| Release artifacts | release-artifacts.json (version / size / SHA-256) | release_check.py + artifact tests |
You can hand a repository to an agent only if the repository can refuse. The registry turns the project's scar tissue into pre-edit interception — which is the same design view as the product's transmit gate: authority is bounded, refusals are explicit, and every refusal cites where it came from.
Echo → Delta → Product, and the fourth practice
The lineage's field loops: deploy where reality is, prove the riskiest assumption, abstract what survived — then let support run the same loop on the product itself.
4.1 · Twelve cycles, one field signal each
The first era is documented as a git-verified cycle map. Each row is one Echo (a field signal), one Delta (the riskiest proof), one Product output (an abstraction that stays). A sample:
| Cycle | Field signal (Echo) | What got built (Delta → Product) |
|---|---|---|
| V1.0 · Jul 6 | Replace SCU-LAN10, one USB cable, no extra hardware | Full 15-chapter SDD baseline + 7 core modules, in one day |
| V1.1 · Jul 6 | "16 kHz TX crackling" | AD-011: 48 kHz codec domain + 44.1 kHz frame-aligned bridge |
| V1.2 · Jul 8 | "Frequency drifts ~20 Hz; TX meters blank" | Active-VFO tracking, meter calibration — and the ruling that became cat-no-dn |
| V1.7 · Jul 19 | "Filter switch works 50% of the time" | Stale-read discard after each poll query + 3 regression tests |
| V1.9 · Jul 21 | "USB reconnect → waterfall frozen forever" | on_reconnected hook re-runs scope init after reconnect |
| V2.1 · Jul 22 | "PTT keys but no voice / no RF power" | TX uplink ownership promotion + per-session TX counters + 5 regression tests |
Four patterns made this era work, and all four are copyable: one cycle = one real signal (no speculative features); documentation is concurrent, not retrospective (the version-history row lands with the fix); the riskiest assumption gets the prototype (direct CAT first, 48 kHz pipeline second, multi-client ownership later); and start from leverage (six assets inherited from the older project meant cycle one began at "integrate", not "invent").
4.2 · Product means abstraction — and then a platform
The Product phase is where field hacks become assets other work can stand on: a standalone scope subprocess with a length-prefixed pipe (byte-wise resync on the FT4222 was proven impossible), a frame-aligned 44.1↔48 kHz resampler, adaptive polling with skip-on-command and stale-read discard, a tagged dual-codec audio frame, and per-PTT TX session logging. The decisive abstraction arrived in August: a pluggable radio backend. The server, polling and state layers became radio-agnostic; radio-specific behaviour moved behind profiles. That single move turned the FT-710 solution into a platform — five Icom models via a shared CI-V core and four Yaesu models via a shared ASCII-CAT core, all built on the field-proven FT-710 transport, arriving behind a transmit gate until hardware acceptance catches up.
4.3 · Practice 4: the support loop
The fourth loop practice turns the same Echo→Delta→Product discipline on the product's own support. It shipped as four slices in three days: (1) the redacted diagnostics bundle; (2) the triage autopilot and the public answers page; (3) the read-only update check with a generated manifest; (4) the public field board and the unattended implementer. The rule that organizes it: classification is the decision — noise closes as answered, bugs auto-schedule into the implementation queue, features wait for the operator, and an un-answered report never closes. The implementer runs single-flight from a clean main worktree: a read-only model proposes a unified-diff JSON contract; guards refuse protected paths and oversized patches; the patch lands only on an isolated fde/<id> branch and commits only if the full test suite is green. main is never touched by an unattended process; the merge stays human. The mechanism is specified in the mechanism volume, §Field Loop; the journal of its first week is in The Support Loop.
Two threads, every station
A field incident is only interesting if you can watch it travel. Two real ones, from symptom to shipped fix to public answer.
5.1 · Thread A — "the recording was silent, but the file looked right"
Field (2026-09-12, 22:26). A live session produced an MP3 with the correct duration and size — and no audio. The logs carried the signature: Recording writer is falling behind — dropping audio as early as 6 ms after start, 950+ drops over 21.9 seconds, ending in Recording queue full on stop — finishing directly. ffmpeg measured the output at -91 dB: digital silence.
Design (V2.44/V2.45). The writer task was one-shot per session by design — it consumes the stop sentinel and finishes — but it was created only once at process start. After the first recording, every later session had no consumer; the bounded queue filled in milliseconds and stop() back-filled the timeline with silence. The fix: an idempotent _ensure_rec_writer() rebuilt on every audio block, stop enqueue and REC; a WARNING when the writer dies (the observability the incident lacked); a full queue drops the oldest block instead of synchronously finishing. Three regression tests; shipped in v1.15.0.
Support (2026-09-18). A new diagnostics bundle arrived carrying the same signature — but every matching log line was dated Sep 12, before the fix. The triage model, required to read the project's own incident history before writing a verdict, cross-referenced the CHANGELOG and the SDD version-history rows, cited server.py:348 in the shipped code, and refused both easy answers ("already fixed" / "regression"): it ran a protocol instead — record one new QSO and report back. The follow-up bundle closed the case with healthy Sep-19 logs. The public answer card states the reasoning and the raw evidence lines.
Without the row, the second report starts from zero and the maintainer re-derives a two-week-old diagnosis. With it, triage is a lookup: signature → history → verdict → protocol. The SDD row is not documentation debt; it is the project's immune memory.
5.2 · Thread B — "the app is damaged and cannot be opened"
Field (release week, v1.18.0). The first macOS installer shipped; users met a Gatekeeper dialog no right-click can bypass, and a second, quieter failure — RX audio was silent while the log told them to "check the radio".
Design (V2.54, shipped as v1.18.1). Two root causes, both instances of silent failure. The signing step had been failing silently since the first macOS build (the build script treated it as non-fatal), so every bundle shipped unsigned; the fix laid the resource tree out the way codesign requires and made a failed signature abort the build. Silent RX was a missing permission key: macOS happily opened the capture stream and filled it with zeros, reporting no error, because the bundle never asked for microphone access; the fix declares the key (asserted at build time) and points the warning at System Settings instead of the radio. Verification was artifact-level, not vibes: codesign --verify valid, the designated requirement satisfied, spctl complaining only about the absent Developer ID, and the system's own permission log moving from Refusing… to AUTHREQ_PROMPTING. The documentation answered the next user before they wrote in: the macOS guide explains both dialogs and the landing page says plainly that pre-v1.18.1 downloads must be replaced.
5.3 · Thread C — "PTT release feels laggy" (a one-paragraph case)
A field feeling ("too long after release before RX returns") was decomposed from logs into ~470–870 ms: a 262–300 ms capture reopen, a ~70–130 ms drain, poll ordering, and ~120 ms of muted Opus frames queued in client jitter buffers. V2.55 shipped three independent fixes — skip the TX-status poll before the release drain, stop broadcasting muted RX frames while keyed, and remember the last-good capture device to skip a full enumeration on reopen — with the reopen later measured down from ~270 ms to ~200 ms. The boundary stayed written down: the end-to-end improvement needs real-radio verification.
The same six stations: field signal → evidence in the log → SDD row (root cause + decision + boundary) → regression tests → release → public answer card. The differences are only cadence — the recording thread took a day from signal to fix and a week to the follow-up closure; the macOS thread took an evening; the latency thread took two days. The cadence is set by the slowest station, which is usually evidence, not code.
What the numbers can and cannot support
Counts are process evidence. They show the machinery ran; they do not promote a product claim by themselves.
| Observation | Value (as of 2026-09-19) | What it supports |
|---|---|---|
| Repo span | 2026-07-06 → 2026-09-19, 435 commits | Sustained cadence on one product line |
| SDD | 15 chapters; 67 version rows; AD-001…AD-022; 36 NFRs; SC1–SC9 | Requirements and decisions are traceable artifacts, not memory |
| Harness | 22 constraints (block/warn/info); pre-edit gate exit 2; release_check 29 checks; artifact registry with SHA-256 | Lessons are enforced, not archived |
| Tests | 262 at the 12th cycle (Jul 22) → 1,299 (Sep 19) | Each incident bought regression coverage |
| Releases | 37 changelog entries; v1.0.0 → v1.18.1; Windows, macOS, rpi64 | Shipping is routine; rollback targets exist |
| Radio surface | 10 model keys across 3 protocol families | The platform abstraction survived contact with other hardware |
| Support chain | 4 slices in 3 days; 2 published answers for Modern; board live | Field feedback has a pipeline, not a mailbox |
Unverified Icom and Yaesu models refuse transmit until hardware acceptance; the Yaesu family is receive-only in this phase; the update channel ships read-only (download/install is a later slice); the FDE board's first real bug is still waiting for its first real scheduling, and the first unattended patch for its first human merge; summary-based triage reads summaries, not whole environments. Comparable cross-project numbers — including the currently maintained test counts — live in one place: the evidence ledger.
What transfers, and in what order to build it
Eight rules extracted from the practice — then a three-stage adoption ladder with the trap at each stage.
Write the contract with the first commit
One vision paragraph, then all the chapters — including the NFR table with targets and verification methods. The first commit is the cheapest moment to have a contract; every later moment costs a retrofit.
Give every requirement a target and a method
"< 500 ms, listening test" is a requirement; "should be fast" is a preference. Success criteria like "PTT cannot stick" need an architecture chapter and a watchdog behind them.
Let the field pick the roadmap
One cycle = one real signal. No speculative features. The version-history row — signal, evidence, decision, boundary — is the roadmap log, and 67 of them make an argument no feature list can.
Turn every incident into a rule, not a memory
Repair is link one. The ruling is link two. The machine-readable constraint checked before the next edit is link three — only then is an incident permanently fixed. Keep the registry small; every row should have cost something real.
Instrument the field before scaling features
A redacted diagnostics bundle converts an interview into a measurement. Build the sensor before you need the data; the support chain here shipped in four slices, sensor first.
Make classification the decision
Answer the taxonomy question once and let it dispatch: environment noise closes, defects schedule, features wait for a human. Never let an un-answered report close — "closed" is a promise.
Automate the doing, never the verdict
Unattended implementation is safe only when bounded: read-only proposal, guards, isolated branch, full test gate,
mainuntouched, human merge. Keep the two ledgers apart — process evidence never promotes a product claim. The umbrella names the retained half — intent, boundaries, judgment — and the reason is accountability, not task size (roles).Write the boundaries down
No automatic promotion between lifecycle states; unverified hardware refuses transmit; the update channel says it is read-only; the known issue stays visible. A boundary written down is a trust asset; a boundary discovered by a user is a breach.
The adoption ladder
| Stage | What you build | You are done when… | The trap |
|---|---|---|---|
| 1 · Document era | SDD baseline (business / technical / product chapters) + a version-history row per change + a changelog | You can name the requirement and the decision behind any shipped behavior | Docs written once and quietly abandoned — the row must land with the fix, or it will always be "later" |
| 2 · Contract era | Constraint registry from real incidents + pre-edit checks + tests/release rules + a source-of-truth map | A known failure cannot be re-introduced without a red gate | Collecting rules nobody runs — enforcement (exit codes in the path of edits) is the whole point |
| 3 · Field-loop era | Bundle → triage → answers → board → unattended implement, with human merge | A user symptom becomes a tested patch on a branch while the verdict stays human | Automating past the guardrails — the loop must fail closed, or it becomes an unreviewed main history |
Copy the discipline, not the counts. Nobody else needs this project's test number or release cadence; what transfers is the shape: a contract that settles judgment, machinery that refuses known mistakes, a field loop that is allowed to correct both — and a human holding the verdict on what counts as done.
One sentence, three axes
A vision sentence became a contract; the contract became requirements with targets; the requirements became decisions; the decisions met reality and were corrected; the corrections became guards; the guards made the next delegation safe; and the support chain closed the circle by letting users report reality back in — with answers, a board, and a tested patch waiting for a human yes. That is the whole journey. The three axes are one sentence:
Let judgment settle into a contract, let machinery refuse the mistakes you have already paid for, and let the field — through the users — correct both. The verdict stays human, always.
Go deeper on the mechanism: Harness, Loops & Living SDD · the field loop · Agentic Engineering. The journal behind this case study: Seven Billion Tokens, Dissected · The Support Loop. Live surfaces: the MRRC Modern site, its answers page, and the FDE board.