Repository navigation
Conversation
There was a problem hiding this comment.
Issue fit
No linked issue, and none is needed: GOVERNANCE.md's "does NOT require an RFC" list covers documentation, and this adds no production code, no protocol surface and no settings key. Reviewed against the PR's own stated intent — a per-control, per-radio register of where the click ends, generated from the source and diffed in CI. It delivers that, and the generator is the real deliverable: I tried four ways to make it pass on a matrix that had stopped being true and it caught all four (details under "What I tried to break").
Scope
| file | what it changes | claimed by the title/body? | verdict |
|---|---|---|---|
docs/architecture/radio-feature-matrix.md |
new — the register, vocabulary, two matrices, per-family derivations, defects section | yes | in scope |
docs/architecture/radio-feature-matrix.json |
new — machine-readable sidecar, 61 records × 6 backends | yes | in scope |
tools/check_radio_feature_matrix.py |
new — source-derived generator + differ, stdlib-only | yes | in scope |
.github/workflows/static-checks.yml |
one --strict step, plus its rationale comment |
yes | in scope — but it is a new blocking step on a required check (see Nits) |
docs/architecture/README.md |
one index entry | yes | in scope |
Nothing in the diff is unexplained; no production source, no test target, no CHANGELOG.md entry, no deletions of any kind (the whole diff is +3443/-0). No socket test or fake peer is added, removed or modified.
Blockers
None.
Nits (all non-blocking)
- The ANAN section is stale in two adjacent sentences (
radio-feature-matrix.md:361and:368) — inline comment.P2Client::setDdcRateLive(P2Client.h:146, body atP2Client.cpp:175) is a secondQ_INVOKABLElive setter that sends on a running session, andAnanBackend::beginRateChangenow writes the rate live and resends it three times (AnanBackend.cpp:1103-1110) instead of a stop/reconfigure/restart. No cell changes —rx/pan-bandwidth/ANAN staysWeither way — but this is precisely the decay class the register exists to stop, in the half of the document the checker cannot see. - A whole feature row can be deleted from both files and CI stays green. I removed
rx/agcfrom the JSON and its line from the table:--strictreported60 feature(s) … 0 disagreement(s)and exited 0. Cell drift is guarded; roster shrink is not, and neither is a new seam verb arriving with a GUI control and no row.MIN_CELLS = 200only catches a wholesale collapse. Worth a line in the doc saying so, or a future roster anchor. rx/dial-lock's note overstates (radio-feature-matrix.json:535) — inline comment.IcomCivBackend.cppreadscapabilities().hasRadioDialLockat 1198, 2444, 4693 and 7017. The claim holds above the seam; it needs those three words.- "
His a good cell … readHas done" (radio-feature-matrix.md:98) sits against the #5262 M3a maintainer ruling quoted inradio-capabilities-map.md:382-391: "individual controls are never hidden per radio — they dim with a reason", with the remaining hide sites scheduled as M3b. 84 cells currently read as finished against a ruling that calls them pending migration. A maintainer framing call rather than a defect — inline comment. ControlAvailabilityRegistry … referenced by nothing but itself(:31) — it also has a registered unit test (tests/control_availability_registry_test.cpp). "No production consumer" is the claim that survives.
What I tried to break
Everything below I ran against the PR head checkout; the checks are empirical, the code claims about ANAN/Icom/RTL are read from source, not observed running.
- Source mutation. Made
RtlSdrBackend::setSliceAgcstore its arguments instead ofQ_UNUSED-ing them.--strictfailed withrx/agc/rtl is committed as D and the source derives W, exit 1. The generator genuinely parses the tree; it is not re-reading its own sidecar. - Document-only mutation. Flipped one cell in the markdown table, leaving the JSON alone:
feature-matrix-doc-drift, exit 1 under--strict, exit 0 without it — matching the documented flag contract. - Weakest-evidence rot. Renamed the
audio_pan=wire token inSliceModel.cpp; the*cell's alt-path assertion fired (feature-matrix-stale-alt-path). The file/symbol/wire triple really is all three. - Can
Vlaunder a dead control? No —check_radio_feature_matrix.py:955-971requires both a run citation andderived == "W", so a hand-addedVcannot sit on top of aD. This was the hole I most expected to find. - Can a broken parser pass everything? No —
Source.vacuity()floors (60 seam virtuals, 90 caps fields, 8 overrides/backend, 20 caps assignments) return 1 regardless of--strict, with a comment forbidding lowering them to match. - Workflow placement. The step sits in the same
static-checksjob as its five siblings, after checkout +setup-python@3.12, withif: ${{ !cancelled() }};static-checks.ymlcarries nopaths:filter, so it runs on every PR. Stdlib-only, path-anchored on__file__(verified by running it from/tmp), 1.3 s. All five checks are green on03e27c07. - Spot-checked the prose against the tree, since the checker only guards cells:
m_nbModeOk && (m_hasRadioSideDsp || m_hasHostNoiseBlanker)atVfoWidget.cpp:3322,refuseKeyOnTransmitIncapableBackendatRadioModel.cpp:4859,RtlReceiverRegistry's only non-self reference being a comment inWdspChannel.cpp:1091— all hold. The ANAN paragraph is the one that did not. - Could not check: whether the 7
Vcells' cited runs (d99,d101,d103,d110,d111,d114,d126) exist — the run records are not in this repo, so the citation requirement is a format check here, not a provenance one.
Recommendation
Approve with nits. The generator is the part that had to be right and it survived every attack I could construct, including the two shapes that would have made the document decorative — a V masking a dead control and a broken parser passing everything. The five nits are prose and framing; only #1 is a false statement about current code, and it changes no cell. I would ask for #1 and #3 as one-line edits before merge, and leave #2 and #4 to the maintainer as follow-ups (the roster anchor, and whether 84 H cells should read as done or as M3b backlog).
🤖 aethersdr-agent · cost: $9.8696 · model: claude-opus-5
| at a fraction of the cost of turning it into a `W`. A vocabulary that scored | ||
| them alike would make the cheap fix invisible. | ||
|
|
||
| **`H` is a good cell, not a gap.** On a receive-only radio a hidden transmit |
There was a problem hiding this comment.
Flagging for a maintainer rather than as a defect: radio-capabilities-map.md:382-391 records the #5262 M3a ruling as "individual controls are never hidden per radio — they dim with a reason", with the remaining hide sites scheduled as M3b, on the grounds that a hidden control is not announced to a screen reader at all.
So the 84 H cells are, per that ruling, pending migration rather than finished — and "read H as done" is the one sentence in the vocabulary a reader could act on in the wrong direction. Either a pointer to M3a here, or a maintainer confirming that H is the intended end state for these rows.
There was a problem hiding this comment.
No change here, deliberately: whether H is the intended end state or a pending-M3b defect class changes the meaning of 84 cells, and that is a maintainer's ruling rather than something a vocabulary section should settle on its own authority. The consequence is on the record in the conversation on this PR under "#4", including the two sub-questions that fall out either way — whether H should split into hidden correctly versus hidden pending migration, and whether a receive-only radio's transmit controls are inside the ruling's scope at all. Leaving this thread open until someone rules; the vocabulary in radio-feature-matrix.md is unchanged until then.
There was a problem hiding this comment.
Changed in 727a568. The line now reads "H and M are both hidden, and neither means done". It points to a new "Hidden cells: H and M" section, which quotes §4a and the M3b line from radio-capabilities-map.md. H now means only an applet hidden wholesale, which M3a permits. M means a single control that is gated away (hidden, disabled without an announced reason, or relabelled) where §4a wants it dimmed with a reason, so it is pending M3b. All 89 hidden cells are M and none is H: no row in the register is hidden by hiding its applet. Each record now carries a hide block naming the gate's file, symbol and observed form, and the checker enforces it. Four rows didn't fit a form §4a names: rx/filter-preset, tx/freq-check, rx/antenna and dsp/notch-enable. They are marked M, with the reason in their notes, rather than scored as compliant.
|
Thanks — this is the review I wanted. You attacked the generator instead of reading it, and the two shapes you went after are the two that would have made the whole document decorative: a Four of the five are addressed in 1 — the ANAN section was false against the tree I generated from. Fixed, and the sting is the point.Verified in the source before touching it, and you are right.
One correction to your framing, since precision is the whole subject here: the live write is in No cell moves — Your sentence about this — "precisely the decay class the register exists to stop, in the half of the document the checker cannot see" — is the honest verdict and I am not going to soften it. The register guards 366 cells and cannot guard the prose that explains them. A dispatch trace written against a file that changed underneath it is exactly how a document goes decorative while its CI step stays green. The only mitigation in the artefact today is that the prose cites file and symbol so it can be re-checked by hand — which is what you just did, and it took a reviewer to do it. 2 — roster shrink is not guarded. Documented here; the anchor is offered as a follow-up.Reproduced, and the mechanism is worse than the symptom.
And This is the same shape this project has been closing all week — the check guards what it was pointed at, and is silent about what was never listed — so the limit should be written down rather than implied. New section in the document, "What a green check does not prove — the roster is not guarded", cross-referenced from "Keeping this true", and a matching section in the PR body. It says plainly: a green run means every cell committed here agrees with the source; it does not mean every control an operator can reach is in this table. The repair, named so it is not lost: a roster anchor — derive the feature list from the seam verbs and the GUI control inventory, and diff it the way the cells are diffed, so an addition or a deletion fails instead of passing quietly. That is a generator of a different shape from this one, and I have deliberately not built it in this PR; it deserves its own change where it can be attacked the way you attacked this one. I will file it as a follow-up issue on a maintainer's word. 3 —
|
#5894 and #5879 collide: six HL2 cells in the feature matrix, and the gate that should catch it cannotFound in a local integration build of our open PRs. Neither PR is wrong on its own, and neither can carry the fix alone — so this is a note, not a change request. The interaction
There is no escape hatch in the record — the matrix has no "expected after" field, and the checker has no write mode ( What I measuredFour standalone trees,
The fourth row is the one that settles placement: putting the record update on #5894's branch today turns a currently-green PR red, because on its own base the source still derives Summary line, before and after, in the combined tree: How this surfaces — and the part that is worse than "the second one goes red"
Either way the record update belongs on whichever PR merges second, applied after its branch carries the other's change:
Applied earlier than that, on either branch, it fails that branch's own The six cells
Note that #5894's own matrix prescribes the change that breaks it: the P-provenance row's repair column reads "One assignment per backend:
|
jensenpat
left a comment
There was a problem hiding this comment.
Issue fit
No linked issue. This is a documentation register plus a source-derived checker: no production code, no protocol surface, no settings key. Reviewed against the PR's own intent — where an operator click ends, per control per radio, generated from the tree and diffed in CI.
Scope
| file | change | claimed? | verdict |
|---|---|---|---|
docs/architecture/radio-feature-matrix.md |
the register | yes | in scope |
docs/architecture/radio-feature-matrix.json |
sidecar the checker diffs | yes | in scope |
tools/check_radio_feature_matrix.py |
generator + --strict differ |
yes | in scope |
.github/workflows/static-checks.yml |
one step on the existing job | yes | in scope |
docs/architecture/README.md |
one index bullet | yes | in scope |
No production edits, no CHANGELOG.md entry, no new test target, no socket peer. The whole diff is additions.
Blocker
The head is consistent with its base 7c8fdf84. It is not consistent with current main (9f81dc00, 14 commits ahead of that base, clean textual merge).
I merged origin/main into this head locally and ran tools/check_radio_feature_matrix.py --strict. Exit 1, two drifts, nothing else:
rx/rf-gain/ ANAN is committedDand derivesW.AnanBackend::setPanRfGain(landed in #5820) drives the G2 step attenuator throughP2Client::setStepAttenuationDb.dsp/noise-blanker/ ANAN is committedHand derivesW.capabilities()now setshasHostNoiseBlanker = true, andsetSliceNoiseBlankerforwards intoAnanRxDsp::setNoiseBlanker(#5824). The row's gate is the OR ofhasRadioSideDspandhasHostNoiseBlanker, so the hide rung no longer applies.
setStepAttenuationDb is a third Q_INVOKABLE live setter on P2Client. The ANAN section's "two live setters, and everything else at connect time" is false on today's tree, and RF gain is one of the things that is no longer connect-time-only.
This PR adds that checker as a step of the required Static checks job. Squash-merging it onto current main would put a register on main that fails its own gate. Rebase onto current main, re-run --strict, and commit the cells it derives in both the markdown table and the JSON sidecar. Update the ANAN dispatch paragraph in the same pass so it names the noise blanker on the DSP path and the step attenuator on the live wire path.
What held on the reviewed head
--strict on 694472bf is clean: 61 features × 6 backends = 366 cells, W=143 D=94 P=7 R=22 H=84 U=9 V=7, matching the PR body. P split is 6 inherited-default repeater cells and 1 cached-readback (rx/filter / RTL).
Scratch mutations, then restored (HEAD still 694472bf, --strict green again):
- Replacing the only
audio_pan=token inSliceModel.cppfailed withfeature-matrix-stale-alt-pathand a drift ofrx/audio-pan/ Flex fromWtoD. Flex does not overridesetSliceAudioPan, so the new RETIRED path correctly called this rot. - Removing
filtfromSliceModel.cppwhileFlexBackend::setSliceFilterstill sends it produced onefeature-matrix-alt-path-retirednotice and exit 0. That is the split added in694472bf, and it behaved as the comment describes.
Spot checks the checker does not own, read from this head and still true on origin/main where I re-read them: setXitOffset aliases setRitOffset; applyMemoryRecallDetails calls setSliceFmRepeater and returns true; validCoverage() refuses minimumHz <= 0 and Flex and ANAN declare {…, 0, 0}; ModelRadioConnectionTarget::supports() admits sim, rtl, and LAN flex/hl2/anan; RTL m_filterLowHz / m_filterHighHz are stored and never loaded; hasFmRepeaterOffset defaults to true. Parsed override counts match the prose: Flex 21, Icom 55, RTL 20, ANAN 15. finishRateChange still sends setDdcRateLive three times (immediate, then 60 ms and 140 ms).
CI on 694472bf is success for build, check-macos, check-windows, Static checks, and Sanitizer option configures. All four commits are GitHub-verified.
Nits, not this review's blocker
The open thread on "Read H as done" versus the #5262 M3a ruling (hide sites are M3b backlog) is a framing call. It does not fail the checker. The roster hole is already written down in the document; I am not asking for that generator in this PR.
The seven V citations are format-checked only. The bench records are not in this repo, so I did not confirm the runs exist.
Recommendation
Request changes. Do not merge until the two ANAN cells, and the dispatch paragraph that still says only two live setters, are regenerated against current main.
| | `rx/pan-center` | W* | W | W | W | W | W | drag the spectrum or waterfall (pan centre) | | ||
| | `rx/pan-bandwidth` | W* | W | V | W | W | D | zoom / span | | ||
| | `rx/pan-framerate` | W* | D | W | W | W | D | waterfall / spectrum frame rate | | ||
| | `rx/rf-gain` | W* | W | V | D | W | D | RF gain slider (ANT panel) | |
There was a problem hiding this comment.
On current main this ANAN cell is W, not D. AnanBackend::setPanRfGain (AnanBackend.cpp on 9f81dc00) writes the G2 step attenuator through P2Client::setStepAttenuationDb (#5820). A local merge of origin/main into this head makes --strict fail with rx/rf-gain/anan is committed as D and the source derives W.
The sidecar has the same cell at radio-feature-matrix.json ("anan": "D" under rx/rf-gain). Both have to move together; the checker diffs them.
| | `rx/rf-gain` | W* | W | V | D | W | D | RF gain slider (ANT panel) | | |
| | `rx/rf-gain` | W* | W | V | W | W | D | RF gain slider (ANT panel) | |
| | feature | Flex | Icom | HL2 | ANAN | RTL | Demo | control | | ||
| |---|:--:|:--:|:--:|:--:|:--:|:--:|---| | ||
| | `dsp/noise-reduction` | W* | W | H | H | H | H | NR button and level | | ||
| | `dsp/noise-blanker` | W* | W | W | H | H | H | NB button and level | |
There was a problem hiding this comment.
On current main this ANAN cell is W, not H. AnanBackend::capabilities() sets hasHostNoiseBlanker = true, and setSliceNoiseBlanker forwards into AnanRxDsp::setNoiseBlanker (#5824). This row gates on hasRadioSideDsp || hasHostNoiseBlanker, so the hide rung no longer applies and the override scores W. --strict after merging origin/main reports exactly that drift.
Update the JSON cell ("anan": "H" under dsp/noise-blanker) in the same edit. The note on that record still correctly describes the OR; it does not need to keep ANAN hidden.
| | `dsp/noise-blanker` | W* | W | W | H | H | H | NB button and level | | |
| | `dsp/noise-blanker` | W* | W | W | W | H | H | NB button and level | |
|
|
||
| ### ANAN (P2) — two live setters, and everything else at connect time | ||
|
|
||
| `P2Client` exposes **two** `Q_INVOKABLE` live setters, `setDdc0FrequencyHz` and |
There was a problem hiding this comment.
This count is stale against the tree the two cell fixes have to be regenerated on. P2Client on origin/main has a third Q_INVOKABLE live setter, setStepAttenuationDb (P2Client.h), and setPanRfGain calls it on the running session. "Everything else the radio is told is a connect-time P2Client::Params field" is no longer true for RF gain, and the DSP bullet below omits the noise blanker, which now reaches AnanRxDsp::setNoiseBlanker.
setDdcRateLive from finishRateChange (immediate plus the 60 ms and 140 ms resends) is still accurate. Please extend this paragraph when you rebase, rather than leaving the heading at two setters.
…r radio
A support matrix normally answers "did the backend declare it" or "is there a
control on screen". Neither is the question an operator has, and neither can
tell a working control from a dead one: the widget moves, the model commits, a
readback confirms the value, nothing logs and nothing signals.
This adds the third artefact — 61 operator controls x 6 radio families, scored
by WHERE THE CLICK ENDS — plus the four dispatch traces behind it (Flex, Icom
CI-V, ANAN P2, RTL-SDR), a JSON sidecar, and a checker that keeps it true.
TWO TABLES, TWO DERIVATIONS, and that split is the substantive finding. The
test that decides is one question: does anything refuse when the declaration is
absent?
* In the GUI, nothing does. Defaults are permissive (hasFmRepeaterOffset is
declared = true), the gates are hand-written and optional, and the one
registry built to enforce them has no callers. So GUI cells are derived
from REACHABILITY and a declaration is only a cross-check.
* In aetherd, something does, and it is written into the target:
ModelReceiveControlTarget opens every admission with `caps.<record> &&
known(authority)` and answers capability.unavailable. There the declaration
IS the contract, so the headless cells are derived from the RECORD.
Deriving the headless cells the GUI way would print a phantom W for every verb
the daemon never exposes, so a feature with no aetherd method gets no headless
cell at all — it is named in prose instead of being scored.
THE CHECKER GENERATES AND DIFFS; it is deliberately not a touch-counter. A gate
that says "edit the matrix when you edit a capability" enforces A change, not
the RIGHT one — the shape tools/audit_colours.py already has, where the
observed behaviour is reasoning about satisfying the counter. A diff cannot be
paid off with a comment. It derives the four questions a parser can settle (is
the capability declared and to what, is the virtual overridden, is the base
Q_UNUSED, is the control gated above the seam) plus two more with teeth: a
runtime-published label list, and whether a stored value is ever read back.
V — verified on real hardware — is NEVER generated. The checker asserts only
that a V carries a run citation and that the generator independently derives W
for the same cell; a V on a cell the source calls dead is either over-claimed
or the generator is broken. Seven V cells, all HL2, all citing bench runs that
answered from the radio's own MAC. d77 and d87 ran against hpsdrsim on loopback
and produce no V however good their numbers are.
U is a legitimate committed value, accepted only where the GENERATOR emitted U.
Nine cells are U, in two honest classes: the setXitOffset alias (correct on an
Icom, which has one shift register; silently wrong on a two-register family),
and applyMemoryRecallDetails, whose base returns TRUE after calling four
no-ops.
WHAT THE MATRIX SAYS TODAY: W=143 V=7 P=7 R=22 H=84 D=94 U=9 over 366 cells.
The 94 D cells are the point of the exercise — live widgets whose intent dies
silently — and they concentrate in three families: squelch and RIT/XIT, which
have no capability field at all; the transmit setpoints on receive-only radios,
where the canTransmit refusal was applied to the keying paths and not to the
setpoints; and slice audio mute/pan on backends that demodulate on this host.
No capability field was added. tools/check_capability_records.py stays at its
frozen 71 and this work does not touch RadioCapabilities.h.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TtnKQu6QGrSeBirGeAsAs
…P grounds
A review of the register's FM rows agreed with the verdicts and corrected the
reasoning behind them, which is the half that would have gone wrong next.
WHAT WAS WRONG. The prose explained HL2's dead FM tone cells by the VALUE of
fmTonePresentation, as though `Legacy` were doing the gating. It is not:
`Hidden` is the safe default and `Legacy` is what turns those controls ON. The
discriminator between D and P is not a field's value at all — it is PROVENANCE:
* the backend ASKED (it assigned the gating capability itself) and did not
implement the verb -> a bug in that backend -> D;
* the backend NEVER ASKED (it does not mention the field) and a permissive
default offered the control on its behalf -> a gap in the default, one
assignment repairs it -> P;
* the gate is effectively false either way -> the operator sees nothing ->
H, and it does not matter who wrote the false.
The generator already implemented this and only this — PERMISSIVE is reachable
solely from DEFAULT_TRUE, which effective() returns only when capabilities()
carries no assignment for the field. Two adversarial checks now pin it: making
AnanBackend assign `hasFmRepeaterOffset = true`, the SAME permissive value it
was already inheriting, flips both cells P -> D on provenance alone; assigning
`fmTonePresentation = Legacy` flips the four tone cells H -> D. Neither test
changes a value the code did not already have.
So gate_state() now states the rule and names the trap, the vocabulary section
asks the contributor's question outright ("did this backend ask for this
control?"), and the FM row notes in the sidecar carry the same correction.
AND THE SPLIT THAT WAS BEING LOST. P has TWO grounds and they are different
repairs, so a single tally hid the more expensive one:
* inherited default (6 cells) — fm/repeater-{dir,offset} on HL2, ANAN, Demo.
Nobody decided to offer these; `c.hasFmRepeaterOffset = false;` repairs each.
* cached readback (1 cell) — rx/filter on RTL. The backend DID ask: it
validates, stores, echoes a SliceDelta, and the DDC stores into two atomics
nothing reads. Real work repairs it, and this is the ground ON8ST's
"reads back cached state" clause exists for.
The checker now labels each P with its ground and prints the breakdown, and a
new document section names every P cell, its ground and its repair.
ANAN's four FM TONE cells were checked against the source and need no change:
AnanBackend never mentions fmTonePresentation anywhere, its default is Hidden,
and a default the backend never requested is only a phantom when it is
PERMISSIVE. An off default hides the control, the app and the radio agree, and
H is the honest cell. ANAN's P cells are the two repeater rows, which the lab's
own matrix merges with the tone rows into one line — which is why splitting them
changes the answer.
No cell value moved. 366 cells, W=143 V=7 P=7 R=22 H=84 D=94 U=9, unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TtnKQu6QGrSeBirGeAsAs
…es not guard Review feedback on the radio feature register. No cell moves: the checker reports the same 366 cells, W=143 D=94 P=7 R=22 H=84 U=9 V=7, 0 disagreements. - ANAN (P2) is no longer "one live setter". P2Client exposes a second Q_INVOKABLE live setter, setDdcRateLive, which early-returns unless the session is running and otherwise resends the whole DDC-Specific packet; AnanBackend::finishRateChange invokes it three times across the settle window, and startP2ClientSession is now reached from the connect path alone. The rate no longer costs a stop/reconfigure/restart. This is the decay class the register exists to stop, in the half of the document the checker cannot see. - rx/dial-lock's note said hasRadioDialLock is read in exactly one file. It is read in exactly one file ABOVE THE SEAM, RadioModel.cpp, which is the read the D cells rest on; IcomCivBackend.cpp reads it below the seam in four places, on the one family whose cell is W. - ControlAvailabilityRegistry is not "referenced by nothing but itself". It has a registered unit test. The claim that survives is no production consumer. - New section: the check guards CELLS, not the ROSTER. main() iterates the committed record, so a feature deleted from both the JSON and the table passes green, and a new seam verb arriving with a GUI control and no row is equally invisible. MIN_CELLS is a vacuity floor, not a roster check: 27 of today's 61 rows could go before it fires. A roster anchor is named as the repair and deliberately not built here. - MIN_CELLS's own comment said 62 rows; there are 61. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TtnKQu6QGrSeBirGeAsAs
…le VIII. Found by running the register's own checker against a live migration. `tools/check_radio_feature_matrix.py --strict` against aethersdr#5907 (aetherd: migrate receive controls behind the backend seam) exits 1 on two `feature-matrix-stale-alt-path` errors — `rx/filter`/Flex and `rx/agc-threshold`/Flex — and both are literally correct: the `'filt '` and `'agc_threshold='` wire text left `SliceModel::setFilterWidth` and `SliceModel::setAgcThreshold` for `FlexBackend::setSliceFilter` and `FlexBackend::setSliceAgc`, below the seam. The defect is not the two entries. EVERY `alt_path` in this register documents an above-seam bypass, and that work-stream exists to delete them — so the check alarms on every SUCCESS of it. After the third time someone learns to ignore the error, and it is then worthless on the day it finds real drift. So the boolean becomes a classification. The question is not "did the triple break" — it breaks identically either way — but *did the evidence get weaker, or did a weaker piece of evidence stop being needed?* ROTTED the claim is false and nothing stronger replaced it. Fails, and now names which of four conditions refused. RETIRED the bypass was deliberately deleted because the control moved BEHIND the seam. Reported as progress; never fails. RETIRED requires all four: the wire token is gone from the declared file (a file that still emits it has the bypass and merely names it wrong — an unrecorded rename); the backend overrides the row's seam verb with a body that is not a Q_UNUSED no-op; the cell still derives W; and the token is still emitted from that backend's own source, so the bypass was absorbed rather than lost. The notice is an edit request and repeats until the record is deleted and the `*` dropped from the document. `LIVE` is also now read comment-free, which is a small strengthening the old predicate did not have: a wire token surviving only in a FlexAPI comment no longer keeps the claim alive. All forty records pass either reading today. One case this cannot separate, and it says so rather than pretending: where a backend ALREADY overrode the verb and ALREADY carried the same wire token below the seam — three records, rx/filter, rx/agc-threshold and rx/pan-center on Flex — mangling the above-seam literal yields a tree indistinguishable from the migration, because in every respect this register scores it IS the same tree. The other 37 of 40 records are load-bearing: breaking one moves the cell and `feature-matrix-drift` fires beside the alt-path error. Second, smaller defect in the same tool: the U-provenance breakdown was gated on `if reasons and not errors`, so it disappeared exactly when a run was failing and you most wanted it — an unexplained U is often the same parse failure that produced the error above it. Ungated. Also fixed on the way: condition (d) first used `publishes()`, which anchors on `\b` for an identifier. A wire literal is not an identifier — `"filt "` ends in a space, `"agc_threshold="` in an equals sign, and the next character in the source is `%` — so `\b` never matched and every relocation was reported as a loss. `emits_wire()` is a plain comment-free substring. Against origin/main: 366 cells, W=143 D=94 P=7 R=22 H=84 U=9 V=7, 0 alt paths retired, 0 disagreements. Against aethersdr#5907's head: identical cells, 2 alt paths retired, 0 disagreements, exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TtnKQu6QGrSeBirGeAsAs
Rebased onto current `main` (`2859fdef`). The head was clean on its own base `7c8fdf84`; on today's tree the register's own checker exits 1 with two drifts, both ANAN, both re-derived from source here rather than taken from the review: rx/rf-gain/anan D -> W dsp/noise-blanker/anan H -> W `rx/rf-gain` moves because `AnanBackend::setPanRfGain` (`AnanBackend.cpp:1214`) is now a real override that reaches the wire: it clamps the slider into -31..0 dB, negates it into 0..31 dB of attenuation, mirrors it into `m_pendingParams`, and invokes `P2Client::setStepAttenuationDb` (`P2Client.h:130`, body at `P2Client.cpp:180`), which resends the High Priority packet immediately when the session is running. Landed in `22389dbb` (aethersdr#5820). `dsp/noise-blanker` moves because `AnanBackend::capabilities()` now sets `hasHostNoiseBlanker = true` (`AnanBackend.cpp:491`) with `hasRadioSideDsp` still false, and `setSliceNoiseBlanker` (`AnanBackend.cpp:919`) forwards into `AnanRxDsp::setNoiseBlanker`. The button's gate is the OR of those two fields (`VfoWidget.cpp:3334`), so the hide rung no longer applies and the override scores W. Landed in `41acb26a` (aethersdr#5824). Both cells are moved in the markdown table and in the JSON sidecar, which the checker diffs against each other. The ANAN dispatch section was the half the checker cannot see, and it had gone false in the same two places. `P2Client` now exposes THREE `Q_INVOKABLE` live setters, not two — `setStepAttenuationDb` joins `setDdc0FrequencyHz` and `setDdcRateLive` — so the heading, the count and the "everything else is a connect-time `P2Client::Params` field" sentence are corrected, the wire bullet gains RF gain, and the DSP bullet gains the noise blanker. The `dsp/noise-blanker` note named only the HL2 for a mechanism the ANAN now shares; it names both. Against `2859fdef`: 366 cells, W=145 D=93 P=7 R=22 H=83 U=9 V=7, 0 alt paths retired, 0 disagreements, `--strict` exit 0. The other nine python gates in the Static checks job are green on this tree too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtHEsQsghFwhUQEZdwVKUz
694472b to
d481e6e
Compare
|
@jensenpat — rebased and all five parts applied in One thing to flag before the detail:
Both markdown edits are byte-identical to your The paragraph at :397 is extended rather than patched: the heading no longer says two setters, the "everything else is a connect-time
I rebased rather than merged, as you asked. Worth recording why it was safe: upstream touched neither shared file ( I have also corrected the body, which had drifted further than the cells: the tally appeared in six places and the two-setter statement was still there, now crediting your correction. The historical A cross-PR hazard you should see, tested rather than reasoned about. #5879 (
The cause is #5879 doing exactly what its title says — withdrawing the FM controls the HL2 has no verb for — which makes every gating capability false, so those rows score The fix is one regeneration and it can only be written on the second one to land. I am not proposing to pre-empt it here — say which order you want and I will put it on that PR. (This is the same gap as #5914.) Not addressed, deliberately: the |
jensenpat
left a comment
There was a problem hiding this comment.
Reviewed d481e6ea387b659a38976bce86c6561341921613. The two ANAN cells and dispatch paragraph requested in my prior review are corrected against the new base; the scope remains the stated documentation register and required checker.
| File | Scope | Verdict |
|---|---|---|
radio-feature-matrix.md and .json |
61-control register and sidecar | In scope; one policy-framing blocker below |
check_radio_feature_matrix.py |
source-derived checker | In scope |
static-checks.yml and architecture index |
CI registration and link | In scope |
Blocker: the vocabulary says H is a good cell and readers should treat every hidden control as done. The maintainer's #5262 M3a rule in docs/style/theme-style-guide.md §4a says individual controls must remain visible in an unavailable state with an announced reason; docs/architecture/radio-capabilities-map.md calls remaining hide sites M3b migration work. The register may accurately record today's hidden state, but it must not present every such site as the completed design. Please distinguish observed H from policy-compliant applet hiding or pending individual-control migration, and update the introductory explanation accordingly. This is the unresolved thread on line 98.
The current GitHub snapshot reports five successful checks on this head. I inspected the complete changed-file inventory and relevant diff; I did not rerun the checker locally after establishing the policy blocker. The seven V run citations are format checked by CI; their external hardware records were not verified here. No new test target or socket peer is added.
I cannot recommend merge while this contradiction, the unresolved thread, and the active change-request gate remain.
Non-blocking follow-up: the checker does not pin the feature roster, so deleting a row from both documents can still pass; a roster assertion would make omissions visible. This is not a merge prerequisite.
Main moved 20 commits under the register, including aethersdr#5879, which gave the HL2 explicit FM capabilities. The six HL2 FM cells it changes are corrected in the next commit, against the generator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ved. Principle VIII. aethersdr#5879 declared FmTonePresentation::Hidden and hasFmRepeaterOffset = false on the HL2, so its four tone rows go D -> H and its two repeater rows P -> H. The provenance example that used the HL2 now uses the Demo backend, which still holds both halves, and the inherited-default P count drops to four. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g-M3b M. Principle VI. The vocabulary said `H` is a good cell and to read it as done. The aethersdr#5262 M3a ruling (theme-style-guide.md §4a) keeps hiding only for a whole applet; an individual control stays visible, dimmed, with its reason announced, and radio-capabilities-map.md schedules the remaining hide sites as M3b. `H` now means an applet hidden wholesale, and `M` a single control gated away where §4a wants it dimmed with a reason. Every one of the 89 hidden cells is `M`; no row is hidden by hiding its applet. The split is authored, because widgets do not yield to a parser. Each record carries a `hide` block with the granularity, the observed form (hidden 60, disabled 10, relabelled 11, not-closed 8) and the gate's file and symbol. The checker fails on a hidden cell with no block, a code that contradicts its granularity, a site whose symbol has left its file, and a block left on a row that no longer hides anything. Where the form is not one §4a names, the cell is `M` and the note says why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@jensenpat, this addresses your 06:24Z review. The head is now Two caveats first. The PR description still has the old vocabulary line (" Your requests, one by one:
Main had moved 20 commits, so I merged it first. #5879 changes six HL2 FM cells: four go from D to H and two from P to H, and all six are now 🤖 Generated with Claude Code |
…o. Principle VIII. rx/rf-gain, tx/keying, tx/power and tx/filter were verified on an HL2 that had a hardware fault (bad joint at AD9866 pin 1, the Tx[5] line: RX gain bit 5 and TX DAC bits 11/5 stuck high; see the correction comments on aethersdr#5354 and aethersdr#5943). Re-measured after the repair on 2026-09-27, into a dummy load: - rx/rf-gain: codes 0..60 rise monotonically; the d103 'fold' was the defect. - tx/keying: USB, LSB and CW keyed; the tone sits on opposite sides for USB and LSB. - tx/power: forward power monotone across 0..100 % drive. - tx/filter: a 200 Hz tone passes at 150/3000 and is stopped at 300/2700. Cells unchanged (all four stay V); only the citations move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed exact head 95812884a0e9e95a02ae42f55efa3eb22c93283b against current main 68cdf661031fcc3358c768faf259478f416a7c0b.
Issue fit
No closing issue is linked. Documentation additions do not require an RFC under GOVERNANCE.md. The change delivers the claimed 61-control register, JSON sidecar and source checker, but is not ready to merge: the clean current-main merge fails its new CI step, and the checker already misclassifies a supported Icom control on the PR head. The earlier H-as-done objection is addressed by the new H/M distinction; that is not this review's blocker.
Scope
| File/group | Change | Claimed? | Verdict |
|---|---|---|---|
docs/architecture/radio-feature-matrix.md and .json |
GUI/headless register, evidence and migration classifications | Yes | In scope; correctness findings below |
tools/check_radio_feature_matrix.py |
Source parsing, derivation and strict drift check | Yes | In scope; parser finding below |
.github/workflows/static-checks.yml |
New strict checker step | Yes | In scope; current-main integration fails |
docs/architecture/README.md |
Register index link | Yes | In scope |
No unexplained commit or file, production edit, new capability/settings/protocol surface, CHANGELOG entry or changed test target. No socket-owning test or synthetic peer is introduced. The body's scope checklist holds.
Blockers
-
[P1] Regenerate the ANAN audio cells against current main before adding the required gate.
docs/architecture/radio-feature-matrix.json:501,:512,:540,:551,:579; corresponding Markdown rows atradio-feature-matrix.md:255-257and:352-353; CI invocation at.github/workflows/static-checks.yml:292.The head alone passes
--strictwith 366 cells and zero disagreements.git merge-tree --write-tree upstream/main HEADis clean, but an actual scratch merge onto68cdf661followed by the same command exits 1, reporting exactly:rx/audio-mute/anan: GUI D -> W rx/audio-gain/anan: GUI D -> W rx/audio-pan/anan: GUI D -> W rx/audio-mute/anan: headless R -> W rx/audio-gain/anan: headless R -> WMain's #5992 (
0817cec5) implements all three seam verbs and declares engine-ownedreceiveAudioControl(AnanBackend.cpp:415,:853-884on the merged tree). This is documented source drift, not an app/build failure. Merge current main, regenerate those five cells in both documents, update the affected ANAN trace/counts and rerun--stricton the exact integration result. Merging the present head would install a failing step in Static checks. The five successful hosted checks were created September 27, before this current-main state; they do not validate this merge. -
[P2] Do not treat nested capability construction as an absent capability.
tools/check_radio_feature_matrix.py:345-347,docs/architecture/radio-feature-matrix.json:135,docs/architecture/radio-feature-matrix.md:245.capability_assignments()only recognizes directc.FIELD = ...assignments. Icom constructsc.rxFilterControlthrough subfield assignments andpresets.append(...)(IcomCivBackend.cpp:582-594on the reviewed head), so the parser misses every write. Empirically, the unmodified head reports:Icom rxFilterControl assignment: None rxFilterControl default: '' setSliceFilterPreset override: True derive(rx/filter-preset, icom): ('H', 'every gating capability is false for this backend')That false absence becomes committed
M. For known/scope Icom models, the backend publishes FIL1/FIL2/FIL3;RxApplet.cpp:3230-3251uses those identities and forwards clicks throughRadioModel.cpp:4507-4519tosetSliceFilterPreset. Thus the record's explanation that the preset meaning was withdrawn does not describe the profiled Icom column. Recognize nested construction/mutations, evaluate the relevant record shape, and correct the Icom cell with its existing model-profile qualification. Add a socket-free Python regression proving this populated record is not parsed as absent. This is reproduced parser output plus source tracing; no Icom GUI/hardware session was run.
Nits — explicitly non-blocking
tools/check_radio_feature_matrix.py:875-881: range extraction drops negative signs. Both{Authority::Engine, -1, 100}and{Authority::Engine, 1, -100}returnENGAGEDin a scratch parser probe, althoughModelSliceFrequencyTargetrejects them. Pan-range derivation also lacks the ceiling enforced byModelReceiveControlTarget::validRange. Current cells do not contain those malformed ranges; harden parsing or return unknown for unsupported expressions.tools/check_radio_feature_matrix.py:477-484: the worked provenance example still says HL2 assigns Legacy and inherits repeater support. This head explicitly withdraws those features; the document correctly moved the example to Demo. Update the checker comment too.
What I tried to break
- Local Python 3.14: exact-head strict check passed, clean current-main integration strict check failed as above;
git diff --checkpassed. The macOS system Python 3.9 cannot evaluate this tool's Python 3.10+ annotations; CI explicitly supplies Python 3.12, so that interpreter mismatch is not a CI defect. - Six scratch mutations all failed with their expected diagnostics: uncited V, cited V over a dead cell, missing hide classification, stale hide symbol, H/M mismatch, and Markdown-only cell drift. Restored the scratch files and confirmed the head passes again.
- Engine-boundary, touchpoint-manifest, capability-record and command-plane gates each passed on both merge base and PR head, with byte-identical output. No baseline or touchpoint manifest grows in this PR.
- Source-reviewed the H/M policy correction and traced the Icom preset path. No app build, CTest or bridge session: this changes a documentation-analysis tool; demo cannot validate an Icom-specific capability declaration. No hardware was accessed. The seven external V bench records remain unverified here. A separate model-invocable code-review skill was unavailable; an independent parallel source audit was cross-checked against the parser and call sites.
Recommendation
Request changes. Correct the nested-record derivation and refresh the register on current main, then rerun the checker and hosted checks at the final head. The older open threads and active change-request decision also remain merge gates; do not bypass them.
|
Closing as "not filed" at the request of the operator. The register has gone stale three times across ANAN and Icom since the initial PR, and is expected to drift again soon after merging. This approach is withdrawn instead of landing the new required gate. No merge. |
Summary
A support matrix normally answers "did the backend declare it" or "is there a
control on screen". Neither is the question an operator has, and neither can
tell a working control from a dead one: the widget moves, the model commits, a
readback confirms the value, nothing logs and nothing signals.
This adds the third artefact — 61 operator controls × 6 radio families = 366
cells, scored by where the click ends — plus the four dispatch traces behind
it (Flex, Icom CI-V, ANAN P2, RTL-SDR), a JSON sidecar, and a checker that
generates the derivable cells and diffs them so the document cannot quietly
stop being true.
docs/architecture/radio-feature-matrix.md(644 lines)docs/architecture/radio-feature-matrix.json(1796 lines)tools/check_radio_feature_matrix.py(1221 lines)--strict.github/workflows/static-checks.ymldocs/architecture/README.mdradio-capabilities-map.mdNo production code changes. No capability field added.
What it says today
W=145 V=7 P=5 R=22 H=0 M=89 D=89 U=9over 366 cells.(Updated 2026-09-26 at
727a5685, after @jensenpat's review:origin/mainmerged in asb72f53c4, and the hidden cells split per #5262 M3a.His now only a whole applet hidden, which M3a permits;Mis one control gated away wheredocs/style/theme-style-guide.md§4a says it must stay visible and dim with an announced reason, i.e. pending M3b. All 89 formerly-Hcells trace to individual-control gates, soH=0; eachMrecord names the file and function of its gate, and--strictfails if that entry is missing, contradicted or stale.)(Rebased onto
main2859fdefon 2026-09-24. Two ANAN cells moved with upstream:rx/rf-gainD->W viaP2Client::setStepAttenuationDbfrom #5820, anddsp/noise-blankerH->W from #5824. The tallies quoted further down in the #5892, #5907 and Corrections from review sections are the figures those experiments actually produced at base7c8fdf84, and are left as they were run — the conclusion in each case is that no cell moved, which still holds.)Wreachable ·Vverified on hardware ·Pphantom (reports success, readsback cached state) ·
Rrefuses visibly ·Ha whole applet hidden on a capability (permitted byM3a) ·
Man individual control hidden where §4a requires it visible and dimmedwith a reason (pending M3b migration, not finished) ·
Ddead ·Uthegenerator declined to decide.
The 89
Dcells are the point — live widgets whose intent dies silently —and they concentrate in three families:
∅). There isnothing to gate on, so the controls are simply live everywhere. The SQL button
is a radio-side DSP control that never joined
VfoWidget::applyRadioSideDspVisibility(), the one owner of radio-side DSPvisibility.
canTransmitrefusal isapplied at the keying sites only. RF power, mic gain, TX monitor, TX audio
monitor, speech processor and VOX run straight into base no-ops on three
families, and
hostModulates = truemakes ANAN worse by pushingsetTxPowerand
setMicGainonce on every connect. One guard, already written, applied inmore places turns six
Dcells intoRcells on three radios.Two tables, two derivations — and that split is the finding
The GUI matrix and the headless matrix are built by different methods. The
test that decides which is honest is one question: does anything refuse when
the declaration is absent?
hasFmRepeaterOffsetis declared
= true), the gates are hand-written and optional, andControlAvailabilityRegistry— the one mechanism built to enforce them — hasno production consumer (a registered unit test and nothing else). So GUI
cells are derived from
reachability, and a declaration is only a cross-check.
aetherd, something does, and it is written into the target.ModelReceiveControlTargetopens every admission withcaps.<record> && known(authority)and returns the typed errorcapability.unavailable;ModelSliceFrequencyTarget::validCoveragedoes thesame with a range. There the declaration is the contract, so the headless
cells are derived from the record.
Deriving the headless cells the GUI way would print a phantom
Wfor everyverb the daemon never exposes — the override exists and nothing headless calls
it. So a feature with no
aetherdmethod gets no headless cell at all; it isnamed in prose instead of being scored.
The case that proved the precaution earns its keep.
validCoverage()refuses on
minimumHz <= 0, and bothFlexBackendandAnanBackenddeclaresliceFrequencyControlas{…, 0, 0}(Authority::RadioandAuthority::Enginerespectively). Headless tuning is therefore refused onboth — on the family whose GUI column is otherwise the fullest. Reading the
authority alone, the GUI way, would have printed
W.Why the check cannot be satisfied by a token edit
A gate that says "edit the matrix when you edit a capability" enforces a
change, not the right change, and a counter can be paid off with a comment.
This one generates the derivable cells from the source and diffs them against
the committed record and its sidecar. A diff cannot be paid off.
It derives the four questions a parser can settle — is the capability declared
and to what (three answers matter: true, false, and never assigned), is
the virtual overridden, is the base
Q_UNUSED, is the control gated above theseam — plus two with teeth: a runtime-published label list, and whether a stored
value is ever read back.
DagainstPis decided by PROVENANCE, not by any field's value. Thequestion is did this backend ask for this control? It assigned the gating
capability itself and did not implement the verb — a bug in that backend,
D.It never mentions the field and a permissive default offered the control on its
behalf — a gap in the default, one assignment repairs it,
P.PERMISSIVEisreachable only from
DEFAULT_TRUE, which is returned only whencapabilities()carries no assignment at all, so an explicit assignment can never produce a
P.Two adjacent FM rows show why the value cannot be the discriminator:
FmTonePresentationdefaults toHiddenandLegacyturns the tone controlson, so the HL2 — which assigns
Legacy— asked, and its tone cells areD;hasFmRepeaterOffsetdefaults totrueand the HL2 never mentions it, so therepeater rows one line below are
P.Pitself has two grounds and they are different repairs, so the checkerlabels each and prints the split: six inherited-default cells
(
fm/repeater-*on HL2, ANAN, Demo —c.hasFmRepeaterOffset = false;fixeseach) and one cached-readback cell (
rx/filteron RTL, where the backend didask, stores the edges, echoes a confirming
SliceDelta, andRtlSdrDdcneverreads them — real work).
Vis never generatedNo static read proves a radio obeyed. The checker asserts only that a
Vcarries a run citation, and that the generator independently derives
Wforthe same cell — a
Von a cell the source calls dead is either over-claimed orthe generator is broken, and both need a person.
All seven
Vcells are HL2, each citing a bench run that answered from theradio's own MAC:
rx/frequency(d114),rx/filter(d111),rx/pan-bandwidth(d101),
rx/rf-gain(d103),tx/keying(d126),tx/power(d99),tx/filter(d110).
d77 and d87 produce no
V, however good their numbers are, because they ranagainst
hpsdrsimon loopback. A simulator agreeing with the software thatdrives it is not evidence that a radio obeyed.
The nine
Ucells, and why an honest "undecided" is the pointBoth reasons are the same shape — the base class forwards the intent somewhere
the generator will not follow.
rx/xit-offseton Icom, HL2, ANAN, RTL, DemosetXitOffsetis an alias intosetRitOffset. The intent is reachable, but not necessarily this control: on Icom the alias is correct because the radio has one shift register; on a two-register family it would silently write the other one.Wwould launder that,Dwould be false on Icom.mem/recallon HL2, ANAN, RTL, DemoapplyMemoryRecallDetailscallssetSliceFmRepeater, which fans out into four FM setters that are themselves no-ops on those families, and then returnstrue. A caller checking the return value is told the recall succeeded. Whether the operator notices depends on which memory fields were set, which the generator cannot know.Uis a legitimate committed value, accepted only where the generator emittedU— committingUover a decided cell is a diff and fails.Relation to #5892
@rfoust's #5892 (aetherd: seed backend receive contracts and correct Icom
capacity, implementing #5890) adds
docs/architecture/backend-receive-contract-tests.mdin this same directory,and separates "daemon capability declarations, actual request dispatch/
configuration, and independently injected radio observations" — the same axis
this register scores, in different words. They are complementary, and they
agree where they overlap.
exhaustive over 61 controls × 6 families and finds gaps nobody has tested
yet — a
Dcell is a control no test exists for, because a test would havecaught it.
handful of operations, through injected transport and state seams.
Two independent derivations landing on the same answer is the useful part.
#5892's doc records that "Icom and ANAN mode/filter remain unadvertised to
daemon clients; that does not mean the desktop implementations are absent", and
that Demo's stored filter state does not justify advertising a daemon filter
operation. This register, derived separately from the capability records, scores
headless
rx/modeandrx/filterasRfor Icom and ANAN and headlessrx/filterasRfor Demo, while the GUI table scores Icom mode and filterW. Same conclusion, two methods.On the Icom capacity correction: nothing here contradicts it, and I checked
rather than assumed. This register asserts no capacity numbers —
pan/createandpan/removeare reachability cells,maxPanadaptersappearsonly as the named gating field, and the generator reads a numeric capability
only as
> 0. Applying #5892'sc.maxPanadapters = m.hasScope ? 1 : 0;to alocal tree and re-running
--strictgives an identical matrix:366 cells, W=143 D=94 P=7 R=22 H=84 U=9 V=7, 0 disagreements with the source. TheIcom
pan/*cells areRfor a reason independent of the number — the basereturns a refusal the caller can see and Icom does not override it.
Where #5892 could feed this register, stated precisely. Its contract tests
are the natural future source for automating what
Vdoes by hand. Onequalification worth making rather than glossing:
Vhere means a real radioobeyed, and #5892 explicitly disclaims firmware convergence — its own table
says each family's tests do not establish "firmware acceptance" or "live Metis
delivery". So a passing contract test earns a class between
WandV—dispatch-verified, stronger than reachability, weaker than a radio moving — not
Vitself. If that class is wanted, #5892's suite is where its citations shouldcome from, and this checker already has the shape to carry it:
Vis the onecolumn it refuses to generate and only validates.
Run against a live migration before merge — what it caught, and what it did not
A register that is only ever run against the tree it was written from proves
nothing about drift. So before merging it,
--strictwas run against #5907(@rfoust, aetherd: migrate receive controls behind the backend seam) — the
first bounded receive-command migration, and exactly the kind of change this
document is supposed to survive. Both halves of the result belong here.
What it did NOT catch: zero cells moved, and that is correct.
--printisbyte-identical between #5907's head and
origin/main(bothmd5 bc8987b9ff0316870f303a959f25f561), and the committed matrix derivesunchanged on his tree:
366 cells, W=143 D=94 P=7 R=22 H=84 U=9 V=7. Themigration is additive by construction —
SliceModelgains intent signals,FlexBackendgainsrequestSliceTune/requestSliceFilter/requestSliceAgc, and the seam verbs the register actually scores(
setSliceFilter,setSliceAgc) were already overridden onmain. Nothingabout where the click ends changed, so the cell derivation was silent. A
register that reported movement here would have been wrong.
What it DID catch: a real relocation, through the documentary side-check.
Exit 1 on two
feature-matrix-stale-alt-patherrors —rx/filter/Flex andrx/agc-threshold/Flex. Both are literally true. Onmain,SliceModel::setFilterWidthends insendCommand(QString("filt %1 %2 %3")),SmartSDR wire text built above the seam; on his branch that body notifies an
intent and the string lives in
FlexBackend::setSliceFilter, below it.'filt 'no longer appears inSliceModel.cppat all.And the parse GREW, which is the strongest evidence here that the derivation
tracks the source rather than agreeing with a still tree. Against his head the
generator finds 82 seam virtuals where
mainhas 79, and 24FlexBackendoverrides where
mainhas 21. Nothing was lost, no backend's override setshrank, and no vacuity floor was approached. The quiet-degradation failure mode —
a parser that falls over and derives every cell the same way — is ruled out by
measurement rather than by argument.
The defect that field test exposed, and the fix
The two errors were right and the check was wrong. Every
alt_pathin thisregister documents an above-seam bypass — forty of them, all Flex — and that
work-stream exists to delete exactly those. So a boolean "does the file still
contain both" raises
feature-matrix-stale-alt-pathon every success ofit. After the third green migration someone learns to wave the error through,
and it is then worthless on the day it finds real drift.
Fixing that by downgrading it to a warning would have been the same disarming by
a different route: a check that cannot fail is the thing this register exists to
replace. So the boolean became a classification. The question is not "did the
triple break" — it breaks identically either way — but did the evidence get
weaker, or did a weaker piece of evidence stop being needed?
RETIREDrequires all four, and each rules out one way the cheerful readingcould be wrong:
has the bypass and merely names it wrong, which is an unrecorded rename;
Q_UNUSEDno-op — a compiler-checked fact, not another text match, and thereason
derive()never consulted the record in the first place;W— the alt path argued reachability, and if thecontrol stopped being reachable then something was lost, not retired;
positive half, which catches a "migration" that moved the call and forgot to
send anything.
The notice is an edit request, not a status line: it names the row, the
family and the two edits (drop the
alt_pathfrom the sidecar, drop the*from the document) and repeats every run until someone makes them.
One case it cannot separate, said out loud rather than glossed. Where a
backend already overrode the verb and already carried the same wire token
below the seam — three records today,
rx/filter,rx/agc-thresholdandrx/pan-centeron Flex — mangling the above-seam literal yields a treeindistinguishable from the migration, because in every respect this register
scores it is the same tree: the cell is still true and the bypass no longer
carries that text. The other 37 of the 40 records are load-bearing — the
derivation rests on them — and breaking one of those moves the cell, so
feature-matrix-driftfires beside the alt-path error. That is why perturbation6 below is aimed at a load-bearing record.
After the change:
origin/maingives366 cells, W=143 D=94 P=7 R=22 H=84 U=9 V=7, 0 alt paths retired, 0 disagreements, exit 0 — identical to before.#5907's head gives the same 366 cells,
2 alt paths retired,0 disagreements, exit 0, with twofeature-matrix-alt-path-retirednoticesnaming the two records to delete.
I have not commented on #5907 and have not contacted @rfoust. Whether and
how to raise any of this with him is ON8ST's call.
Two smaller repairs in the same tool
U-provenance breakdown was gated onif reasons and not errors, soit vanished exactly when a run was failing and you most wanted it — an
unexplained
Uis very often the same parse failure that produced the errorabove it. Ungated; it now prints on every run.
LIVEis now read comment-free. The old predicate searched raw text, so awire token surviving only in a
// FlexAPI: "…"comment kept the claim alive.All forty records pass either reading today, so nothing moved — but the
comment-only case is now caught (perturbation 6e).
Verification
python tools/check_radio_feature_matrix.py --strictpasses on the committedmatrix, rebased onto current
main. The other ninetools/check_*.py --strictalso pass, before and after that rebase.
It fails on each of eight deliberate perturbations, all re-run on the
rebased tree:
dsp/squelch/HL2D→W)feature-matrix-driftfeature-matrix-doc-driftV(rx/frequency/HL2)feature-matrix-uncited-vVon a cell the source calls deadfeature-matrix-impossible-vHl2Backend.hfeature-matrix-drifttx/power/Flex,rfpower=)feature-matrix-stale-alt-pathandfeature-matrix-drifthasSquelchadded toRadioCapabilities.h— the∅probefeature-matrix-absent-probeIRadioBackend.hemptiedfeature-matrix-vacuityPerturbation 6 now has five more shapes, because the rot/retirement split is
the part of the checker most easily turned into a rubber stamp and it has to be
shown failing where it should and passing where it should:
tx/power/Flex)stale-alt-pathclassified ROT ("FlexBackend does not override setTxPower, so this record was the only evidence the control reaches the radio at all") anddrift(W→D)rx/filter/Flex)setFilterWidth→setFilterEdges)'filt ', so the bypass is still there and only the symbol is wrong — a rename nobody recorded"tx/power/Flex)Q_UNUSEDno-opdrift(W→D)Only 6b is green, and 6b is the retirement this change exists to recognise.
Perturbation 7 is the one worth naming:
∅is a claim about absence, whichis exactly the claim that rots silently when someone adds the field. The probe
makes it fail loudly instead. Perturbation 8's floors return 1 regardless of
--strict— a parser that finds nothing derives every cell the same way, andthe diff would then pass on a matrix that had stopped meaning anything.
Two further checks pin the provenance rule specifically. Making
AnanBackendassign
hasFmRepeaterOffset = true— the same permissive value it was alreadyinheriting — flips both cells
P → Don provenance alone; assigningfmTonePresentation = Legacyflips the four tone cellsH → D. Neither testchanges a value the code did not already have, so neither could pass under a
rule that keyed on the value.
Corrections from review
@aethersdr-agent's review (approve with nits) found one false statement about
current code, in the half of the document the checker cannot see — exactly the
decay class this register exists to stop:
P2Clientexposes a secondQ_INVOKABLElive setter,setDdcRateLive, and, since feat(anan): drive G2 attenuation from RF Gain. Principle V. #5820, a third,setStepAttenuationDb(@jensenpat's correction on the rebase — it is whatmoved
rx/rf-gain/ANAN fromDtoW).setDdcRateLivewhich returns false withoutsending unless the session is running and otherwise resends the whole
DDC-Specific packet;
AnanBackend::finishRateChangeinvokes it three timesacross the settle window, and
startP2ClientSessionis now reached from theconnect path alone. The rate no longer costs a stop/reconfigure/restart. The
section is rewritten. No cell moves —
rx/pan-bandwidth/ANAN staysWeither way.
rx/dial-lock's note overstated.hasRadioDialLockis read in exactlyone file above the seam,
RadioModel.cpp— which is the read theDcells rest on.
IcomCivBackend.cppreads it below the seam in four places,on the one family whose cell is
W. The note now says so.ControlAvailabilityRegistryis not "referenced by nothing but itself":it has a registered unit test. The claim that survives is no production
consumer, and that is now what both this body and the document say.
MIN_CELLS's own comment said 62 rows; there are 61.--strictafter all four: 61 features × 6 backends = 366 cells,W=143 D=94 P=7 R=22 H=84 U=9 V=7, 0 disagreements — identical to before, sono cell moved on prose changes. All ten
tools/check_*.py --strictpass.What the checker cannot see
Stated so a green run is not read as more than it is: whether the radio obeyed
(only
Vclaims that); whether a control reaches its signal, since thewidget-to-model wiring is Qt lambdas in constructor bodies; whether a model gate
refuses visibly or drops silently (the
visibleflag is authored, not derived);and whether an override that reaches the wire writes the right register.
The
*mark is the weakest evidence in the document by design — an override isa compiler-checked fact, an alternate path is a file, a symbol and a wire
literal that all have to still be there.
What the check does NOT guard — the roster
Stated here as plainly as in the document, because it is the one way a green run
can be read as more than it is.
main()iterates the committed record —for rid, row in matrix.items()—so every check is a question asked about a row that already exists. Nothing
iterates the other way: nothing enumerates
IRadioBackend's seam verbs or thecontrols the GUI builds and asks whether each has a row. Two consequences, both
silent:
rx/agcfrom the JSON and its line from the table during review:--strictreported
60 feature(s) … 0 disagreement(s)and exited 0.MIN_CELLS = 200is a vacuity floor against a broken parser, not a rostercheck — over six backends it does not fire until the matrix falls below 34 rows,
so 27 of today's 61 rows could be deleted before it says anything.
So: the register proves the cells are true. It does not prove the roster is
complete. That limit is now written into the document itself, under "What a
green check does not prove — the roster is not guarded", and cross-referenced
from "Keeping this true".
The repair is a roster anchor — derive the feature list from the seam and
the GUI control inventory and diff it the way the cells are diffed, so an
addition or a deletion fails instead of passing quietly. It is a generator of a
different shape from this one and it is deliberately not in this PR; it is
offered as a named follow-up.
No capability field was added
tools/check_capability_records.pystays at its frozen 71 and this work doesnot touch
RadioCapabilities.h. The∅rows are recorded as absences with theprobe above, and the document says what shape can land instead: an
std::optional<…Control>record whose absence means no support, in the style ofReceiveAudioControl— or an interface through the seam, the wayAutoRfGainControl.hdid when auto-RF-gain hit the same wall.Constitution principle honored
Principle VIII — Evidence Over Assertion. The register replaces the
assertion "this radio supports X" with a cell derived from the source and a
CI step that fails when the source and the cell disagree. The one column that
cannot be derived,
V, is the one the checker refuses to generate and onlyvalidates against a citation.
Defects found, not filed
The four dispatch traces turned up ten defects, recorded in the document's own
table and not filed as issues here — each is a separate decision. They
include the
canTransmitguard applied only at the keying sites (#4 in thattable),
IcomCivBackend::hasRadioSideCwKeyeranswering two different questions(#5), and
RxAppletoffering"ANT1"/"ANT2"on radios that never claimedthem (#7) — an invented control rather than a dead one.
Test plan
Documentation, one CI step and one Python checker; no production code path is
touched and no build is required for correctness.
tools/check_radio_feature_matrix.pyis pure-Python source analysis with no build dependency.
tools/check_*.py --strictpass on the rebased branch.and so do the five further alt-path shapes (6a, 6c, 6d, 6e, 6f). 6b is green
by design and is the retirement.
--strictwas run against a live migration (aetherd: migrate receive controls behind the backend seam #5907) before merge, and againstorigin/mainafter the change: same 366 cells, 0 disagreements, exit 0 onboth.
Vcitations reference bench runs recorded before it.
Checklist
static-checks.ymlstrict check🤖 Generated with Claude Code