fix: refuse a deck with no frequency, instead of succeeding in silence - #451
Merged
Conversation
`fnec deck.nec` on a deck with no `FR` card exited **0 having written zero bytes
to stdout AND stderr**. A silent success is indistinguishable from a run that
worked, and it was one of four different answers to the same deck (FND-070):
- CLI: exit 0, nothing at all;
- GUI, two seams: refused, "deck has no FR card";
- `fnec_py::solve_deck_str`: raised, "deck has no FR card";
- `fnec_py::sweep_deck_str`: returned `[]`, at success.
That last one my own site enumeration missed; fable's design review found it.
It is the defect *inside* the frontend FND-070 credits with refusing — one
module disagreeing with itself, its sweep half returning the empty-result-
standing-for-an-error shape that `docs/json-output-schema.md` now tells
consumers not to read that way.
The predicate is `validate::no_frequency_error(freqs_hz, remedy)`, typed on the
**resolved list rather than on the deck**, and that is the load-bearing choice.
Frequencies do not have to come from an `FR` card: `--sweep-config` supplies
them for the CLI, the sweep tab's own range widgets for the GUI, the wire for
the worker. So this cannot live in `pre_solve_error` — the gate every frontend
already calls — because that sees only the deck and would refuse the working
`--sweep-config` case. Measured: an FR-less deck with `--sweep-config` solves at
both configured points, and **no test covered that** until this change added
one.
Sharing the predicate and not merely the sentence is what makes the sabotage
bite. Forcing the helper to return `None` fails the CLI gate, the GUI gate and
the pytest gate, while both controls stay green. FND-145's equivalent sabotage
left every CLI test passing, because the CLI had a second route; here there is
none, so every frontend notices.
**FND-084 is closed by the opposite of the fix it proposed.** That row asked for
"one line: emit `[]` before the early return". Rejected, for reasons the review
supplied and I had not considered: `[]` is printed only under `--output-format
json`, so text mode would have stayed silent and FND-070 survived intact; and
`[]` + exit 0 already means *solved, and there was no feedpoint to price*, so
spending it here would make "never solved" indistinguishable from a real result
— in the very document where, one PR ago, I wrote that an empty array had been
silently standing for an error and that consumers must branch on the exit code.
There was no contract to honour either: `json.loads('')` raises, so every
consumer in the tree already crashed on this deck and now gets a clean non-zero
exit.
BREAKING for anyone scripting `fnec` on a frequency-less deck and reading exit
0. Deliberately divergent from nec2c, which defaults such a deck to 299.8 MHz
(λ = 1 m) and answers it: on a 10.5 m dipole it reports 133.18 + j280.36 Ω,
pricing the wire as 10.5 λ — a plausible number for a deck the user did not
write.
Recorded rather than fixed: FND-150 (a bad `--hosts` path was never reported on
this deck, because the early return preceded reading the file) and FND-151 (the
frequency checks now live at two seams plus the worker's wire check; folding
them into a `pre_solve_error` that takes the resolved list is a four-frontend
signature change, deferred deliberately).
Gate: scripts/check-all.sh, EXIT=0, 15/15. Python gates run against a freshly
built wheel: 30 pytest tests pass, and both new ones fail under the sabotage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018p7FxX7QMWNJVNp9LbaLkB
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
fnec deck.necon a deck with noFRcard exited 0 having written zero bytes to stdout and stderr. A silent success is indistinguishable from a run that worked — and it was one of four different answers to the same deck (FND-070):fnec_py::solve_deck_strfnec_py::sweep_deck_str[], at successsweep --resonanceThat fourth row my own site enumeration missed; the design review found it. It is the defect inside the frontend FND-070 credits with refusing — one module disagreeing with itself, its sweep half returning the empty-result-standing-for-an-error shape that
docs/json-output-schema.mdnow tells consumers not to read that way.Why the predicate is typed on the resolved list
validate::no_frequency_error(freqs_hz, remedy)takes the resolved frequency list, not the deck. That is the load-bearing choice, not a detail.Frequencies do not have to come from an
FRcard:--sweep-configsupplies them for the CLI, the sweep tab's own range widgets for the GUI, the wire for the worker. So this cannot live inpre_solve_error— the gate every frontend already calls, and the obvious home — because that sees only the deck and would refuse a working, documented capability.$ fnec --sweep-config sweep.toml no-fr.nec # measured: exit 0, both points solvedNo test covered that capability until this change added one.
docs/cli-guide.mdsaid--sweep-config"overrides theFRcard", which is true and incomplete; it also supplies one.Sabotage — this is the improvement over FND-145
Forcing the helper to return
None:sweep_contractgui_smoke--sweep-configsolve, FR-deck sweep)FND-145's equivalent sabotage left every CLI test passing, because the CLI had a second route to that refusal. Here there is none, so every frontend notices — which is what sharing the predicate buys over sharing only the sentence.
FND-084 is closed by the opposite of the fix it proposed
That row asked for "one line: emit
[]before the early return". Rejected, for reasons the review supplied and I had not considered:[]is printed only under--output-format json, so text mode would have stayed silent and FND-070 would have survived intact — it fixes the symptom, not the defect;[]+ exit 0 already means solved, and there was no feedpoint to price (measured oncorpus/dipole-ex1-freesp-51seg.nec). Spending it here makes "never solved" indistinguishable from a real result — in the very document where, one PR ago, I wrote that an empty array had been silently standing for an error and that consumers must branch on the exit code;json.loads('')raises, so every consumer in the tree already crashed on this deck and now gets a clean non-zero exit instead.FND-070 and FND-084 have sat adjacent in the ledger since the 2026-08-28 audit proposing opposite fixes, and nobody noticed. Closing either as written would have entrenched the other.
Breaking
Breaking for anyone scripting
fnecon a frequency-less deck and reading exit 0. Deliberately divergent from nec2c, which defaults such a deck to 299.8 MHz (λ = 1 m) and answers it — on a 10.5 m dipole it reports 133.18 + j280.36 Ω, pricing the wire as 10.5 λ. A plausible number for a deck the user did not write.This is the second breaking change to this contract paragraph, and it follows the maintainer's "refuse everywhere" decision on the identical no-
EXcase rather than a fresh instruction — flagged rather than assumed.Recorded, not fixed
--hostspath was never reported on this deck, because the early return preceded reading the file.pre_solve_errorthat takes the resolved list is a four-frontend signature change, deferred deliberately.Gate
scripts/check-all.sh→ EXIT=0, 15/15, on the committed tree. Python gates run against a freshly built wheel: 30 pytest tests pass, and both new ones fail under the sabotage.🤖 Generated with Claude Code
https://claude.ai/code/session_018p7FxX7QMWNJVNp9LbaLkB