Open issues, by the harm they do¶
84 open, sorted into six tiers by the kind of harm each one does rather than by age or label. Most carry no label at all, so this ordering is the triage rather than a view onto one that already existed.
Derived 2026-08-21 by make issues, which reads the live issue list. Nothing re-reads it for you, so treat the date as the age of this page — the tier assignments below are committed in issue-tiers.toml and reviewed like code.
| Tier | What it means | Open |
|---|---|---|
| 0 | Breaks for a user | 0 |
| 1 | A gate that does not gate | 15 |
| 2 | A claim nothing measures | 17 |
| 3 | Measured cost | 6 |
| 4 | Cannot be reached | 15 |
| 5 | Convergence and hygiene | 31 |
1 of them has a pull request open against it; the Status column says which.
Tier 0 — Breaks for a user¶
Reproducible through an interface someone actually uses — a crash, a race, a link failure, or a stub documenting a signature the extension does not have.
Empty.
Tier 1 — A gate that does not gate¶
This repo's own doctrine turned on itself: a claim nothing runs is prose. Each is a check that reports green because it cannot see the thing it names — several confirmed by sabotage.
| Issue | Summary | Status |
|---|---|---|
| #659 | make test can pass against a stale build — no prerequisite on build | open |
| #682 | test: §12's blob-determinism check compares in float at 1e-9, so it asserts float-equality not bit-identity | open |
| #692 | characterization: the category has no envelope gate and no figure-freshness gate | open |
| #693 | gen_jm_pin --check: a version is 'recorded' if any old arrow line names it, so a rollback passes silently | open |
| #696 | docs-check and docs-drift-check keep two copies of the generator --check list, and CI only runs one | open |
| #701 | make bench skips function-only modules: bench_util_core builds and is never run | open |
| #716 | Validation figures and CSVs are gated by nothing — validate-check only compares rendered markdown | open |
| #736 | check_doc_face_parity compares Examples/Raises only, so the divergence that shipped was invisible — and lock-detection.md is stale on CarrierNda | open |
| #744 | check_lifecycle: verify the phase-9 example too, by having each report cite it | open |
| #780 | The gallery has no gate: 21 committed assets are stale, one script is in no target, and make gallery is red |
open |
| #847 | Phase 4's sabotage rule is stated on three pages and executed by none | open |
| #857 | make gallery runs a script the examples gate deliberately skips, so it cannot pass | open |
| #871 | check_stimulus_sources: a hand-computed Es/N0 sigma is an invented level convention, and no marker catches it | open |
| #919 | libdoppler.a's declared link line is only checked where pthread is a separate library | open |
| #936 | check_stimulus_sources: the pulse marker's C branch cannot fire — it is matched per line against a signature clang-format always splits | open |
Tier 2 — A claim nothing measures¶
A header, report or design doc asserts a number or a behaviour that no test and no validator establishes. Not wrong — unestablished, which is a quieter problem.
| Issue | Summary | Status |
|---|---|---|
| #645 | MpskReceiver timing loop: two measured default changes not taken — DTTL for BPSK/QPSK, and bn_timing's knee | open |
| #646 | The resamp double-emit fix is pinned at RateSync level only — MpskReceiver, where it was observed, has no regression test | open |
| #652 | Matched pulse bank is laid out against the pre-fix resamp accumulator | open |
| #684 | test(symsync): the embedded timing NCO never states its rate, and the gate that would catch it never landed | open |
| #689 | DSSS test captures inject 3.01 dB less noise than their cn0_dbhz label, and correcting it exposes non-monotonic acquisition | open |
| #711 | agc: the header promises level-independent settling, and §6 measured otherwise | open |
| #715 | The DTTL-vs-Gardner tradeoff is measured only where DTTL wins: no SNR axis exists anywhere | open |
| #732 | carrier_nda: the acquisition contract needs the seeding number (|df| \<= bn/M), and the arm's absolute wall it does not name | open |
| #733 | carrier_nda: the half-symbol arm's 1/2 + 1/(M+1) coherent gain does not reproduce |
open |
| #734 | carrier_nda: the lock statistic's H0 spread is M-independent per look, and is not through the arm | open |
| #751 | The DTTL's "degrades faster at low SNR" claim is unmeasured — the RateSync validator adds no noise | open |
| #754 | MpskReceiver: false lock (negative lock, correct frequency and timing) at any m_out mpsk.md §8 would not derive — the cost of #644 | open |
| #814 | MpskReceiver: six header claims are covered by nothing in either language (F9) | open |
| #830 | The MpskReceiver validation report characterises the complex face only — the real face has no Python evidence | open |
| #831 | MpskReceiver: state the link and the requirement, derive the loops (mpsk-refactor 4.3) | open |
| #855 | The timing rate residual doubles with a carrier offset present, and nothing characterises the coupling | open |
| #932 | symsync_lock's published false-alarm rate moves 1.5x with -ffast-math, and its header states a conclusion neither value supports | open |
Tier 3 — Measured cost¶
Performance findings with a number attached. Filed, not fixed; each names the measurement that produced it.
| Issue | Summary | Status |
|---|---|---|
| #502 | Performance: prepare() scales O(n) with users instead of sharing RRC filters | open |
| #845 | make coverage's -O0 tree costs 6 minutes, and -O2 takes two bit-exactness tests red under CI's clang 18 | open |
| #890 | snr: both _db_series forms are O(nwindow) — they re-scan the window at every sample | open |
| #896 | Multi-config C benchmarks charge the clock ramp to whichever config runs first | in review (#941) |
| #898 | make coverage: ddc_fn_scaling.py times out, and the more cores you have the worse it gets | open |
| #905 | arith: the Q8 shift kernels are slower than their Q15 twins, not faster | open |
Tier 4 — Cannot be reached¶
The capability exists in C and no caller can get to it, or it exists and nothing tells anyone it does.
| Issue | Summary | Status |
|---|---|---|
| #545 | Reading a capture is undiscoverable and undocumented — enriched view over wfm_reader + a guide | open |
| #578 | Container UX: pulled image ships no examples — split into a runtime (compose + Python demos) and a build (C/jm consuming projects) image | open |
| #635 | Resampler's bank= is a working, undocumented constructor parameter | open |
| #658 | AGC: a seed that cannot fire on silence — buy back the acquisition spread | open |
| #661 | RateSync reports over-drive but is silent about under-drive | open |
| #668 | RateSync.clipped conflates transmitter over-drive with noise-crest clipping | open |
| #725 | wfmgen: --detached silently ignores --realtime, while the other two destinations honour it | open |
| #735 | carrier_nda_get_nco_freq is unreachable from Python and telemetry — both published frequency views are the same view | open |
| #750 | AGC has no signal-presence notion: on a noise floor it applies 59.9 dB and the next burst arrives hot | open |
| #762 | Generated PN/Gold sequence kinds are unreachable from any face | open |
| #813 | fec: virtual fill (4.4.2 shortened codeblock) is not implemented, so a frame off the 223*I grid has no path through the outer code | open |
| #844 | stream: ten public send entry points have ZERO execution — the CI8/CI16/CF32 slice of PUSH/REQ/REP, including the durable JetStream tier | open |
| #865 | doppler reads 131.0-B-3; B-6 is current — and it changed the randomiser default | open |
| #940 | DopplerChannel per-sample Doppler profile — deferred; the carrier needs the warping step, not a chord | open |
| #942 | wfmgen: sources cannot express clock Doppler — scalar + rate with range draws, over a stateful per-source channel | open |
Tier 5 — Convergence and hygiene¶
Duplication, stale pins, harness drift, and the long tail. Real, none of it urgent — and the tier that grows when the ones above it are held.
| Issue | Summary | Status |
|---|---|---|
| #535 | DsssReceiver: derive segments, adopt async's squaring carrier combine | open |
| #568 | RFC: Doxygen → docstring derivation — the jm-side plan, and where the F-series fits | open |
| #615 | CI: every action is on the node20 generation — bump to the node24 majors | open |
| #630 | GitHub Actions: every pin is stale (up to 5 majors), and the Node 20 forcing we rely on is temporary | open |
| #639 | CI is make-SSOT for every gate — the drift is all in the plumbing layer | open |
| #648 | resamp_execute_ctrl_push is ULP-sensitive: gcc 13 and gcc 15 disagree on the converged lock metric by 0.031 | open |
| #651 | Makefile diverges from the shared convention: no PYTEST command variable, and make test runs only ctest |
open |
| #655 | UBSan alignment ratchet: 821 misaligned member accesses | open |
| #676 | Telemetry: an over-long prefix silently aliases two probes onto one id, and the attach reports DP_OK | open |
| #677 | C tests: 88 hand-rolled CHECK macros, 9 of which abort at the first failure | open |
| #678 | util: no C test harness, and square_clip.c duplicates its own body | open |
| #685 | feat(telemetry): attaching a pipeline takes one call per object; the one-call form was written then lost to a redesign | open |
| #694 | make gallery: GALLERY_CHARACTERIZATIONS is plural but the recipe only works at n=1 | open |
| #712 | make docs-check gives a wall of false broken links when site/ is absent | open |
| #720 | clang-tidy: 172 findings block gating at zero — triage, judgement, and config, in that order | open |
| #721 | Three helpers are byte-identical across modules; zeropad_1d is the one that will drift | open |
| #730 | jm_test.h is deliberately not vendored: a jm 0.58.0+ test scaffold needs converting to dp_test.h | open |
| #756 | Retire wfm_synth_mls_poly() in favour of pn_mls_poly() | open |
| #757 | The docstring-coverage ratchet is slack in 24 of 26 modules | open |
| #760 | The waveform enum tables have three C homes, and the manifest claims one | open |
| #781 | mpsk_receiver_performance_demo: a BPSK trial declares lock with a clean constellation and SER 0.27, and .examples-skip's reason names a different failure | open |
| #802 | Cleanup plan for the test/measurement layer: a convergence plan, not a deletion plan (measured) | open |
| #853 | A frame is a field list and a stage list: fold both framers into one description | open |
| #856 | design/bpsk-receiver is superseded by the collapse and the view — triage its design doc, then close it out | open |
| #893 | bench: six benchmarks are declared at a level jm was never told about (three below an object, three inside c_deps) | open |
| #894 | Certify viterbi as its own object, rather than inside conv |
open |
| #900 | Audit: what is left to declare, and the capabilities with no Python path | open |
| #906 | The function-only modules are benchmarked in C but not in Python | open |
| #922 | Two hand-written out= parameters are invisible to the stub they should be published in | open |
| #933 | Migrate the four inline LCG streams off scripts/.private-rng-ratchet | open |
| #937 | Four validation harnesses hand-roll AWGN instead of calling the library's awgn, and nothing says they shouldn't | open |
How an issue gets its tier¶
By asking what goes wrong if it is never fixed, and nothing else. Age does not raise a tier and neither does effort — a one-line fix that stops a crash outranks a week of hygiene. The tiers are deliberately about harm rather than cost, so that the order to work in falls out of the table instead of being argued each time.
make issues fails if an open issue has no tier, or if a tier names an issue that is closed — so this page cannot rot in either direction without saying so.