Work the open issue tracker: ~100 issues, 26 of them already fixed - #145
Merged
Conversation
FR-CHAIN-030's tagged test compared four samples at latency 0 and never performed the null test its Verify: line specifies; FR-LIB-070's "disappear, change or are added" had no test spanning the *added* member; FR-LIB-040's search tests covered two of the six metadata fields searchable_text reads. Each requirement now has one test spanning it whole, and the narrow tests keep their assertions but lose the tag that over-claimed for them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
, #121, #122) A line gains min/max/default, or steps/default/values=<FNV-1a digest>. The digest rather than a count is what catches a reordering of stepped labels, which silently re-points an already-stored index. A changed shape is recorded as a stale file, not a build failure: D-10.1 reserves failure for a changed identifier or type, and several FRS section 5 ranges are stated as "at least", so widening one is legitimate. Recording it makes it a diff a reviewer reads. Also: format_version is now matched exactly rather than by prefix and is compared, so a future version is one violation instead of a pile of malformed lines and --write refuses to overwrite it; and ParamDescriptor::validate runs over every descriptor in the gate, so a self-contradicting descriptor fails xtask params-lock rather than only a test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
M14 already taught the gate to read a **Result: line; what was missing was what happens when there is not one. A missing verdict, a verdict line opening with no token, and a PASS contradicted by its own sentence were all treated as "unreadable, therefore uncovered" -- relaxable by --allow-uncovered, and recorded nowhere for the author to fix. They now abort the run upstream of --write, --allow-uncovered and every exit-status term, on the same footing as a malformed trace annotation: a bad input rather than a coverage gap. Ten documents gained or corrected a verdict line stating what their own prose already said. No verdict was promoted: the six Verify: M Musts that record no pass (FR-IO-030, FR-IO-050, FR-UI-030, FR-UI-040, FR-UI-050, FR-UI-070) are the same six before and after, and the required half of the gate stays green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
…102, #89) FR-STATE-020's corpus had no preset with a references section, so a writer and reader that agreed with each other on a wrong key name went undetected. Adds section 9's worked example as hand-authored bytes, both slots, embedded present and absent, with a real BLAKE3 hash. Renaming a key in both to_value and from_value now fails these two tests and nothing else. The UI intent test called host.dispatch itself. It now runs four real NamirUi::frame passes over a shared egui Context and drags the widget; dropping the dispatch in frame() fails it, which the old body survived. #89's premise is stale -- 608fdde landed the real-callback harness at M14. What remained is the integer-format path: a converter wrapping the real callback, which is what a device invokes when it will not take f32, ran under no harness. It does now, at both i32 and I24. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
FR-GATE-020's test used a bare decaying envelope with no carrier, so nothing it asserted rested on hysteresis. It now drives an 82.41 Hz E2 carrier and sweeps hold_ms across the FRS range, with a falsifier at the shortest hold. This exposes a real DSP defect, recorded rather than papered over: at hold settings below ~5 ms the gate does not meet FR-GATE-020. The detector's 1 ms symmetric one-pole lets the rectified carrier ripple ~9 dB p-p, wider than the shipped 3 dB hysteresis, giving 62 close events at hold 0. FR-GATE-010's table admits 0 ms, so this is in range. The tag is demoted to trace-partial with the gap named. FR-IR-030 skipped its stopband assertion whenever the measurement was None. Now unwrapped and asserted for all 24 pairs -- measured: every pair reports Some, so the assertion is unconditional. The .nam fuzz target only parsed; it now runs new_state and process_block at three block sizes. NFR-SEC-010 demoted to trace-partial: probe_metadata, the weights-free read the library scanner runs on every indexed file, is still reached by no target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
…132) The determinism test called generate_shared_corpus twice, and the second call was a cache hit of the first -- it compared a build against itself. It now builds two independent corpora into distinct roots under its own seed and compares all 10,000 files byte for byte, re-hashing each. Injecting a per-build counter into the seed fails the new test; the old shape passed it. The mutation corpus could not produce a null or wrong-typed field, which is the shape of the real post-M6 parser bug. Adds NullField and RetypeField, appended to ALL so seeds 0-3 stay byte-identical, with a test asserting some seed reaches "metadata.<field>": null over a real generated .nam. reference_infer_lstm was a paraphrase of the implementation it checks. It is re-derived from NAM/lstm.cpp at the pinned commit, structured like the C++ rather than like namir-nam, with four analytic tests that discriminate the plausible misreadings. Two residues are now stated in mod.rs instead of a claim of independence: the re-derivation was not blind, and the multi-layer facts rest on analytic tests rather than an external render. Float summation order is deliberately preserved -- generate_lstm calibrates from this function's RMS, so a last-ulp change would move every generated LSTM fixture including the pinned golden. Verified identical by hash. Checked all four risk items against upstream: none was a real misreading. The defect was that nothing established that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
FR-GATE-020 and NFR-SEC-010 become PARTIAL, each carrying its uncovered: text. Output of `xtask traceability --write`; not hand-edited. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
BiquadCoeffs::design panicked below a 4 Hz sample rate ("min > max ... min =
1.0, max = 0.4995"). Its floor is now lowered to meet the ceiling rather than
the ceiling raised to meet the floor: the issue's suggested fix would return a
design at the sample rate, i.e. above Nyquist, which the constant's own doc
calls meaningless.
#126 is filed as low severity on the grounds that design runs on a worker. It
does not. apply_param_direct -- documented wait-free, called from the CLAP
audio processor's host-automation path -- reaches it through EqStage::retarget
and IrStage's cut targets, so the panic was reachable on the audio thread and a
Result was never viable. That doc claim is corrected where it was made.
A panicked scan left the scanning flag set, so the library never scanned again
for the rest of the session. A ScanFlag guard releases it on unwind; the
explicit early drop preserves the documented ordering, where the flag must read
false before on_complete so a caller restarting from its own callback is not
refused. A job dropped without running now releases it too.
NaN into ParamValues::set stored NaN and wrote JSON null, which then would not
read back. It now takes the default, the same rule from_document_section
already applies to a non-finite number arriving from a file. Infinity keeps
saturating to the bound rather than resetting.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
The panic #52 names was already fixed at M14 (ec46de9): the convolver's c2r site falls back to silence. But nothing rejected a non-finite tap at load, and that was live on a path the issue does not name -- the resampled one, which is the common case for a downloaded 44.1 kHz IR at a 48 kHz engine: panicked at rubato-0.16.2/src/synchro.rs:182:14: called `Result::unwrap()` on an `Err` value: Imaginary part of first value was non-zero. That is rubato's own unwrap, before this crate sees a tap, so no guard of ours could reach it. On the rate-matched path the file instead loaded "successfully" and then produced non-finite output forever, from a load that reported success. Rejected rather than sanitised: zeroing a NaN is defensible, clamping an Inf invents a tap, and the two cannot be told apart at the point of repair. A silently repaired IR whose response is wrong is its own defect. Contrast was_truncated, which repairs and reports, because truncation is a policy applied to valid data -- a NaN tap is not data. No RT-path change: realfft's r2c fails only on buffer lengths, never on values. #53: build_schedule's assert was over-strict. With max_partition <= block_size the loop already yields a valid uniform schedule, so max_partition is floored at block_size and a 16384-frame offline render loads instead of panicking. Checked against the direct-convolution reference at -100 dB, not merely for absence of a panic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
#47, #49) All three loaded cleanly and then misbehaved inside process_block, which runs on the audio thread, so each is refused in from_file instead: wavenet.rs:1711: range end index 32 out of range for slice of length 8 (#46) wavenet.rs:558: range end index 16 out of range for slice of length 8 (#47) #46 is a real reference-supported multi-output model, not a damaged file -- NeuralAmpModelerCore derives NumOutputChannels() from the last layer array's head_size -- so it is refused as unsupported, the same mono-only scope limit in_channels != 1 already carries. The check reads the resolved head width, so A1's head_size and A2's head.out_channels are both covered, and a wide non-final head still loads, which every real two-array export has. #47 takes a different code from the one the issue proposed: inconsistent, not unsupported. The reference takes the layer's input width from this field while sizing the signal from in_channels, and never checks they agree; since in_channels is already pinned to 1, a first-array input_size of 2 is a file contradicting its own declared width. A genuine two-input model declares in_channels: 2 and is still rejected earlier, by name. #49: non-finite weights are rejected via a shared helper. Only deserialized JSON text can carry one -- serde_json writes non-finite as null, and rustc refuses the literal -- which is why no generated fixture ever had this shape. Extended past the issue to A2 activation parameters, which are weights by another name and reach f32 the same way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
…et (#74-#83) #74 is the real one: the ldmxcsr block declared preserves_flags, which the Rust Reference defines to include MXCSR's six sticky exception flags on x86 -- and ldmxcsr loads the whole register, clearing any flag raised since the guard was constructed. A false promise to LLVM regardless of EFLAGS. Option dropped, SAFETY rewritten. stmxcsr keeps it (it only reads), and the aarch64 msr fpcr blocks are correct as they stand: that rule covers NZCV and FPSR, which it does not write. #77, same file, same class: MXCSR and FPCR are per-thread, so a guard sent across threads restores one thread's mode onto another and leaves the first engaged for life. PhantomData<*const ()> makes it a compile error, as MutexGuard does. Both production call sites and both benches drop on the constructing thread, so nothing breaks. #75: SCHED_FIFO at max is 99 on Linux, where watchdog/N and migration/N live and threaded IRQ handlers sit at 50 -- outranking everything able to preempt a runaway audio thread. Now min+10, expressed relative to min because the Linux and Darwin ranges (1..=99, 15..=47) do not overlap in meaning. Read back on Linux as SCHED_FIFO priority 11. #81 is recorded, not implemented: Darwin promotes audio threads with THREAD_TIME_CONSTRAINT_POLICY, a deadline contract no POSIX priority expresses, so this module's success there is materially weaker than on Windows and Linux. macOS is secondary and no CI machine here can exercise a Mach binding. #76 is half done. Its proposed fix -- log the outcome from stream.rs -- would fail the build: stream.rs is on rt-logging's audio-thread list, and both callers elevate from inside the audio callback, where a format! also trips the allocation harness. diagnostic() returns a catalogued code with no allocation instead; the shells must carry the Copy outcome off-thread. Those caller changes are still owed. #80: OsError widens to i64, so 0x8007000E reaches a bundle as itself rather than -2147024882. #79: the bad-level warning bypasses severity admission, except at Off, whose contract is that no file is created. #83 reproduced for real by building for wasm32 -- three unused_variables, a -D warnings failure -- and fixed, plus the same defect in both test modules' imports. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
The detector was a 1 ms symmetric one-pole, so it tracked the waveform rather than its envelope: a rectified 82 Hz carrier rippled 9.54 dB peak-to-peak, wider than the 3 dB default hysteresis, so the ripple itself crossed the gap. Measured before: 62 close events at hold 0 ms, 61 at 1 ms -- both in-range settings per FR-GATE-010's table. It also mis-calibrated the threshold, a -70 dBFS setting first opening at -69.09 dBFS at 82 Hz and -66.50 at 1 kHz. Now a sliding-window maximum of |x|: instantaneous attack, and a release that is the age of the window rather than a time constant. Ripple is 0.00 dB at every frequency measured, and the threshold reads within 0.07 dB of its setting across 82 Hz to 5 kHz. Not the slow-release peak follower first proposed: an exponential release cannot be both slow enough for a 6.07 ms half-period (needs tau >= 17.5 ms for 3 dB) and fast enough to concede silence inside 10 ms, which namir-engine's hold test requires. A windowed max is flat and then drops, so it does both. Fixed-size array of 16 sub-block maxima -- O(1) per sample, no allocation, no data-dependent loop, so the RT harness still passes. #124: Opening now falls back to Closing when close goes true, mirroring the existing resumption. One -36 dBFS sample reaches gain 0.155 rather than 1.0 at a 50 ms attack. The test's stimulus had to change, and this is the part worth reviewing: its old falsifiers depended on the detector defect to produce chatter, since a decaying low E is monotonic at a peak detector's output. Keeping them would have meant asserting a bug. It now runs the requirement's own two scenarios -- a low E hovering at the threshold, then decaying through it -- and asserts one close event at every hold in the FRS range, at three sample rates, with the gap as the variable under test. Tag promoted back to plain. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
#56: nothing re-tested self.retired, so a handover deferred by a full retire pen was never finalized -- the stage stayed bypassed for the rest of the session (mix_target stuck at 0.0). Finalization is retried at the top of each block. #57: split now raises retire capacity to the per-offer headroom rather than erroring, keeping its infallible signature that both shells call. The covering test really did prove nothing: with retire: 1 restored, its run is byte-identical to an engine that never loaded anything -- peak difference 0.0. It now asserts that difference, plus a strong_count that counts the engine's own reference rather than only the test's. #58: bypass compensation read a latency frozen at prepare. Fixed-capacity delay lines sized at prepare replace the per-channel VecDeques, and process reads the current latency per block -- no allocation, one modulo per block. Correction to the issue: under global bypass the Nam stage never runs, so the handover never completes and the reported latency does not move; the misalignment materialises on the load-then-bypass path, which the new probe drives with a 44.1 kHz model in a 48 kHz engine. #59: the delay ring is now fed on both paths, so engaging bypass emits the real signal instead of stale content. #60: process validates the block against the PrepareContext. A check, not the debug_assert suggested -- an assert is loud where the caller is a test and absent where a host runs, which is backwards for D-16.3 and leaves the release fallback untestable. Both profiles now behave alike. The panic came one stage earlier than the issue says: gate.rs, which runs first. #61: the ceiling clamp no longer runs when bypassed. The NaN scan still does, pinned by its own test. FR-CHAIN-030's null test is unchanged and its plain tag is now more defensible, the method no longer failing above 0 dBFS. #62: retire headroom is reserved across the drain rather than checked once. #63: deferred_blocks counts only when a command is actually waiting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
#65: an unreadable directory marked the scan complete, so its whole subtree was inferred deleted and erased. A second mechanism now carries "nobody looked here": unreadable prefixes filter removals under them. Chosen over clearing `complete`, which would suppress removals tree-wide and leave a genuinely deleted file lingering forever after one ACL-restricted folder. The single-root shape both shells run is the dangerous one -- pre-fix the entire index became removals and was then written over the shared file. #66: read_dir returned Result<Vec<_>>, which made partial success unrepresentable, so one bad child discarded its siblings. Each per-entry failure now skips that child; a child that failed but has a path is still marked seen, so it is not inferred deleted either. #67: the settling window was centred on scan completion, which protected only files examined in the last two seconds. The index now records the scan's start, and the test is one-sided. On-disk key renamed with a serde alias so an existing index still loads. #68: the corruption policy promised a rebuild -- true for entries, false for favourites, which are hand-curated and unrecoverable. They are now mirrored to a sidecar, with the index document authoritative whenever it loads so a stale sidecar cannot resurrect a mark the user removed. #69: the staging file now carries pid and counter, so two shells no longer collide on a predictable, unowned name. #70: the size ceiling is enforced at the read via take(max + 1), not by re-stat'ing, so at most one byte past the limit is ever in memory. Non-regular paths are refused before opening, because File::open on a FIFO blocks at open and no bounded read can rescue that. #72: the folded blob is precomputed at upsert, so search allocates and folds nothing per call -- Cargo.toml's claim is now literally true. #73: directory symlinks are traversed rather than warned about; not traversing was never a decision, it fell out of asking file_type(). A symlinked collection is an ordinary setup, and a warning alone still leaves that user an empty library. Loop safety is now an explicit canonical-path guard consulted only when following a link. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
Eight of the nine notices issues (#15, #39-#41, #43-#45, #99) were already fixed by 0a993da, verified against the tree rather than the commit message. #99 duplicates #15 and was filed about ninety minutes before the fix landed. The one live defect is on the axis nobody reported, and W10's own fix created it: adding a remedy line under every message and capping the list at 16 traded the horizontal overflow for a vertical one. Measured by driving the real render at the CLAP editor's 960x640, where can_resize() is false: 13 Dismiss buttons drawn, 3 clipped away entirely, and not one FR-UI-020 control painted -- the notice list had swallowed the window. Permanently undismissable notices in a list that nothing but Dismiss empties, which is step 14's failure. The list is now a scroll area capped at a third of the window height. The bound is a fraction of the window rather than a row count, because a row's height depends on how far its text wraps -- a row bound would repeat one level up the exact mistake the horizontal fix removed. Tests read the layout render actually painted rather than a hand-copied one, and one of them guards the failure the fix itself could introduce: a bound that hides rather than clips. #98 is not needed for this; a resizable editor would have been a workaround. D-16.1 gains a Consequence note: it still described three catalogue fields while the tree has carried a fourth, and a substitution vocabulary, since W10. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
…, #138) #136's non-determinism is the platform libm -- not the toolchain, not evaluation order. rand_pcg, IEEE arithmetic and ryu printing are all bit-exact, and Rust contracts no FMA, so the only entry points were three: calibration's f32 sin, infer's f32 tanh, and the LSTM's tanh and exp. This sandbox's f32::tanh(-0.544) returns -0.49600992 where correctly-rounded is -0.49600986, 2 ULP out; its f64 libm is not. That settles M14's A2 drift: A2 inference is LeakyReLU only, so sin in the calibration probe is the whole explanation -- which is why "1.94.1 vs 1.98" never fit the evidence of ~50,000 RNG weights all matching. detmath.rs replaces those four calls with f64-only implementations. No new dependency, deliberately: one would move Cargo.lock, THIRD-PARTY-NOTICES.md and the dependency register, none of which this change should touch. A source guard test fails if a platform transcendental is reintroduced, which is the only kind of test that catches that. Committed bytes checked by regenerating in memory: the WaveNet and A2 goldens and the nam fuzz seed come back byte-identical. lstm_tiny.nam would move 4 weights by 1 ULP, but nothing regenerates it, so no committed bytes moved. The LSTM parity print goes from -inf dB to -125.1 dB, well inside its -100 dB bar -- the -inf was bit-exactness bought by both sides sharing one libm, not agreement. #133: an unreadable destination was treated as known junk and deleted. Only a readback that succeeds and returns junk now clears it; unreadable retries. #137: the cache key folds in generator versions plus two derived fingerprints, so a changed weight layout is caught mechanically rather than by remembering to bump a constant. #138: JSON Pointer segments are escaped per RFC 6901. Verified no committed corpus moves -- no key in any seed document contains / or ~. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
NFR-LIC-050: my own commit 3a5f515 added four fuzz-corpus files and a state corpus preset without recording their provenance. All five are generated -- the corpus files by the two generate_*_fuzz_corpus examples from seeded namir- fixtures mutations, the preset hand-authored from the format spec's worked example -- matching how every sibling in those directories is already declared. `xtask assets` is not in AGENTS.md's command list, which is why I had not been running it; the wave-3 gate runs did not cover it. Plan row: FR-GATE-020 returns to plain, the detector fix having closed it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
Consequence notes, appended in place per the house convention, for what this session's fixes changed underneath them: D-12.1 (removal suppression split from cancellation; settling window re-anchored to scan start; symlinks now followed, with loop safety made explicit rather than implicit), D-12.3 (favourites exempted from the corruption policy; staging file made per-process), D-13.2 (SCHED_FIFO target lowered off the policy maximum; Darwin's real mechanism recorded; the "not yet called from any audio thread" line retired). AGENTS.md's unsafe census said five blocks in thread_priority.rs; it is six, one sched_get_priority_min call added with the SCHED_FIFO fix. AGENTS.md's FR-NAM-030 illustration is corrected rather than deleted. M14 Phase 4b landed a real NeuralAmpModelerCore comparison, so "the only such comparison in the tree is S-1's" is false -- but the lesson the passage teaches still holds, because no trainer-produced A2 export has ever been loaded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
#129: the never-format-NaN contract rested on f32::max's NaN semantics. The non-finite cases are now handled before any arithmetic, so it holds by construction. The NaN assertions passed before this change too -- which is the issue's point: they were passing by accident, and are now pinned to something. Deviation from the prescribed fix, deliberately: mapping +inf to MIN_DB would make a blown-up signal read as dead silence, the same silently-wrong reading the issue objects to in Meter::peak. A symmetric MAX_DB ceiling keeps the reading monotonic in the magnitude and always finite. #128 was a doc error, not a code one: negative input reads as its magnitude, about +14 dB for -5.0, and only sub-normal magnitudes and NaN floor. The regression guard passes before and after by nature -- it exists so the doc's old claim is not implemented later. #130's premise is half stale: all three crates do call assert_unique_ids and add a namespace assertion on top, so the empty-id check is dropped nowhere. The true half is the namespace divergence, now checked centrally by deriving it from the first id, which gives all ten catalogues the check without editing any of them. #118: a duplicate key silently overwrote, and check_manifest returned Ok(()) -- reporting nothing at all. It is now a violation, and the tombstoned line wins the slot regardless of file order, so a regeneration cannot drop the record. A new code rather than DUPLICATE_KEY, whose remedy is about the registry and is the wrong advice for a duplicated file line. Still open from #129: Meter::peak's own poisoning, in namir-dsp and outside this change's scope. One non-finite sample still leaves the meter reading silence forever; the gain fix does not mask it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
… write #112: write_onto merged but could never remove, so a user who unloaded a model, saved and reloaded got it back -- the saved section still carried the old display_name and hash. Section 7's "absent means nothing of that kind is loaded" is the only way this format expresses an empty slot, so the two slots are removed when their field is None. Per-key, so D-11.2's promise to preserve an unknown key inside a section we own still holds. Checked the CLAP path: the clearing setter is reachable only from adopt_state, so a failed recall cannot erase a still-loaded reference. #113: resolve() ran the spec's three external steps and stopped, reporting Missing for a reference whose embedded copy was right there. Step 4 now runs, strictly last, borrowing rather than cloning -- an embed can be tens of MB. Candidate deliberately gains no Embedded variant: a Candidate is a question for a FileResolver, and an embedded copy needs no resolver, no I/O and no roots. namir-worker's post-loop fallback is already the right shape and its exhaustive match stays valid. #115: the ceiling was checked when reading and not when writing, so a 192 MiB embed produced 268 MB that this crate's own reader then refused -- a document we can save and cannot open. try_write and try_to_pretty_bytes check it, over a seam that makes the rule testable in microseconds rather than a gigabyte. The read-side check stays: section 7.2 requires a conforming reader to check the encoded length, and the redundancy holds only while the two constants are equal. MAX_EMBEDDED_BYTES's doc said "decoded"; the code has always checked the encoded length, matching the spec. The doc was wrong and is corrected. Still owed: namir-clap's state_ext and xtask's preset still call the unchecked writer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
#48: bottleneck was bounded after being used. resolve_layer_array handed the unbounded value to the activation resolver and then cloned the slope vector per layer, so a 188 KB file allocated 147 MB before anything rejected it. Measured with the crate's existing assert_no_alloc harness: 8219 allocator calls before, 21 after. A second shape -- bottleneck 8192 with 4096 layers, each legal on its own -- cloned 134 MB and only then failed on a weight-count mismatch; now 9 calls. The ceiling is on the product, since that is what gets allocated. #54 is fixed, but not by the mechanism proposed: hound 3.5.1 has no spec_ex().bytes_per_sample -- the accessor is private and the chunk reader is not re-exported. The block align is read back out of the fmt chunk instead, walking chunks as hound does, bounds-checked, returning None rather than panicking. The allowed layouts are exactly the pairs hound's Sample::read implements. That found a third divergence the issue did not name: a data chunk declaring more bytes than the file holds probes fine and fails decode. An existing test already exercised it; nothing had documented it. probe_wav's contract now lists three exceptions. User-visible consequence, intended but worth stating: a WAV with an unsupported container layout that the library previously indexed is now rejected at scan time. That is what making probe and decode agree means. #51: the error remedy still told users about "the four activations Namir implements" when it implements ten. #127's three call sites are converted, each behind a seam so prepare and its test cannot drift -- the defect is invisible at the shipped 0.0 dB defaults, so the tests drive the same constructor at -12 dB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
#106: try_submit is a GUI-thread contract both shells cite, and it waited out another thread's full deadline -- 2.14 s in the reproduction. It now tries the lock and hands the command back as Timeout, which is the answer callers already handle, so no new variant and no dead arm on the blocking path. #107 (security): recall::locate read an untrusted preset-supplied path with no bound. The reproduction read a 2 GB sparse candidate in 9.73 s; it is now a miss in microseconds. Same shape as namir-library's post-#70 fix rather than a second design: refuse non-regular paths before opening, keep the stat only as a cheap reject and capacity hint, and enforce the ceiling with take(max + 1). The same gap in LoadSource::read is closed with it. #108: the submit deadline was paid per parameter, so a backed-up ring cost 66.13 s for one recall. One budget for the whole pass now, each parameter waiting on what is left, the rest still attempted non-blocking and still counted. The two resource submits keep their own deadline -- they are a constant two, not a multiple of the registry. #110 is a doc fix, not a code one. The sweep runs only on a miss that has just parsed a whole file, so an O(n) retain over a few dozen Weaks is invisible, while the documented 2*live rule would need state across calls and would deliberately hold dead entries to twice the live set -- the residue NFR-PERF-070 exists to bound. Its tests were green on arrival, so their value was checked by implementing the documented rule instead: one then fails, 70 entries against 65, so they do discriminate the two candidate rules. #111: a second concurrent shutdown returned while threads were still running, and clap_plugin.destroy's caller may unload the library the instant it returns. Callers now wait, except one of the pool's own threads, where waiting would deadlock against the joiner. The residue no implementation can remove -- a re-entrant joiner skips its own handle -- is documented rather than papered over. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
namir-ui gains SavePreset/RecallPreset and both shells implement them, so these land together -- the seam does not compile split. namir-ui hands the host a name to place and offers back only paths the host listed: D-5.1 forbids it depending on namir-platform, so it cannot know where presets live, and a file dialog belongs in the shell where NFR-PORT-030's no-blocking-dialog clause applies. FileRefs are constructed rather than deferred. With record_reference disabled the new test panics because the preset had forgotten its IR -- a save button that silently loses the user's model and IR is worse than no save button. #84 and #87 were already fixed by 608fdde, 28 minutes after being filed. But #87's detector was blind: its harness warm-up drove the oversized callback pair before arming assert_no_alloc, pre-growing mono_scratch, so re-planting the unchunked extend still passed. Warm-up cut to the exact-size pair; the planted defect now fails. #88 moves an inline 160-byte Copy payload through a pre-allocated ring and formats on the UI thread. #90 was 452 allocations in one telemetry drain. #91 failed 6 of 6 runs against a writer swapping state. #86 intersects both devices pairwise instead of applying the input's answer to the output. #93's existing test had to change: it asserted latency == 0 after reactivation, which encoded the bug. #94 reports GUI parameter changes as gesture-wrapped events. Residual, stated rather than hidden: the GUI cannot request a flush at the moment a knob moves -- open_parented needs H: 'static and every host handle is 'a-bound -- so while inactive the change waits in the mirror. Never lost. #98 reports the refusal rather than becoming resizable, but the refusal cannot reach a host: clack-extensions 0.1.1 wraps the plugin's whole Result as the closure's success value, so Err becomes true at the C ABI. Its three sibling functions do this correctly, and the version is pinned. A second clack defect found: CoreEventSpace::from_unknown has no arms for the two gesture ids, so it drops well-formed events its own enum has variants for. #97 is a refactor of two byte-for-byte equivalent implementations, so no test can be red before and green after. Saying so rather than inventing one. Both shells now carry an identical presets.rs by deliberate agreement, each warning it belongs in namir-platform. Hoisting it next. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
Both shells were carrying a byte-for-byte identical presets.rs by deliberate agreement, each with a banner saying it belonged here. That is the shape of the bug AGENTS.md already records: namir-app and namir-clap each computing the library's default location independently is what let their wiring drift, and a scan against zero roots then erased the shared index. FR-STATE-030's "interchangeable between the two products" fails at discovery, not at the format, if the two look in different directories. D-13.2 puts filesystem locations here and nowhere else, so preset_dir, preset_dir_under, preset_path, sanitise_name and the listing live here now. The split is at the layering boundary, not an arbitrary one: list_preset_files returns (name, path) pairs because D-5.1 lets this crate depend on namir-core alone, so it cannot build namir-ui's PresetSummary. Each shell maps the pairs, which is the one line of the rule that is legitimately per-shell. preset_dir_under keeps the app's shape rather than the plugin's, so startup_probe's config-dir override still applies to a benchmark launch -- a probed run never opens a window, and taking the directory as a parameter keeps that true by construction rather than by argument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
…#36, #129) #36: Chain::process read "if not bypassed OR not prepared, run every stage", so a chain that never called prepare_crosscutting processed the signal while reporting bypass. No production path reaches it -- both shells build through build_default_chain, whose last statement is that call -- so it was a latent trap guarded only by an ordering nothing checks. Fixed by passing the input through, not by a debug_assert: this codebase already argues at engine.rs:243 that an assert on the audio thread degrades in exactly the wrong direction, loud in the test build and absent in the release build a host runs. An unprepared bypass is now the input undelayed rather than the input delayed -- still the input, which is all bypass claims. prepare_crosscutting still gates the NaN scan, the ceiling and latency compensation, and each doc comment now says which. The issue's excerpt was stale (apply_bypass is gone) but the defect was intact in its new form. #129's other half: one non-finite sample left Meter::peak reading dead silence for ever. Guarded -- and the average is guarded on its result too, because the same poisoning is reachable from a finite sample: x*x overflows above ~1.8e19 and the next sample is NaN. A guard reading only x.is_finite() would have left that open. The clip latch is deliberately untouched: infinity still latches clip, NaN still does not, exactly as before. Removing poisoning must not also remove the one visible trace a blown-up sample leaves in a meter. #33 needed no work: M14 sealed ErrorCode with non_exhaustive and added the xtask error-catalogue check, including the site the issue names and a second one it did not. Verified clean rather than assumed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
…#88) Building FR-IO-070's named apparatus found the defect it exists to catch. The notice half already passed; the assertion that failed was that nothing stopped the stream, while DEVICE_LOST's catalogue text has told users "the stream was stopped" since M14. The RunningStreams move out of a run() local into AppHost -- the UI thread is the only one that both learns of the loss and may act on it -- and are dropped on a DEVICE_LOST classification and only that one, since cpal reports survivable conditions through the same callback. The premise is out of date on one point: the substring recovery for the observed unplug already shipped with #44. The real gap was a level up, and the pinned fork's own source is the evidence -- wasapi/mod.rs:72 maps AUDCLNT_E_RESOURCES_INVALIDATED, the exact observed code, onto ErrorKind::StreamInvalidated, a kind Namir's match never named. cpal had classified it; Namir had not read the classification. The case no substring can reach is in the same file: default_device_change_error returns a bare StreamInvalidated with no message, no OS number and no marker. The tag moves onto the new test but is NOT promoted: "allow the user to select another device" is spanned only by a restart-mediated substitute, because no in-session device chooser exists in either shell (#26). Its uncovered: field now says that instead of describing an apparatus that no longer fails to exist. #88's owed half: app.rs, audio_io.rs and convert.rs join AUDIO_THREAD_MODULES. The gate went red on exactly the two predicted main-thread logging calls, which move to a new diagnostics.rs -- the standalone's shared.rs. Nine modules now covered, plus two guard tests so the hand-maintained list cannot silently shrink. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
#30's sample-accuracy clause was already closed at M14: 9dbbe9d splits the block at each automation event's own frame. Verified rather than assumed -- reverting audio.rs to the pre-M14 shape turns two clap_host_automation tests red, one at six offsets including both boundaries. The design is right for a reason specific to this chain, worth recording: global.bypass is the parameter FR-CLAP-060 is about, and it is stepped. It routes to set_global_bypass, a bool, so there is no ramp to carry an offset into -- ramp-offset smoothing could not deliver the one parameter the requirement names. What was never asserted is the issue's own caveat, that sub-blocks must not reintroduce the starvation M9b fixed. No existing test could: the FR-CLAP-070 schedule runs with nothing loaded, so neither the resampler nor the convolver is in that comparison. This probe runs a full chain -- 44.1 kHz model in a 48 kHz engine, live resampler, 2048-tap IR -- whole-block against split, and measures bit-exact. A negative control confirms it would notice a single spliced sample. The probe was red on first run, and the cause was not the split. On a first load, a stage's output is bit-exactly the dry input for the whole 960-sample equal-power handover crossfade, and the wet signal appears only at the start of the block the fade completes in -- frame 512, 768, 896, 959 for block sizes 512, 256, 64, 1, on both NamStage and IrStage. So the fade FR-NAM-070/FR-IR-060 specify is inaudible on a first load, and what a user hears instead starts at a block-quantised instant: up to ~85 ms of jitter at 4096 frames, the same defect shape #30 describes for automation. Recorded in the test's doc comment; it wants its own issue. Still unmet, and not promoted: FR-CLAP-060's click-free limb. set_global_bypass flips a bool with no crossfade where FR-CHAIN-020 fades over 15 ms. The fix belongs in chain.rs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
) The gate could only ever see one Verify code, so a compound method was structurally invisible: FR-STATE-040 read fully covered on its manual document while the S half had no artifact at all. The parser now reads a code set, and the method text is the marker's line plus its continuations -- required, not cosmetic, since NFR-RT-010's second code sits on a wrapped line. build_report looks up each evidence class separately (manual document for M, source annotation for U/I/G/B/S) and requires all of them. Eight compound Musts, not the six the issue lists: it missed FR-CHAIN-020, FR-IN-020, FR-ERR-070 and NFR-RT-010. The grammar is deliberately narrow. NFR-PERF-010's "B, as a CI regression gate" and NFR-RT-020's "S plus code review" stay single-code -- reading a qualifier as a second code would invent codes the FRS never wrote. Two demotions, both correct and neither papered over. FR-STATE-040 is re-covered by the schema validator built here. FR-IN-020 is now uncovered because no manual document exists for its display half -- which its own uncovered: field has said in prose all along. The tool now agrees with the tag's author. The validator restates section 3-7 from the prose rather than delegating to the reader, which would agree with it by construction. It already disagrees in one real place: 7.3 says a stored library_relative is always /-separated, while RelPath::parse accepts and normalises a backslash. Asserted as a deliberate divergence. Two tests parse 7.1's and 7.2's own field tables and assert they name exactly what the validator checks, which closes the drift residue a hand-written schema check otherwise carries. Still owed, outside this change: a CI step running xtask schema, with the README line xtask ci-commands requires in the same commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
Ten lines: the header's new sentence on compound methods, the eight compound rows' Verify column (M+S, U+I and so on), and FR-IO-070's rewritten uncovered: text from d236e00, which replaced the annotation without regenerating. FR-IN-020 and FR-STATE-040 also move their Covered by cells, the two compound methods whose codes span both evidence classes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
D-23.1 gains the rule the gate now enforces: each code resolves against the evidence class it names, and all of them are required, so on a compound method a plain trace: asserts the source-class half in full while D-18.6 leaves the M half with the manual document. Two corrections to roadmap §15 item 21 as written, both recorded rather than quietly fixed: there are eight compound Musts, not six, and two of the six it lists are not compound at all -- a qualifier clause is not a second code. FR-IN-020's demotion is left standing and explained. It is the mechanism working: its own uncovered: field had said in prose for milestones that the display half has no manual document, and the tool now agrees with its author instead of contradicting them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHVuZbYNsdWUfemA2tLP5N
This was referenced Aug 30, 2026
Open
Closed
Open
Open
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.
Sweeps the open issue tracker in three passes — tests, panics, then the rest — plus the four issues this work uncovered (#141–#144).
Gate green at HEAD:
fmt,clippy --workspace --all-targets -D warnings, all eightxtaskgates (layering,rt-logging,params-lock,attribution,identity,error-catalogue,assets,schema),traceability --allow-uncoveredexit 0, 62 test targets.cargo-denywas not run — not installed in the authoring sandbox.The toolchain moved during this work, 1.94.1 → 1.98.0: the declared MSRV is 1.97 and the sandbox was below it, so the workspace could not build. Everything here is verified on 1.98.0, golden suites included.
26 issues were already fixed or had a wrong premise
Verified against
git log, not taken on report. Closeable with a reference: #15, #31, #33, #39, #40, #41, #43, #44, #45, #84, #87, #89, #99, #117, #120 (M14:ef4c8b1,608fdde,0a993da,e083b90). #34, #30, #52, #55, #101, #125, #126 were partly stale — the live half is fixed here.Three worth calling out:
rubato's ownunwrapon the resampled path, i.e. an ordinary 44.1 kHz IR at a 48 kHz engine. Stopping at "already fixed" would have missed it.assert_no_alloc, so re-planting the defect still passed.BiquadCoeffs::designruns on a worker. It is reachable from the CLAP audio processor viaapply_param_direct, so that panic could take a host down.Severity corrections
SCHED_FIFOat policy maximum is 99 on Linux, wherewatchdog/Nandmigration/Nlive and threaded IRQ handlers sit at 50. A runaway audio thread outranked everything able to preempt it.ldmxcsrdeclaredpreserves_flags, which on x86 covers MXCSR's sticky exception flags. A false promise to LLVM.Tests that asserted nothing
Measured results
#136: the A2 drift, explained
Not the toolchain and not evaluation order — the platform libm.
rand_pcg, IEEE arithmetic andryuare bit-exact and Rust contracts no FMA, leaving four transcendental calls; this machine'sf32::tanh(-0.544)is 2 ULP out. A2 inference is LeakyReLU-only, sosinin the calibration probe explains it entirely — which is why "1.94.1 vs 1.98" never fit ~50,000 matching RNG weights.detmath.rsreplaces the four calls with f64-only implementations; the WaveNet and A2 goldens regenerate byte-identical.Coverage moved both ways
Verify:code, so a compound method was invisible. It now reads a set — eight compound Musts, not the six §15 item 21 lists, and two of that list's six are not compound at all. FR-STATE-040 is re-covered by a newnamir-stateschema validator; FR-IN-020 is now uncovered, because no manual document exists for its display half — which its ownuncovered:field had said in prose all along.Verify: MMusts record no pass.New in CI
headless-windowruns the interface against Xvfb (#143). Verified with the exact invocation, under Mesa/llvmpipe — not on a GitHub runner; if that image's GL stack differs, this job is where it surfaces, andcontinue-on-erroris a one-line change.Not addressed — needs a decision, not code
#29 (does CI gate merges — needs trunk's ruleset read; the issue's own inference is wrong, the red run it cites failed only on an informational job), #23 (signing — turns on whether 1.0 is public), #26 (device panel), #18 (macOS/Linux editor), #35 (accessibility), #28 (upstream cpal), #19 (Wayland), #37 (needs a trainer-produced A2 export this project does not have).
Follow-ups recorded, not silently dropped
namir-clap'sstate_extandxtask presetstill call the unchecked state writer;LibraryService::open_atstill hardcodes one root (#32's FR-LIB-010 half); R-16's mechanism text and #19's body still carry the GLX misattribution; the clack requirements are caret, not exact pins —=0.1.1is an open call (#144).