From d847d8b82a92b5fa5aed99ab06adb770ee80e420 Mon Sep 17 00:00:00 2001 From: "Jeremy [KK7GWY]" Date: Sun, 14 Jun 2026 17:46:22 -0700 Subject: [PATCH] =?UTF-8?q?docs(constitution):=20v2.0.0=20=E2=80=94=20trim?= =?UTF-8?q?=20domain=20conventions,=20add=20governance=20principles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Constitution 1.1.0 -> 2.0.0 (MAJOR). Removed 5 AetherSDR-domain implementation conventions (formerly II MeterSmoother, III UI Labels, IV BandPlanManager, VI CHAIN Widget, VII Auto-Generated Contributors) — they were "use this class / read from that manager" guidance, not governance invariants. Relocated to AGENTS.md as implementation pointers. Added 5 governance principles in their place: - II The Radio Is Authoritative On Live State - III Radio-Persistable Settings Live On The Radio - IV Every Contribution Is Clean-Room - VI AetherSDR Never Transmits Without Operator Intent - VII Untrusted Input Is Validated At The Boundary Rewrote V from "use nested JSON" into the invariant beneath it (each feature owns its configuration as one self-contained object), the integrity pair to Principle XIV. Net: gapless I-XIV, 14 principles. I, V, and VIII-XIV keep their numerals and meaning; II/III/IV/VI/VII numerals were refilled (a clean renumber is deferred). CONSTITUTION.md and the canonical .specify/memory/constitution.md updated byte-identically. Downstream synced: AGENTS.md / CONTRIBUTING.md / GEMINI.md / copilot-instructions.md counts + domain lists; PR template gains a clean-room checklist item; stale Principle-II/IV citations re-pointed to their conventions. Principle XIII. Co-Authored-By: Claude Opus 4.8 --- .github/PULL_REQUEST_TEMPLATE.md | 6 +- .github/copilot-instructions.md | 2 +- .specify/memory/constitution.md | 260 ++++++++++++++---------- AGENTS.md | 47 ++++- CMakeLists.txt | 2 +- CONSTITUTION.md | 260 ++++++++++++++---------- CONTRIBUTING.md | 10 +- GEMINI.md | 4 +- tests/band_plan_license_filter_test.cpp | 4 +- 9 files changed, 372 insertions(+), 223 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 637177e06..d1de220a6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -21,7 +21,7 @@ an issue, lead with "Fixes #N" or "Closes #N". --> ## Test plan @@ -36,6 +36,8 @@ See CONSTITUTION.md for the full list. --> - [ ] Commits are signed (`docs/COMMIT-SIGNING.md`) - [ ] No new flat-key `AppSettings` calls — use nested-JSON-under-one-key (Principle V) -- [ ] All meter UI uses `MeterSmoother` (Principle II) +- [ ] Code is clean-room — not decompiled, disassembled, or + reverse-engineered from a proprietary binary (Principle IV) +- [ ] All meter UI uses `MeterSmoother` (AGENTS.md convention) - [ ] Documentation updated if user-visible behavior changed - [ ] Security-sensitive changes reference a GHSA if applicable \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2be5cdd6b..1fd6c31a3 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -28,7 +28,7 @@ priority must-knows that fit in Copilot's chat context window. stores `{"enabled": true, "mode": "auto"}` under `AppSettings["MyFeature"]`, not `MyFeatureEnabled` + `MyFeatureMode` as flat keys. -5. **All meter UI uses `MeterSmoother`** (Principle II). Do not +5. **All meter UI uses `MeterSmoother`** (AGENTS.md convention). Do not suggest envelope followers, `std::pow`/`exp` smoothing, or asymmetric `kAlpha` blenders for new meter widgets. diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index 9d4bd5aac..2becc9333 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -1,14 +1,14 @@ @@ -17,7 +17,7 @@ This block is regenerated on every constitution change; do not hand-edit below t | Field | Value | |---|---| -| **Version** | 1.1.0 | +| **Version** | 2.0.0 | | **Status** | `STABLE` | | **Applies to** | All AetherSDR contributions: source code, documentation, automation, release artifacts | @@ -60,99 +60,155 @@ radio. The failure mode is "I sent the command, the radio ignored it, nothing logged the mismatch." FlexLib doesn't have that ambiguity because it is the implementation the radio side was built against.* -### II. The Canonical MeterSmoother Owns All Meter Ballistics - -Every meter UI in AetherSDR uses `src/gui/MeterSmoother.h` (30 ms -attack / 180 ms release at 120 Hz / 8 ms interval). Targets are -normalized to `[0, 1]` via a `dbToRatio` helper before being passed -to `setTarget()`. Asymmetric `kAlphaUp`/`kAlphaDown` blenders, -`std::pow`/`exp` envelope followers, or copy-pasted smoothing from -other meter widgets are prohibited unless the source widget is -verified to itself be using `MeterSmoother`. - -*Why this is inviolable: meter ballistics are an interface-wide design -property. When one meter follows different ballistics than its -neighbors the whole panel reads as miscalibrated, and chasing the -inconsistency back to its source takes far longer than always using -the canonical class. If a meter genuinely needs different ballistics -(e.g., a slower GR-bar release), use `MeterSmoother::Ballistics` to -opt into different constants; never roll your own envelope follower.* - -### III. User-Facing Names Match The Visible UI Labels - -The toggle button at the top of `AppletPanel` says **DIGI**, so every -user-facing reference — issue comments, wiki pages, README, the -What's-New strings, error toasts — calls it **DIGI applet**. The -internal class name `CatApplet` is *not* user-facing. Same convention -applies anywhere the on-screen label and the C++ identifier disagree: -the on-screen label wins for prose. - -Similarly: the Help → Support → diagnostic logging toggles are -**Discovery**, **Commands**, and **Status** — never `radio.connection` -or any other backend category name. When asking a user to enable -logging, use the names they actually see in the UI. - -*Why this is inviolable: asking a user to "Enable DAX in the CAT -applet" or "toggle radio.connection logging" sends them looking for a -button that does not exist by that name. Support-channel friction -compounds.* - -### IV. Region-Aware Data Comes From BandPlanManager, Not BandDefs.h - -Anything that needs band edges, segment sizes, or per-band metadata -reads from the active band plan loaded by `BandPlanManager` (driven -by `AppSettings["BandPlanName"]` and the JSON files in -`resources/bandplans/`). `src/models/BandDefs.h::kBands[]` is -ARRL/US-allocations only and is not region-aware; it must not be the -source for new features. The dialog should display the active plan -read-only ("Using band plan: IARU Region 1 — change in View > Band -Plan"). - -*Why this is inviolable: AetherSDR's user base spans IARU regions -1/2/3. A feature that hardcodes ARRL band edges is wrong for everyone -outside Region 2 and silently transmits outside the band plan in -specific cases.* - -### V. New Configuration Uses Nested JSON Per Feature, Not Flat AppSettings - -`AppSettings` has ~460 call sites in a flat key namespace; that -flatness is on the refactor roadmap. New features must store their -configuration as a single nested JSON blob under one root key (e.g. -`AppSettings["AtuPreTune"] = {region, mode, …}`), not as a stack of -new flat keys. Existing flat keys can stay until they are migrated. - -*Why this is inviolable: every new flat key adds friction to the -roadmapped refactor and produces an `AppSettings` namespace that is -harder to reason about, harder to migrate, and harder to default -correctly across versions. Nested JSON gives each feature its own -isolated scope.* - -### VI. The TX DSP Chain Has A Visual CHAIN Widget As Primary Entry - -The TX DSP chain is stage-per-applet, and the visual CHAIN widget is -the primary entry point for understanding and configuring it. New TX -DSP stages must integrate with the CHAIN widget (be ordered, be -toggleable, be inspectable through it) rather than introducing -parallel UI entry points. - -*Why this is inviolable: the CHAIN widget is the user's mental model -for the TX signal path. A new stage that bypasses the widget is a -stage the user cannot reorder, cannot disable, and cannot see in -context — which fragments the model and produces support calls about -"missing" controls that are actually present elsewhere.* - -### VII. The About Dialog Contributors List Is Auto-Generated - -The Contributors list in the About dialog is built at runtime from -the GitHub API. Manual edits to it are reverted on the next build. -If a contributor is missing, fix the GitHub-side attribution (commit -authorship, co-authored-by trailer) — do not patch the dialog string. - -*Why this is inviolable: manual edits drift, get reverted by the -auto-generation, and create a maintenance burden where every release -must re-curate a list that the build system will overwrite anyway. -Fixing the underlying attribution data is durable; patching the -dialog is not.* +### II. The Radio Is Authoritative On Live State + +The radio holds the live state; the client mirrors it. Whenever the +client's model and the radio's reported state disagree about what is +live — a frequency, a mode, a filter width, a slice or panadapter +property — the radio wins and the client reconciles to it. +Reconciliation flows one way only: radio status updates the client +model; the client never writes its remembered value back over the +radio's. + +A user action is a *request* to the radio. Where a command has no +status echo the client may update optimistically, but the radio's +subsequent status is the truth and supersedes the optimistic value if +they differ. The command path runs client → radio; the truth path +runs radio → client; the two must never form a feedback loop. + +*Why this is inviolable: when the client lets its own model override +what the radio reports, the two form a feedback loop and the operator +sees values fight or flicker. The sharper failure is Multi-Flex — a +second client that trusts its own optimistic guess over the radio's +status drifts out of agreement with the radio and every other client, +and nothing logs the divergence. Principle I fixes the protocol source +of truth (FlexLib); this principle fixes the live-state source of +truth (the radio): radio status is read as truth, client commands are +only requests.* + +### III. Radio-Persistable Settings Live On The Radio + +If the radio can persist and recall a setting, that setting is saved +to and recalled from the radio — never duplicated in client-side +config. This holds for the whole of the radio's stored state, today's +and whatever the firmware adds later; the deciding test is simply +*whether the radio can save and restore the value*, not whether it +appears on any list. The client persists only state the radio does not +store at all — things like window geometry, layout, client-side-only +DSP, and UI/display preferences. + +*Why this is inviolable: when both the client and the radio persist +the same setting, they fight on reconnect. The radio's GUIClientID +session restore is always more current than the client's remembered +copy, so a client that recalls its own value clobbers the radio's live +state and the operator watches their rig jump to stale settings for no +reason they can see. Storing every radio-persistable setting only on +the radio removes the second source of truth that can drift. This is +the persistence corollary to Principle II: II says the radio wins on +live state; III says the radio owns the saved state that becomes live +state on the next connect.* + +### IV. Every Contribution Is Clean-Room + +Every contribution is clean-room from start to finish. Its code, and +the protocol knowledge behind it, must come from clean sources: public +documentation, open-source references, behavior observed on the wire, +and the contributor's own design and implementation. Code that is +decompiled, disassembled, or otherwise reverse-engineered from a +proprietary binary — or transcribed, translated, or paraphrased from +such output — must never enter the codebase, however correct or +convenient it is. + +The clean inputs are explicit. Reading FlexLib's published open-source +code (Principle I), capturing and studying the protocol as it actually +behaves on the wire, and reading official or public documentation are +all clean-room. + +The standard holds end to end: a contribution that began clean but +pulled in decompiler output at any point is contaminated, and +contamination is not a local defect — it travels to everything written +by reading it. + +*Why this is inviolable: decompiled code carries its original +copyright and license, so merging it silently relicenses someone +else's proprietary work as GPLv3 — which we have no right to do. +Worse, the contamination spreads to everything written from it, so the +only remedy is to rip all of it out and rebuild clean; one tainted PR +can put the licensing of the whole tree, and every fork, in question. +Trivial to refuse at the door, ruinous to undo after.* + +### V. Each Feature Owns Its Configuration As A Single Object + +Every feature's configuration is one self-contained object, owned by +that feature and stored under a single root key as a nested value +(e.g. `AppSettings["AtuPreTune"] = {region, mode, …}`) — never +scattered as loose flat keys across the shared `AppSettings` +namespace. The object is the unit of ownership: one place to default +when it is absent, one place to migrate across versions, one value to +write atomically. + +Because it is whole, the feature's config can be defaulted, versioned, +and persisted as a unit — the self-contained blob Principle XIV +(atomic persistence) requires. New configuration always takes this +shape; the legacy flat keys are grandfathered until migrated, and +nothing new is added to them. + +*Why this is inviolable: configuration scattered as independent keys +has no owner and no boundary. Defaults drift as keys accrete piecemeal, +keys orphan when a feature changes shape, and no migration or atomic +write can treat the feature's settings as the coherent unit they +actually are — a crash mid-write or a half-finished migration leaves +the namespace in a state no feature put it in and no reader expects. A +single owned object has one place to default, one to migrate, and one +value to write atomically, so its persistence is correct by +construction; a flat namespace of hundreds of keys is one nobody can +fully reason about, and every change to it carries unpredictable blast +radius.* + +### VI. AetherSDR Never Transmits Without Operator Intent + +The operator is the licensed control operator and is responsible for +every emission. The radio enforces its own out-of-band limits; +AetherSDR's duty is narrower and absolute — it never causes a +transmission the operator did not deliberately initiate. + +AetherSDR never keys the transmitter on its own: not on a timer, not +as a side effect of a status update or model change, not to recover or +resync a state, not as an automatic retry. Every emission traces to a +deliberate operator action — PTT, a tune request, a keyer or beacon +the operator explicitly started. Any code path that can transmit fails +closed: if the operator's intent to transmit is not unambiguous, it +does not key. + +*Why this is inviolable: a transmission the operator never asked for +puts a signal on the air under their callsign and their legal +responsibility, and it cannot be recalled once it leaves the antenna. +A client that can key the radio as a side effect — a stray retry, a +state-recovery path, a misfired timer — makes a software defect +transmit on the operator's license. Transmit is the one action where, +if intent is in any doubt, the only safe choice is not to.* + +### VII. Untrusted Input Is Validated At The Boundary + +AetherSDR consumes many external byte streams — the radio's VITA-49 +status and IQ, TCI, MQTT, KISS, rigctl, SmartLink/WAN, HTTP map and +spot feeds, and contributor-supplied files. None of it is trusted to +be well-formed. Every parser bounds-checks lengths, caps allocations, +validates ranges, and fails closed on malformed input; it must not +crash, hang, over-allocate, or act on bad data. + +Validation happens at the boundary — where the bytes enter, once, +before the data reaches the rest of the app — so the interior can +treat parsed values as sound. A malformed or hostile message is an +expected input, not an exceptional one, especially on paths reachable +beyond localhost (SmartLink/WAN, a shared MQTT broker, an exposed KISS +or rigctl port). + +*Why this is inviolable: the radio is on the LAN and several of these +protocols are reachable over the WAN or a shared broker, so a single +oversized field, truncated frame, or out-of-range index that a parser +trusts becomes a crash, a hang, or a memory-safety bug an attacker can +drive.* --- diff --git a/AGENTS.md b/AGENTS.md index 77e2ad9cc..ed0cb7b75 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,10 +44,11 @@ When helping with AetherSDR: - **Read the AetherSDR Constitution before writing or reviewing code.** Canonical source: `.specify/memory/constitution.md`. Byte-identical mirror at `CONSTITUTION.md` in repo root for discoverability. 14 - principles total: 7 AetherSDR-domain conventions (FlexLib authority, - MeterSmoother, UI labels, BandPlanManager, nested-JSON config, CHAIN - widget, auto-generated Contributors) + 7 defensive engineering - principles adopted from Cisco's + principles total (constitution v2.0.0): 7 AetherSDR-domain governance + principles (FlexLib authority, radio-authoritative live state, + radio-persistable settings, clean-room contributions, per-feature + config ownership, transmit-on-intent, boundary input validation) + 7 + defensive engineering principles adopted from Cisco's [Foundry Constitution](https://github.com/CiscoDevNet/foundry-security-spec/blob/main/constitution.md) (Evidence Over Assertion, Surface Only What Survives, Claims Are Atomic And Mortal, Fixes Are Demonstrated, Sandbox By Infrastructure, @@ -367,9 +368,10 @@ Run once at app or feature startup, not on every access. ### Radio-Authoritative Settings Policy -**The radio is always authoritative for any setting it stores.** AetherSDR -must never save, recall, or override radio-side settings from client-side -persistence. Only save client-side settings for things the radio does NOT save. +**The radio is always authoritative for any setting it stores** (Constitution +Principles II & III). AetherSDR must never save, recall, or override radio-side +settings from client-side persistence. Only save client-side settings for things +the radio does NOT save. **Radio-authoritative (do NOT persist):** frequency, mode, filter, step size, AGC, squelch, DSP flags, antennas, TX power, panadapter *count* and per-pan @@ -406,6 +408,37 @@ value through `MeterSmoother` (`src/gui/MeterSmoother.h`). Don't write new envelope-follower code or copy smoothing logic from other widgets — `MeterSmoother`'s header has the API and a usage example. +### User-facing names match the on-screen UI labels + +In prose (issue comments, README, What's-New strings, error toasts, support +requests) call a control by the label the user sees, not the C++ class name — +e.g. the **DIGI applet** (class `CatApplet`), and the Help → Support logging +toggles **Discovery / Commands / Status** (not backend names like +`radio.connection`). The on-screen label wins for prose, so users can find the +control you're naming. + +### Region-aware band data — read from BandPlanManager, not BandDefs.h + +Anything needing band edges, segment sizes, or per-band metadata reads the +active plan via `BandPlanManager` (`AppSettings["BandPlanName"]` + the JSON in +`resources/bandplans/`). `src/models/BandDefs.h::kBands[]` is ARRL/US-only and +not region-aware — don't source new features from it; AetherSDR's users span +IARU regions 1/2/3. + +### TX DSP stages integrate with the CHAIN widget + +The TX DSP chain is stage-per-applet and the visual **CHAIN** widget is the +primary entry point. New TX DSP stages must be ordered, toggleable, and +inspectable through the CHAIN widget rather than adding a parallel UI entry — +it's the user's mental model for the TX signal path. + +### The About-dialog Contributors list is auto-generated + +The Contributors list in the About dialog is built at runtime from the GitHub +API; manual edits are overwritten on the next build. If someone is missing, fix +the GitHub-side attribution (commit authorship / `Co-Authored-By` trailer), +don't patch the dialog string. + --- ## Multi-Panadapter Support diff --git a/CMakeLists.txt b/CMakeLists.txt index 09b5a442e..af5fb8215 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2218,7 +2218,7 @@ add_executable(perf_telemetry_test tests/perf_telemetry_test.cpp src/core/PerfTelemetry.cpp # LogManager.cpp owns the lcPerf Q_LOGGING_CATEGORY definition (per - # Principle III consolidation, #2770); LogManager has AsyncLogWriter + # the Q_LOGGING_CATEGORY consolidation in #2770); LogManager has AsyncLogWriter # as a member which transitively needs AppSettings, so all three .cpp # files come along to satisfy the ctor/dtor chain at link time. src/core/LogManager.cpp diff --git a/CONSTITUTION.md b/CONSTITUTION.md index 9d4bd5aac..2becc9333 100644 --- a/CONSTITUTION.md +++ b/CONSTITUTION.md @@ -1,14 +1,14 @@ @@ -17,7 +17,7 @@ This block is regenerated on every constitution change; do not hand-edit below t | Field | Value | |---|---| -| **Version** | 1.1.0 | +| **Version** | 2.0.0 | | **Status** | `STABLE` | | **Applies to** | All AetherSDR contributions: source code, documentation, automation, release artifacts | @@ -60,99 +60,155 @@ radio. The failure mode is "I sent the command, the radio ignored it, nothing logged the mismatch." FlexLib doesn't have that ambiguity because it is the implementation the radio side was built against.* -### II. The Canonical MeterSmoother Owns All Meter Ballistics - -Every meter UI in AetherSDR uses `src/gui/MeterSmoother.h` (30 ms -attack / 180 ms release at 120 Hz / 8 ms interval). Targets are -normalized to `[0, 1]` via a `dbToRatio` helper before being passed -to `setTarget()`. Asymmetric `kAlphaUp`/`kAlphaDown` blenders, -`std::pow`/`exp` envelope followers, or copy-pasted smoothing from -other meter widgets are prohibited unless the source widget is -verified to itself be using `MeterSmoother`. - -*Why this is inviolable: meter ballistics are an interface-wide design -property. When one meter follows different ballistics than its -neighbors the whole panel reads as miscalibrated, and chasing the -inconsistency back to its source takes far longer than always using -the canonical class. If a meter genuinely needs different ballistics -(e.g., a slower GR-bar release), use `MeterSmoother::Ballistics` to -opt into different constants; never roll your own envelope follower.* - -### III. User-Facing Names Match The Visible UI Labels - -The toggle button at the top of `AppletPanel` says **DIGI**, so every -user-facing reference — issue comments, wiki pages, README, the -What's-New strings, error toasts — calls it **DIGI applet**. The -internal class name `CatApplet` is *not* user-facing. Same convention -applies anywhere the on-screen label and the C++ identifier disagree: -the on-screen label wins for prose. - -Similarly: the Help → Support → diagnostic logging toggles are -**Discovery**, **Commands**, and **Status** — never `radio.connection` -or any other backend category name. When asking a user to enable -logging, use the names they actually see in the UI. - -*Why this is inviolable: asking a user to "Enable DAX in the CAT -applet" or "toggle radio.connection logging" sends them looking for a -button that does not exist by that name. Support-channel friction -compounds.* - -### IV. Region-Aware Data Comes From BandPlanManager, Not BandDefs.h - -Anything that needs band edges, segment sizes, or per-band metadata -reads from the active band plan loaded by `BandPlanManager` (driven -by `AppSettings["BandPlanName"]` and the JSON files in -`resources/bandplans/`). `src/models/BandDefs.h::kBands[]` is -ARRL/US-allocations only and is not region-aware; it must not be the -source for new features. The dialog should display the active plan -read-only ("Using band plan: IARU Region 1 — change in View > Band -Plan"). - -*Why this is inviolable: AetherSDR's user base spans IARU regions -1/2/3. A feature that hardcodes ARRL band edges is wrong for everyone -outside Region 2 and silently transmits outside the band plan in -specific cases.* - -### V. New Configuration Uses Nested JSON Per Feature, Not Flat AppSettings - -`AppSettings` has ~460 call sites in a flat key namespace; that -flatness is on the refactor roadmap. New features must store their -configuration as a single nested JSON blob under one root key (e.g. -`AppSettings["AtuPreTune"] = {region, mode, …}`), not as a stack of -new flat keys. Existing flat keys can stay until they are migrated. - -*Why this is inviolable: every new flat key adds friction to the -roadmapped refactor and produces an `AppSettings` namespace that is -harder to reason about, harder to migrate, and harder to default -correctly across versions. Nested JSON gives each feature its own -isolated scope.* - -### VI. The TX DSP Chain Has A Visual CHAIN Widget As Primary Entry - -The TX DSP chain is stage-per-applet, and the visual CHAIN widget is -the primary entry point for understanding and configuring it. New TX -DSP stages must integrate with the CHAIN widget (be ordered, be -toggleable, be inspectable through it) rather than introducing -parallel UI entry points. - -*Why this is inviolable: the CHAIN widget is the user's mental model -for the TX signal path. A new stage that bypasses the widget is a -stage the user cannot reorder, cannot disable, and cannot see in -context — which fragments the model and produces support calls about -"missing" controls that are actually present elsewhere.* - -### VII. The About Dialog Contributors List Is Auto-Generated - -The Contributors list in the About dialog is built at runtime from -the GitHub API. Manual edits to it are reverted on the next build. -If a contributor is missing, fix the GitHub-side attribution (commit -authorship, co-authored-by trailer) — do not patch the dialog string. - -*Why this is inviolable: manual edits drift, get reverted by the -auto-generation, and create a maintenance burden where every release -must re-curate a list that the build system will overwrite anyway. -Fixing the underlying attribution data is durable; patching the -dialog is not.* +### II. The Radio Is Authoritative On Live State + +The radio holds the live state; the client mirrors it. Whenever the +client's model and the radio's reported state disagree about what is +live — a frequency, a mode, a filter width, a slice or panadapter +property — the radio wins and the client reconciles to it. +Reconciliation flows one way only: radio status updates the client +model; the client never writes its remembered value back over the +radio's. + +A user action is a *request* to the radio. Where a command has no +status echo the client may update optimistically, but the radio's +subsequent status is the truth and supersedes the optimistic value if +they differ. The command path runs client → radio; the truth path +runs radio → client; the two must never form a feedback loop. + +*Why this is inviolable: when the client lets its own model override +what the radio reports, the two form a feedback loop and the operator +sees values fight or flicker. The sharper failure is Multi-Flex — a +second client that trusts its own optimistic guess over the radio's +status drifts out of agreement with the radio and every other client, +and nothing logs the divergence. Principle I fixes the protocol source +of truth (FlexLib); this principle fixes the live-state source of +truth (the radio): radio status is read as truth, client commands are +only requests.* + +### III. Radio-Persistable Settings Live On The Radio + +If the radio can persist and recall a setting, that setting is saved +to and recalled from the radio — never duplicated in client-side +config. This holds for the whole of the radio's stored state, today's +and whatever the firmware adds later; the deciding test is simply +*whether the radio can save and restore the value*, not whether it +appears on any list. The client persists only state the radio does not +store at all — things like window geometry, layout, client-side-only +DSP, and UI/display preferences. + +*Why this is inviolable: when both the client and the radio persist +the same setting, they fight on reconnect. The radio's GUIClientID +session restore is always more current than the client's remembered +copy, so a client that recalls its own value clobbers the radio's live +state and the operator watches their rig jump to stale settings for no +reason they can see. Storing every radio-persistable setting only on +the radio removes the second source of truth that can drift. This is +the persistence corollary to Principle II: II says the radio wins on +live state; III says the radio owns the saved state that becomes live +state on the next connect.* + +### IV. Every Contribution Is Clean-Room + +Every contribution is clean-room from start to finish. Its code, and +the protocol knowledge behind it, must come from clean sources: public +documentation, open-source references, behavior observed on the wire, +and the contributor's own design and implementation. Code that is +decompiled, disassembled, or otherwise reverse-engineered from a +proprietary binary — or transcribed, translated, or paraphrased from +such output — must never enter the codebase, however correct or +convenient it is. + +The clean inputs are explicit. Reading FlexLib's published open-source +code (Principle I), capturing and studying the protocol as it actually +behaves on the wire, and reading official or public documentation are +all clean-room. + +The standard holds end to end: a contribution that began clean but +pulled in decompiler output at any point is contaminated, and +contamination is not a local defect — it travels to everything written +by reading it. + +*Why this is inviolable: decompiled code carries its original +copyright and license, so merging it silently relicenses someone +else's proprietary work as GPLv3 — which we have no right to do. +Worse, the contamination spreads to everything written from it, so the +only remedy is to rip all of it out and rebuild clean; one tainted PR +can put the licensing of the whole tree, and every fork, in question. +Trivial to refuse at the door, ruinous to undo after.* + +### V. Each Feature Owns Its Configuration As A Single Object + +Every feature's configuration is one self-contained object, owned by +that feature and stored under a single root key as a nested value +(e.g. `AppSettings["AtuPreTune"] = {region, mode, …}`) — never +scattered as loose flat keys across the shared `AppSettings` +namespace. The object is the unit of ownership: one place to default +when it is absent, one place to migrate across versions, one value to +write atomically. + +Because it is whole, the feature's config can be defaulted, versioned, +and persisted as a unit — the self-contained blob Principle XIV +(atomic persistence) requires. New configuration always takes this +shape; the legacy flat keys are grandfathered until migrated, and +nothing new is added to them. + +*Why this is inviolable: configuration scattered as independent keys +has no owner and no boundary. Defaults drift as keys accrete piecemeal, +keys orphan when a feature changes shape, and no migration or atomic +write can treat the feature's settings as the coherent unit they +actually are — a crash mid-write or a half-finished migration leaves +the namespace in a state no feature put it in and no reader expects. A +single owned object has one place to default, one to migrate, and one +value to write atomically, so its persistence is correct by +construction; a flat namespace of hundreds of keys is one nobody can +fully reason about, and every change to it carries unpredictable blast +radius.* + +### VI. AetherSDR Never Transmits Without Operator Intent + +The operator is the licensed control operator and is responsible for +every emission. The radio enforces its own out-of-band limits; +AetherSDR's duty is narrower and absolute — it never causes a +transmission the operator did not deliberately initiate. + +AetherSDR never keys the transmitter on its own: not on a timer, not +as a side effect of a status update or model change, not to recover or +resync a state, not as an automatic retry. Every emission traces to a +deliberate operator action — PTT, a tune request, a keyer or beacon +the operator explicitly started. Any code path that can transmit fails +closed: if the operator's intent to transmit is not unambiguous, it +does not key. + +*Why this is inviolable: a transmission the operator never asked for +puts a signal on the air under their callsign and their legal +responsibility, and it cannot be recalled once it leaves the antenna. +A client that can key the radio as a side effect — a stray retry, a +state-recovery path, a misfired timer — makes a software defect +transmit on the operator's license. Transmit is the one action where, +if intent is in any doubt, the only safe choice is not to.* + +### VII. Untrusted Input Is Validated At The Boundary + +AetherSDR consumes many external byte streams — the radio's VITA-49 +status and IQ, TCI, MQTT, KISS, rigctl, SmartLink/WAN, HTTP map and +spot feeds, and contributor-supplied files. None of it is trusted to +be well-formed. Every parser bounds-checks lengths, caps allocations, +validates ranges, and fails closed on malformed input; it must not +crash, hang, over-allocate, or act on bad data. + +Validation happens at the boundary — where the bytes enter, once, +before the data reaches the rest of the app — so the interior can +treat parsed values as sound. A malformed or hostile message is an +expected input, not an exceptional one, especially on paths reachable +beyond localhost (SmartLink/WAN, a shared MQTT broker, an exposed KISS +or rigctl port). + +*Why this is inviolable: the radio is on the LAN and several of these +protocols are reachable over the WAN or a shared broker, so a single +oversized field, truncated frame, or out-of-range index that a parser +trusts becomes a crash, a hang, or a memory-safety bug an attacker can +drive.* --- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c984acb46..0735625d4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,10 +47,12 @@ that matches our conventions. 2. **Read the [AetherSDR Constitution](CONSTITUTION.md).** (Canonical source: [`.specify/memory/constitution.md`](.specify/memory/constitution.md); the root [`CONSTITUTION.md`](CONSTITUTION.md) is a byte-identical - mirror.) **14 principles total**: 7 AetherSDR-specific (FlexLib - authority, MeterSmoother, UI labels, BandPlanManager, nested-JSON - config, CHAIN widget, auto-generated Contributors) + 7 defensive - engineering principles adopted from Cisco's + mirror.) **14 principles total** (constitution v2.0.0): 7 + AetherSDR-specific (FlexLib authority, radio-authoritative live + state, radio-persistable settings, clean-room contributions, + per-feature config ownership, transmit-on-intent, boundary input + validation) + 7 defensive engineering principles adopted from + Cisco's [Foundry Constitution](https://github.com/CiscoDevNet/foundry-security-spec/blob/main/constitution.md) (Evidence Over Assertion, Surface Only What Survives, Atomic Claims, Demonstrated Fixes, Infra Sandbox, Operator Outranks Agents, Atomic diff --git a/GEMINI.md b/GEMINI.md index a34f60c3e..f073b356d 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -26,8 +26,8 @@ the must-knows that fit in Gemini's chat context efficiently. under one root key per feature (Principle V). Example pattern: `AppSettings["MyFeature"] = {"enabled": true, "mode": "auto"}`. -5. **All meter UI uses `MeterSmoother`** (Principle II) — never - roll your own envelope follower. +5. **All meter UI uses `MeterSmoother`** — never roll your own + envelope follower. (AGENTS.md → Key Implementation Patterns.) 6. **Assign yourself to an issue or PR before posting a review, comment, or merge action** (`gh issue edit NNNN --add-assignee @me` diff --git a/tests/band_plan_license_filter_test.cpp b/tests/band_plan_license_filter_test.cpp index be286c853..8387f322d 100644 --- a/tests/band_plan_license_filter_test.cpp +++ b/tests/band_plan_license_filter_test.cpp @@ -6,8 +6,8 @@ // disallowed frequency. A future refactor (STL-algorithm consolidation, // parallelisation) could silently break this; these tests pin it. (#3060) // -// Constitution: Principle IV (region-aware data comes from -// BandPlanManager, not BandDefs.h). +// Convention: region-aware data comes from BandPlanManager, not +// BandDefs.h (AGENTS.md → Key Implementation Patterns). #include "models/BandPlanManager.h"