# Documentation Review: mzdata (root crate, src/)
Scope: `src/` in the root `mzdata` crate -- the facade crate that re-exports
`mzdata-spectrum`, `mzdata-bindata`, `mzdata-param`, `mzdata-meta`, and adds the `io/`
subsystem (format readers/writers), grouping/averaging iterators, and tutorials. No code
was changed; this is a review only. Companion reports already cover the sub-crates in
depth: `crates/mzdata-spectrum/spectrum-docs.md` and `crates/mzdata-bindata/bindata-docs.md`.
This review was produced by four parallel passes (root data-model files and tutorials;
mzML/MGF/imzML plus the core I/O traits; the vendor formats Thermo/TDF/mzMLb; and the
format-inference/index/USI/PROXI infrastructure), then consolidated here.
## How to read this report
- "Missing" means a public item has no `///` doc comment at all.
- "Thin" means a doc comment exists but omits information a caller needs (units, panics,
errors, preconditions, side effects, algorithm behavior).
- A number of findings below describe places where the observable behavior looks like it
does not match what a reasonable reader would expect from the name or the (thin/absent)
doc comment, including several likely-genuine bugs and cross-file inconsistencies. These
are flagged as "worth verifying," not asserted as confirmed, since no code was run or
changed. They are included because writing accurate documentation is exactly the
exercise that surfaced them.
- Given the size of this crate, per-file sections summarize repetitive small gaps (e.g. a
run of undocumented enum variants, or a family of undocumented one-line getters) as a
pattern with a representative line range, rather than listing every instance.
---
## 1. Crate root
### src/lib.rs
- The crate-level docs are a genuine strength here (overview, a working example, a feature
table) -- better than either sub-crate's root. The one clear gap: it never names the
constituent sub-crates (`mzdata-spectrum`, `mzdata-bindata`, `mzdata-param`,
`mzdata-meta`) or explains that this crate is a workspace facade gluing them together.
A docs.rs reader has no way to know, from this crate alone, that e.g. `SpectrumLike` and
`DataArray` actually live in and are documented by different crates. The re-exports at
the bottom of the file (`mzdata_param::{curie, impl_param_described, ...}`,
`mzdata_meta::{impl_metadata_trait, ...}`) appear with no explanation of why they sit at
crate root rather than under `params`/`meta`.
- Typo: "funcitonality" (appears twice).
- `src/spectrum.rs` line ~10-11 states "[bindata] ... does not include signal processing
as that is outside the scope of this crate" -- worth verifying/clarifying: under the
`mzsignal` feature this root crate *does* perform signal processing directly
(`pick_peaks`, `reprofile_with_shape`, `denoise` re-exported from `mzdata-spectrum`;
`average_spectra`/`SpectrumAveragingIterator` defined directly in `src/spectrum/
group.rs`). The sentence is presumably scoped to the `bindata` sub-crate specifically,
but as written it reads as a blanket claim about the whole crate, which is false once
`mzsignal` is enabled.
### src/main.rs
No issues; a trivial stub pointing at `examples/mzinfo.rs`.
### src/meta.rs
One-sentence module doc ("Metadata describing mass spectrometry data files and their
contents.") with no listing of the module's actual key types (`InstrumentConfiguration`,
`DataProcessing`, `Software`, `FileDescription`, `Sample`, `ScanSettings`, per this
project's own architecture notes). This is noticeably thinner than its siblings
`params.rs` and `spectrum.rs`, both of which enumerate their key types and give runnable
examples.
### src/params.rs
No gaps found. Excellent module doc: lists core types and includes multiple runnable
examples (building params, generic value reads, `ParamDescribed`, CURIE matching, PSI-MS
vocabulary lookups). Flagged here only as the crate's best example, worth using as a
template for `meta.rs` and `prelude.rs`.
### src/prelude.rs
One-line module doc ("A set of foundational traits used throughout the library.") with no
grouping or rationale for what's included, despite being the crate's primary "import
everything" entry point. A short paragraph explaining the trait families (spectrum access,
ion-mobility-frame access, metadata, parameter access) and why each belongs in the prelude
would help a newcomer who does `use mzdata::prelude::*;` and wants to know what just came
into scope.
### src/utils.rs
`PROTON`, `mass_charge_ratio`, and `neutral_mass` have **zero** documentation -- not even a
one-line comment -- despite being the crate's core mass-spec unit-conversion functions
(m/z <-> neutral mass). This is the same finding as `crates/mzdata-bindata/src/utils.rs`
(a near-identical, separately-defined pair of functions with the same gap); see that
report's section 5 for the full analysis (single-proton-adduct assumption, sign convention
for negative charge, behavior at `z == 0`). Worth noting explicitly: **this crate has two
independent copies of essentially the same physics formula** (root `src/utils.rs` and
`mzdata-bindata`'s `src/utils.rs`), neither documented, and nothing cross-references the
other or explains why both exist.
---
## 2. src/spectrum/ (grouping, averaging, MS^e handling)
Several files here (`bindata.rs`, `frame.rs`, `spectrum_types.rs`) are thin
`pub use mzdata_spectrum::*`/`pub use mzdata_bindata::*` re-export shims with no new public
items; they add nothing beyond what the sub-crate reports already cover. The substantive,
root-crate-only logic lives in `group.rs`, `group/flat.rs`, `group/mse_iter.rs`,
`group/util.rs`, and `utils.rs` (the `Collator`), and this is where the real documentation
gaps are -- and where the crate's most algorithmically complex code lives.
### src/spectrum/group.rs (largest file in this area, ~1550 lines)
- `SpectrumGroupingIterator`/`IonMobilityFrameGroupingIterator`: the struct docs describe
the *purpose* ("batch together MSn spectra with their MS1 precursor") but not the
*mechanism*: the generation-tracking flush logic (`generation_tracker`,
`flush_generations`), the `buffering` field (only ever mentioned as "default buffering
level of 3" inside `new()`'s doc, with no explanation of what it controls, no accessor,
and no way to change it after construction), or that emitted groups can legitimately
have **no precursor at all** (`deque_group_without_precursor`) -- a caller reading only
the struct doc would not expect `group.precursor()` to ever be `None`.
- `clear()`'s doc ("Reset the internal storage as though this iterator hadn't ever been
used") does not mention that the underlying `source` iterator is *not* rewound --
only internal buffers are cleared. That is a materially different guarantee than "as
though never used" and could mislead a caller trying to restart iteration.
- Worth verifying: `max_ms1_seeking_depth`'s doc reads like a scoped fallback ("maximum
number of entries to pass without seeing an MS1 entry before emitting a group without
a precursor"), but `deque_group_without_precursor` calls `flush_all_products`, which
drains the **entire** pending-product buffer, not just the entries responsible for
exceeding the depth limit. Unrelated, still-pending MSn spectra belonging to other,
future precursors can end up dumped into one orphan group. This is not documented and
would surprise a caller relying on the "maximum number of entries" framing.
- `mzsignal_impl` module (feature `mzsignal`), the spectrum-averaging/reprofiling engine:
- `average_spectra`'s doc omits the hard-coded `FWHM_DEFAULT = 0.01` used when
reprofiling centroid input (not configurable, not mentioned) and doesn't say what
happens on an empty or all-non-profile `spectra` input.
- `ArcArrays` and `SpectrumAveragingContext`: struct docs are one line each, and every
field and nearly every inherent method (`new`, `iter`, `reprofile_with`,
`reprofile_with_average_with`, `average_with`, etc.) has **zero** documentation,
despite being the actual averaging API surface.
- **Undocumented `unsafe`**: `reprofile_with_average_with` and `average_with` both
contain `unsafe { std::slice::from_raw_parts(mp, ...) }`, reinterpreting an
`Arc<Vec<f64>>`/`Arc<Vec<f32>>` pointer as a borrowed slice, with no `// SAFETY:`
comment anywhere and no rustdoc explanation of the invariant that makes this sound
(e.g. that the `Arc` must outlive the returned slice, or that no other thread mutates
it). Not asserting unsoundness, but unexplained `unsafe` in signal-averaging code that
processes every spectrum passing through this path deserves, at minimum, a documented
safety argument.
- `DeferredSpectrumAveragingIterator`/`SpectrumAveragingIterator`: struct docs exist but
fields, `into_inner`, `get_ref`, and `new` are undocumented on both; `initial_feed`'s
one-line doc doesn't explain the sliding-window pre-fill algorithm it implements.
- By contrast, `SpectrumGroupAveraging` (the public trait tying this together, with
`averaging`/`averaging_deferred` and proper `# Arguments` sections) is genuinely well
documented -- a good template for the rest of this module.
### src/spectrum/group/flat.rs
Entirely undocumented: `FlatSpectrumGroupingIterator`/`FlatIonMobilityGroupingIterator`
and every method have zero `///` comments. More importantly, nothing explains that this is
a **materially simpler, different grouping strategy** than `SpectrumGroupingIterator` in
`group.rs`: "flat" grouping starts a new group at every MS1 and appends everything until
the next MS1, with no buffering, no precursor-ID matching, and no orphan handling. Since
both strategies are public and reachable, a reader choosing between them has no doc
anywhere explaining the tradeoff (simpler/faster but less correct for interleaved/
out-of-order acquisitions, vs. the buffered, ID-matching, more correct but more complex
`SpectrumGroupingIterator`).
### src/spectrum/group/mse_iter.rs (Waters MS^e alternating-scan grouping)
- `HasScanConfiguration` (trait and its `get_configuration` method) has no doc comment.
Also worth flagging to a maintainer as a possible dead-code/visibility issue: the
containing module isn't `pub`, and the trait isn't re-exported, so it appears
unreachable from outside the crate despite being marked `pub`.
- `MSEIterator`'s core algorithm (`next_group`, `feed_item`) has no explanation of what
"low configuration" physically means (a scan-configuration CV/user-param value
identifying the low-collision-energy function in an MS^e acquisition), why a match on it
triggers a group boundary, or what happens to configurations that are neither the "low"
one nor in `ignored_configurations` (they silently accumulate as "high energy" scans in
the same group).
- Asymmetric doc coverage: `IonMobilityFrameMSEIteratorExt` and `IonMobilityFrameMSEIterator`
have doc comments; their structurally-identical siblings `SpectrumMSEIteratorExt` and
`SpectrumMSEIterator` do not.
### src/spectrum/group/util.rs
`GenerationTracker` (`pub(crate)`) and all of its methods, including `older_than` (which
implements the generation-window eviction algorithm underpinning `group.rs`'s
`flush_generations`), have zero doc comments. Since this is the one piece of code that
would actually explain the grouping iterator's stale-buffer/flush semantics, its complete
lack of documentation compounds the gaps in `group.rs` above -- a reader trying to
understand "when does a group get flushed" has nowhere to go.
### src/spectrum/scan_properties.rs
Mostly a re-export shim plus one new trait, `PrecursorRetrieval`. The trait itself has no
top-level doc (only its two methods do); neither method documents what happens when
`precursor_id`/`product_id` is present but not found in the given source (falls through to
`None`, unstated here).
### src/spectrum/utils.rs (the `Collator<T>` type)
- `Collator`'s struct doc is good, including an explicit "Warning" about unbounded memory
growth -- a positive example. But its four public fields (`waiting`, `next_key`, `ticks`,
`done`) have no field-level docs.
- `receive_from_map_timeout`, `receive_from_map_iter_timeout`, `receive_map`,
`receive_map_iter` (four consecutive public methods) have **zero** documentation despite
taking a nontrivial remapping callback `F: Fn(usize, U) -> ...` as a parameter. This
stands out because the neighboring, simpler `receive_from`/`receive_from_timeout`
methods just above them *are* well documented (batch size, timeout semantics) -- an
inconsistency within the same short file.
- `collate` (the unbounded-channel sibling of the well-documented `collate_sync`, which
has an explicit "blocks the calling thread" note) doesn't restate that caveat, forcing a
cross-reference.
---
## 3. Tutorials (src/tutorial/*.rs and the docs/*.md they include)
These are thin Rust shims (`#![doc = include_str!(...)]`) pulling in markdown from `docs/`.
The markdown itself was checked against the code it describes.
- **docs/reader_tutorial.md** and **docs/writer_tutorial.md**: accurate; spot-checked
claims (e.g. `StreamingSpectrumIterator` panic behavior, `MZReader` enum dispatch,
`SpectrumWriter`/`MSDataFileMetadata` usage) match the code. No discrepancies found.
- **docs/spectrum_tutorial.md**: accurate where checked, including the specific claim that
`pick_peaks` does not update `signal_continuity` automatically (confirmed against
`mzdata-spectrum`) and the `MultiLayerSpectrum` field-by-field construction example
(confirmed against the struct definition). One systemic issue: **every code example
touching signal processing (peak picking, reprofiling, denoising, the full
"putting it together" example) is fenced as ` ```ignore ` rather than ` ```rust `**,
meaning none of them are compiled or checked by the doc build, unlike the earlier,
simpler metadata/peaks examples in the same file which do use ` ```rust `. The most
complex and most failure-prone examples are exactly the ones never verified.
- **docs/ion_mobility_tutorial.md**: mostly accurate (spot-checked the
`VoltSecondPerSquareCentimeter`/`Millisecond` unit claims against `mzdata-param`), but:
- Line ~64 has a broken/truncated sentence: "Ion mobility data can come from many
places, but the common :" (missing words, stray colon).
- The same ` ```ignore `-for-signal-processing-examples pattern recurs here: the feature
extraction and querying examples (the parts doing the actual "signal processing" work)
are never compiled, only the simpler metadata-access examples are.
- `extract_features_simple`'s `maximum_gap_size` parameter is described as "maximum
allowed gap (in ion mobility units)" without clarifying whether that means the frame's
*native* unit (1/K0 vs. milliseconds, which the tutorial elsewhere stresses varies by
instrument) or a normalized unit -- worth clarifying given the tutorial's own emphasis
on unit variability.
Recommendation: fix the broken sentence, and change the signal-processing example fences
from ` ```ignore ` to ` ```rust ` (or `no_run` if they require external data files) so the
doc build actually exercises them.
---
## 4. src/io/ -- core traits and dispatch
### src/io/mod.rs, src/io/traits.rs
Both have only a stub or absent module doc. Neither explains the overall shape of the I/O
subsystem: format dispatch (`MZReaderType`/`MZReader`), the `mz_read!`/`mz_write!` macros,
or that `io::traits` is the hub defining `SpectrumSource`, `RandomAccessSpectrumIterator`,
`SpectrumWriter`, `MZFileReader`, etc. A newcomer landing on `io::mod`'s docs.rs page gets
no orientation.
### src/io/shorthand.rs (`mz_read!`/`mz_write!`)
This project's own CLAUDE.md states these macros are "zero-overhead alternatives... usage
is discouraged outside of throwaway code," but **that caveat exists only in CLAUDE.md, not
in the macros' own doc comments**. A reader who only sees rustdoc/docs.rs (which is
everyone except contributors reading this specific repo file) has no warning against
reaching for these pervasively. Additionally, no file anywhere compares the three ways to
open a reader (`MZReaderType` enum dispatch vs. `mz_read!` vs. constructing a concrete
reader type directly) or explains what tradeoff each makes -- the macro's own doc only
says it avoids "penalty" without saying what the alternatives' penalty actually is, and
doesn't contrast its own real limitation (the reader is scoped to the macro's closure and
"cannot be extracted without substantial type system torture") against `MZReaderType`,
which *can* be stored and returned freely.
### src/io/traits/util.rs, chromatogram.rs, spectrum.rs, frame.rs
- `SeekRead` (a widely-used bound throughout every reader implementation) has no doc
comment at all.
- `SpectrumSource`'s trait-level doc doesn't surface `DetailLevel` semantics or the
chronological-ordering assumption behind `get_spectrum_by_time` (that assumption is
documented on the method, but not promoted to the trait level where a reader would look
first); nothing states what happens with out-of-order scan times (undefined --
binary-search-based lookup will simply misbehave).
- `MZFileReader::open_path`'s doc says index building is "usually negligible on 'regular'
IO file systems" but says nothing about failure/recovery. This directly undersells the
actual complexity: concrete readers (see mzML below) implement multi-tier fallback and
validation logic that this trait-level doc gives no hint of.
- `StreamingSpectrumIterator`'s panic behavior is documented once at the type level but
not repeated as a `# Panics` section on each individual panicking method (`reset`,
`iter`).
- `RandomAccessSpectrumIterator for SpectrumIterator::start_from_time` calls `.expect(...)`
on a boundary-testing spectrum fetch -- undocumented panic path, including a possible
underflow-adjacent edge case at `len() == 0`.
- `SpectrumWriter::write`/`write_owned`'s returned `usize` is not documented at all, and
**its meaning differs by implementation**: `MzMLWriterType::write` returns the post-write
stream byte position, while `MGFWriterType::write` always returns `Ok(0)` regardless of
what was written. A caller using the trait-level `write_all`'s accumulated byte count
gets a meaningful number for mzML and a meaningless `0` for MGF, and neither the trait
nor either impl documents this.
- `IonMobilityFrameSource::has_ion_mobility`'s doc ("reads a sparse 1% of the entries")
doesn't mention that it temporarily mutates `detail_level` mid-call (restored
afterward) -- a side effect a caller concurrently reading `detail_level` could observe.
- `BorrowedGeneric3DIonMobilityFrameSource`'s `start_from_id/index/time` have `_ =>
todo!()` arms for unmapped `SpectrumAccessError` variants (e.g.
`SpectrumIndexNotFound` reaching `start_from_id`) -- an undocumented, apparently
reachable panic trap.
---
## 5. src/io/mzml/ (mzML and indexedmzML)
- `mod.rs` has a genuinely good module doc; no significant gaps there.
- `reading_shared.rs`: `MzMLParserState`/`MzMLParserError` enums have block-level docs but
zero per-variant docs across roughly 50 and 9 variants respectively (pattern, not
itemized). More substantively:
- `CVParamParse::handle_param_borrowed`/`handle_param` decode XML attributes with an
**undocumented, inconsistent** malformed-input policy: `name`/`value`/`unitName`
gracefully fall back to Latin-1 decoding when not valid UTF-8, but `cvRef`,
`accession`, and `unitAccession` panic (`.unwrap_or_else(|e| panic!(...))`) on the
same class of failure. A non-UTF8 CV accession attribute crashes the whole parse while
a non-UTF8 name attribute silently degrades -- worth documenting as a real asymmetry,
if not reconsidering.
- `FileMetadataBuilder::start_element` has dozens of `.expect("Error decoding X")` calls
on attribute text (id, name, location, version, order, etc.); every one panics on
malformed/non-UTF8 input, none of it documented as a precondition anywhere.
- `build_spectrum_index` is the best-documented function in the file (has
`# Arguments`/`# Returns`), but doesn't mention that it silently ignores unparsable
spectrum-id attributes (no warning logged) or what happens on duplicate spectrum IDs.
It also contains an unresolved mystery left as a plain code comment rather than
addressed: "This count is off by 2 because somehow the < and > bytes are removed?" --
an admission that the byte-offset arithmetic here isn't fully understood by its
author. Worth flagging verbatim as an unverified correctness note in load-bearing
offset-index-building code.
- `reader.rs`:
- Extensive `.expect(...)`-based panics on malformed numeric CV values throughout
`SpectrumBuilding` (`fill_binary_data_array`, `fill_selected_ion`,
`populate_isolation_window`, `fill_scan_window`) and `fill_param_into` (scan start
time, injection time, collision energy) -- none documented with `# Panics`.
- `MzMLReaderType::from_buffered_and_detail_level` silently swallows any error from
`parse_metadata()` (`Ok(())=>{}, Err(_err)=>{}`) -- a malformed mzML header/metadata
section leaves the reader with partial/default metadata and gives the caller no way to
know parsing failed. Not documented anywhere.
- **Index recovery is a substantial, entirely undocumented three-tier fallback**: (1)
try reading the trailing `indexedmzML` index; (2) on failure, rebuild via a full
linear scan; (3) even on a successful index read, *validate* it by probing a spectrum
near the middle of the file and checking for CRLF/LF byte-offset drift, rebuilding via
full scan with a `warn!` if drift is detected. None of `MzMLReaderType`'s docs,
`new_indexed`'s one-line doc, or the module doc mention any of this. `_read_index`
also panics outright if seeking back to file start fails during recovery.
- `get_chromatogram_by_id/_by_index`, `get_spectrum_by_id/_by_index`, and `reset` all use
`.expect(...)` around seek/checkpoint operations -- every random-access call can panic
on I/O failure mid-seek; undocumented, and the shared trait in `traits/spectrum.rs`
gives no indication either.
- **Worth verifying, high confidence, cross-file**: the sync reader's graceful
index-recovery fallback described above has **no counterpart in the async reader**.
`AsyncMzMLReaderType::new_indexed` calls `.expect("Failed to read mzML index")` (i.e.
panics) if the trailing index is missing or invalid, and
`AsyncMZFileReader::construct_index_from_stream` explicitly `panic!`s with "Building
an index from byte stream not yet implemented." **Net effect: a plain, non-indexed
mzML file that the synchronous reader opens successfully by falling back to a full
scan will cause the async reader to panic outright.** This is a major, load-bearing
capability gap between the sync and async mzML readers, and it is not documented in
either `async_reader.rs` or the shared trait docs.
- `writer.rs`: struct doc is reasonable but brief; a large number of public methods
(`write_scan_list`, `write_isolation_window`, `write_precursor`,
`write_binary_data_arrays`, `start_spectrum`, `start_chromatogram`, `write_chromatogram`,
`write_index`, `write_index_list`, compression getters/setters) have zero doc comments
(pattern, not itemized). Notably, the inherent `write_spectrum` *does* have good
`## Side-Effects`/`# Errors` sections (documenting that it implicitly opens
`<spectrumList>` and freezes `spectrum_count`), but the `SpectrumWriter` trait impl's
`write`/`flush`/`close` -- the primary interface most callers actually use -- has none
of that, so the documented behavior is effectively invisible through the generic path.
`write_binary_data_array_pre_encoded` has a real `# Panics` section (a good exemplar,
panics on `BinaryDataArrayType::Unknown`); its siblings `write_binary_data_array`/
`write_binary_data_arrays`, which call into the same panic path, do not repeat it.
`ChromatogramCollector::to_chromatogram` panics for any `ChromatogramType` other than
TIC/BPC, undocumented. `write_spectrum` also `.unwrap()`s when reading back a
caller-supplied TIC/BPI param as `f32`, panicking if a caller manually inserted a
non-numeric value there.
- `async_reader.rs`: struct doc is a one-line pointer to the sync type and does not
mention the capability gap above, nor which sync-side features (e.g. EOL-mismatch
detection) are simply absent here (confirmed absent). Otherwise mirrors the sync
reader's general documentation thinness.
---
## 6. src/io/mgf/
- `mod.rs`: fine, trivial.
- `reader.rs`: `MGFParserState`/`MGFError` enums undocumented per-variant (pattern).
`handle_scan_header` has the same kind of **inconsistent malformed-input handling** seen
in mzML: `PEPMASS`/`CHARGE` degrade gracefully to a proper `MGFError::
MalformedHeaderLine`, but `RTINSECONDS` calls `value.parse::<f64>().unwrap()` directly
and will panic the entire reader on one malformed retention-time value. None of this is
documented, and the inconsistency itself (some fields recoverable, one is not) is worth
calling out explicitly.
- `read_next` swallows every `MGFError` via `.ok()`, meaning a parse error mid-file (a
genuinely malformed peak line, say) is indistinguishable from a cleanly-exhausted
file when consumed through the `Iterator`/`SpectrumSource` API -- callers cannot tell
"no more spectra" from "the file is truncated or corrupt." This is not documented on
`read_next`, the trait, or the module doc. `read_into`, by contrast, does propagate the
error -- so the same underlying failure surfaces differently depending on which method
is called, and that inconsistency is also undocumented.
- `build_index` panics via `.expect(...)`/`panic!` on any I/O error during its pre-scan;
its own doc describes only the happy path.
- Random-access methods (`get_spectrum_by_id/_by_index`) again use undocumented
`.expect(...)` around seek/checkpoint operations, matching the mzML pattern.
- `writer.rs`: struct docs are reasonable. `write` always returns `Ok(0)` (see the
cross-file discrepancy in section 4 above). `write_arrays` rejects profile-mode spectra
(MGF is a centroid-only format) but that constraint is not mentioned on `write_peaks`,
the actual public entry point most callers would reach for.
- `async_reader.rs`: same general thinness as the sync reader; unlike mzML, no sync/async
divergence was found here (both panic identically on I/O errors during index building).
---
## 7. src/io/imzml/ (imaging mzML)
- `mod.rs` has one of the best module docs in the whole crate: explains the `.ibd`
external-file model and the continuous/processed storage modes clearly.
- `reader.rs`, however, has real gaps that matter for a format whose entire value
proposition is correctly pairing two separate files:
- `ImzMLReaderType` itself (the primary public reader type) has **no doc comment at
all**, unlike its mzML sibling which has a solid multi-paragraph type-level doc.
- `ImzMLFileMetadata` and `DataRangeQuery` have zero documentation, despite
`ImzMLFileMetadata` carrying the UUID/checksum fields central to the format's
data-integrity story that the module doc itself describes.
- `check_ibd_file` validates the `.ibd` file's UUID against the value declared in the
`.imzML` file, but **only logs a warning on mismatch and does not fail** -- a
wrongly-paired `.ibd`/`.imzML` pair will silently proceed to read binary data from the
wrong file. Worse, the **checksum is never validated at all**: there is a literal
`// TODO check that the checksum matches if available` in the code, despite
`ImzMLFileMetadata` already storing `ibd_checksum`/`ibd_checksum_type` for exactly
this purpose. None of this (partial validation, silent-on-mismatch behavior, checksum
never checked) is documented as a caveat anywhere, despite being a real, meaningful
data-integrity gap for an imaging format where silent misalignment between two files
would quietly corrupt every spectrum read.
- The same silent-error-swallowing pattern as the mzML reader recurs here:
`parse_metadata()` and `check_ibd_file()` errors are both discarded during
construction, undocumented.
- `load_ibd_arrays` (the actual binary-array decoding hook for this format) only
supports `BinaryCompressionType::NoCompression`/`Decoded`; any other compression type
returns an error, with a plain code comment (not a doc comment) noting that imzML has
its own external-data-compression CV terms that simply aren't implemented yet. This
real format-coverage limitation (compressed `.ibd` data is unsupported) appears
nowhere in the module or struct docs.
- Unlike the mzML reader, which tries a trailing index before falling back to a full
scan, `ImzMLReaderType`'s constructor **unconditionally does a full linear XML
pre-scan** on every open (`build_index()` with no fast path). Given that imzML's
entire use case is large imaging datasets (per the module doc's own framing), this
undocumented performance characteristic is a meaningful, real difference from its
mzML sibling that a user would want to know about before opening a large file.
- `open_file`'s dependency on the `filename` feature to auto-pair the `.ibd` file is
documented only in an inline comment and the runtime error message, not on the method
or type itself, and the `filename` feature is not listed among this crate's documented
features anywhere else either.
---
## 8. Vendor-format readers: Thermo, Bruker TDF, mzMLb
These are feature-gated (`thermo`, `bruker_tdf`, `mzmlb`) and, on the whole, the weakest
documentation coverage in the crate -- several files (tdf/sql.rs, tdf/arrays.rs,
tdf/constants.rs, thermo/instruments.rs) have essentially zero `///` documentation on any
public item.
### Thermo (`src/io/thermo/`)
- `mod.rs`'s module doc is a genuine strength: states the feature gate, the .NET 8
runtime requirement, licensing, and the `MZDATA_IGNORE_UNKNOWN_INSTRUMENT` escape hatch.
It does not state supported OS/platforms or the per-call marshaling cost of the .NET
interop layer.
- `reader.rs`: the main struct and most methods (including `get_tic`/`get_bpc`, which sit
right next to a properly-documented sibling method) have no docs. `get_centroiding`/
`set_centroiding` don't name the underlying vendor algorithm, its performance cost, or
that toggling it changes `signal_continuity` on subsequently-read spectra.
`get_load_extended_spectrum_data`/`set_load_extended_spectrum_data` don't list which
array types get populated (ChargeArray, BaselineArray, SignalToNoiseArray,
ResolutionArray), nor a plain-code-comment caveat that the resolution array is only
present on FT mass analyzers, which never made it into rustdoc.
- Undocumented panic: precursor population calls `todo!()` for the `MPD`/`PTD`
dissociation methods -- a file using either will panic mid-read, with no `# Panics`
anywhere.
- There is a `#[cfg(feature = "doc-only")]` stub module providing a fake
`ThermoRawReaderType` purely so docs.rs can build without the .NET runtime; that it is
a stub, and that the "real" type differs, is only recorded in a plain code comment,
never surfaced to a rustdoc reader.
- `async_reader.rs`: no module doc explaining this requires the `async`/`async_partial`
features or that every call is dispatched through `tokio::task::spawn_blocking` (a real,
undocumented per-call cost). Worth verifying: every async method temporarily takes
`self.inner` out via `.unwrap()` and restores it after the blocking call completes; if
the blocking closure panics, `self.inner` is left `None` permanently and every
subsequent call on that reader will then panic. Not documented anywhere.
- `instruments.rs`: essentially zero documentation on ~100 enum variants and several
matching/mapping functions. Worth verifying: the model-name matcher list is
order-sensitive for substring (`Contains`) matches (first match wins, undocumented as a
convention), and `"ASTRAL"` is listed before the more specific `"ASTRAL ZOOM"`, which
would make the dedicated `Orbitrap_Astral_Zoom` variant unreachable from real-world
instrument-name strings containing "ASTRAL ZOOM." Also worth verifying: a display-name
literal, `"Orbitrap Atral"`, looks like a typo for "Astral" with no citation to confirm
or deny against the PSI-MS term text.
### Bruker TDF (`src/io/tdf/`)
- `mod.rs` states the feature gate and dependencies but not which acquisition modes are
supported (DDA-PASEF, DIA-PASEF) versus which cause a panic (see below), nor the
required on-disk `.d` directory layout.
- `calibration.rs` is the single most important signal-processing file in this batch (see
Section 9), and its documentation is thin-to-absent on exactly the parts that matter:
field units are unstated throughout `MzCalibration`/`TimsCalibration`/
`MzCalibrationModel2` (is `t1`/`t2` a temperature? what are `c0..c7`?), and the actual
TOF-to-m/z conversion formula (`convert_f64`/`invert_f64`: a cubic polynomial plus up to
8 Newton-Raphson iterations, falling back to quadratic/linear solves) has no explanation
of the physical model, why 8 iterations, or what happens when the derivative hits zero
(breaks early, silently returning a possibly-inaccurate value).
- Worth verifying: three separate impls expose a method literally named `convert_f6`
(not `convert_f64`) alongside a correctly-named `invert_f64` -- looks like a typo, and
because it's undocumented there's no way to confirm intent.
- Worth verifying: an unconditional `eprintln!("Converting {value:?} to m/z
calibration");` fires on every m/z-calibration-model resolution (potentially per
frame). This looks like leftover debug output, inconsistent with the `log::` macros
used everywhere else in the codebase, and is undocumented either way.
- Worth verifying, and directly relevant to result accuracy: `reader.rs` contains a
commented-out line disabling m/z recalibration, with the comment "The m/z models
aren't consistently good enough, or at least my translations aren't" -- i.e. an
admission from the author that the ported Bruker m/z-recalibration formula may not
match Bruker's own values closely. This caveat about accuracy is not surfaced in any
public doc on `CalibrationParameters`, `MzCalibrationModel`, or `TDFFrameReaderType`,
even though it directly bears on the trustworthiness of every m/z value the reader
produces.
- `sql.rs`: almost entirely undocumented (the `FromSQL` trait, `SQLFrame`'s 17 fields,
`RawTDFSQLReader`, etc.). `SQLFrame.time`'s unit is never stated in this file (only
inferable from a division-by-60 at a call site elsewhere). `SQLTrace::load_trace_data`
implements a nontrivial in-place reordering of chromatogram trace chunks that may arrive
out of time order, entirely undocumented, with no explanation of why they'd be unsorted
in the first place.
- `arrays.rs` (the file that actually assembles spectra/frames from raw TDF binary blobs,
called out specifically by this review's scope): **no doc comments at all** on
`FrameToArraysMapper` or `consolidate_peaks`. The prefix-sum offset-array explanation
that does exist is a plain code comment, not rustdoc, so it's invisible to API
consumers. `consolidate_peaks`'s ion-mobility feature merging (via `mzsignal`) uses
unexplained magic numbers for a minimum point count and an ion-mobility gap tolerance,
with no stated units or rationale.
- `constants.rs`: no documentation on `InstrumentSource`/`MsMsType` or their numeric
codes, and no citation back to the Bruker schema fields they represent.
- `reader.rs`: a struct apparently misnamed `IndexExtry` (likely meant "IndexEntry") is
entirely undocumented. **Undocumented panics on real acquisition modes**: `scan_range`,
`guess_number_of_entries`, and `build_index` all call `todo!()` for
`MsMsType::MRM`/`PRMPASEF`/`Unknown` -- meaning PRM-PASEF and MRM Bruker files will
panic when read, a constraint not documented anywhere (not in `tdf/mod.rs`, not on the
reader type, no `# Panics` section). `index_to_precursor` assumes isolation windows are
symmetric around the target without documenting or citing that assumption against real
quadrupole isolation behavior.
### mzMLb (`src/io/mzmlb/`)
- `mod.rs`'s module doc, unlike Thermo's, never mentions the native **libhdf5** system
library dependency required at build/runtime -- a real, undocumented barrier for a
docs.rs reader trying to use this feature.
- `common.rs`: the HDF5-integer-type-to-`BinaryDataArrayType` mapping maps 8-bit and
16-bit integer HDF5 types to `Unknown` (i.e., unrepresentable). Worth verifying,
cross-file: this feeds directly into `reader.rs`'s array-reading match, which has a
`todo!()` arm for exactly `BinaryDataArrayType::Unknown` -- meaning an mzMLb file
storing an array as an 8-bit or 16-bit HDF5 integer will panic on read, undocumented
anywhere.
- `reader.rs`: the chunk-caching machinery for external HDF5 datasets (`
ExternalDataRegistry`, `CacheInterval`, `ByteReader` -- exactly the "how chunked
datasets map onto arrays" mechanism this review's scope asked about) is undocumented
except for its two public constructors, which do reasonably explain chunk-size/memory
tradeoffs (a good, if isolated, example). Worth verifying: a `handle_encoding` function
marked `#[allow(unused)]` (suggesting it isn't wired into the live read path)
additionally has a `NumpressLinear` branch that, even on successful decompression,
falls through to an unconditional `Err(...)` return -- if this code is in fact live
anywhere, a successful decompression would be reported as a failure. Undocumented
either way, and unclear whether it's dead code or a live bug.
- `writer.rs`: no module doc. Chunk size and compression-scheme constants/methods
(`DEFAULT_CHUNK_SIZE`, `zlib_compression`, `blosc_zstd`) have zero documentation -- no
units, no compression-level meaning, no size/speed tradeoff explanation -- and the
builder silently defaults to zlib level 9 (maximum compression, slowest) when a caller
doesn't explicitly choose a scheme, a default that is not surfaced anywhere a caller
would see it before writing a large file. `write_binary_data_array` panics on any
`ArrayType` not in its explicit match list. Worth verifying, cross-file: that list
excludes `BaselineArray`/`ResolutionArray`/`WavelengthArray`, yet the Thermo reader
actively produces `BaselineArray`/`ResolutionArray` when extended-data loading is
enabled -- round-tripping a Thermo spectrum with extended data through the mzMLb writer
would panic, and this incompatibility between two vendor-format modules is undocumented
anywhere. Also worth verifying: `Drop for MzMLbWriterType` calls `.close().unwrap()`,
meaning an I/O failure during an implicit drop (rather than an explicit `.close()` call)
panics inside a destructor -- undocumented, and no guidance anywhere to always close
explicitly and check the `Result`.
---
## 9. src/io/ infrastructure: format inference, offset index, USI, PROXI, compression
### Format inference and the read/write pipeline (`infer_format/*.rs`)
This is where "which format did the user hand us, and how do we know" is decided, and it
is one of the least-documented areas relative to how much a caller's correctness depends
on it.
- No file explains the overall precedence when multiple detection strategies could apply
(the specific thing this review's scope asked about). Reconstructed from the code:
**extension-based inference wins outright and is never cross-checked against file
content** once it succeeds; content-sniffing (reading the first 500 bytes, decompressing
that same budget for gzip input) is used only as a fallback when the extension is
unrecognized. Within that fallback, sniffers run in a fixed, undocumented order (imzML
is checked before mzML, since imzML content is a superset-like profile of mzML). None of
this is written down anywhere; a mislabeled file (e.g. an MGF renamed `.mzml`) would be
trusted on its extension alone with no content verification.
- "Indexed" vs. "raw" mzML is not a distinct format-inference outcome at all; both map
to the same `MassSpectrometryFormat::MzML`, and indexing is handled transparently
inside the mzML reader. This isn't stated anywhere and could confuse a reader
expecting indexing to be an inference-time concern.
- `pipeline.rs`'s `MassSpectrometryReadWriteProcess` trait doc claims to "handle all the
gory details of file format inference" but never says what those details are, and in
practice layers its *own* undocumented behavior on top of the inference module: a
gzip-compressed MGF/mzML is silently downgraded to a non-random-access streaming reader
(losing seek support) while an uncompressed file gets a fully indexed, random-access
reader through the same API; gzip is silently ignored altogether (neither decompressed
nor rejected) for mzMLb/imzML/Thermo/TDF inputs opened through this same entry point.
`Source::Stdin` always content-sniffs regardless of any hint and only supports MGF/mzML,
an undocumented restriction relative to the path-based branch. Writing supports far
fewer formats than reading (no Thermo/TDF/imzML writers at all), an asymmetry not
flagged anywhere near `open_writer`'s doc.
- `dispatch.rs`: `MZReaderType` explains its fast-dispatch design reasonably but never
states *why* it's `#[non_exhaustive]` (forward-compatibility for adding formats without
a semver break -- present only in project memory/CLAUDE.md, not in source).
`IMMZReaderType` (the ion-mobility-frame dispatch enum) has **zero** documentation of
any kind, including no mention that it lacks an `open_path` and must be reached via
`MZReaderType::try_into_frame_source` -- a fact otherwise recorded only outside the
codebase.
### src/io/offset_index.rs
`OffsetIndex`'s struct doc describes it as "an ordered mapping from entity ID to byte
offset" but never explains the mechanism it actually enables (a reader seeks the
underlying stream to the stored offset and parses the record starting there), nor what the
offset is relative to when the stream is wrapped by a decompressor (ambiguous as
documented). A representative run of public methods (`new`, `len`, `is_empty`, `keys`,
`clear`) have no doc comments at all.
### src/io/usi.rs (Universal Spectrum Identifier)
No module doc, no reference to the PSI USI specification, and -- the most concrete gap --
**no grammar description or worked valid/invalid examples anywhere in a doc comment**
(examples exist only inside `#[cfg(test)]`, invisible in rendered docs). The `USI` struct
and all six of its fields are entirely undocumented, as is the `Identifier` enum
(`Scan`/`Index`/`NativeID`). This matters concretely: `proxi.rs` treats `Scan` as 1-based
and `Index` as already 0-based when mapping to the internal spectrum index, and that
numbering-convention difference is load-bearing but appears nowhere in `usi.rs` itself --
a reader has to reverse-engineer it from an unrelated file. The actual `FromStr` parsing
algorithm (splitting on `:`, then re-associating trailing tokens by a repository-code
marker to separate interpretation from provenance, even though interpretation strings can
themselves contain colons) is exactly the kind of nontrivial parsing logic this review was
asked to check, and it has no explanation anywhere.
### src/io/proxi.rs
No module doc; nowhere does it state which parts of the PROXI spec are implemented (in
practice: only the `spectra?usi=...&resultType=full` GET endpoint against a fixed list of
known servers) or what's explicitly out of scope. Worth verifying: the blocking and async
"try all known backends" entry points have near-identical doc wording ("try all known
backends... return the first successful spectrum"), but they do not behave the same way --
the blocking version tries backends **sequentially, deterministically**, while the async
version **races them concurrently** and returns whichever completes first, which is
network-timing-dependent and non-deterministic. This meaningful divergence between two
"equivalent" documented methods is not called out anywhere. Separately, the conversion
from a parsed PROXI JSON response into `SpectrumDescription`/`MultiLayerSpectrum` contains
a long list of `.expect(...)`/`.unwrap()` calls on fields like ms level, scan start time,
selected ion m/z, charge state, and isolation window bounds -- all parsing **untrusted
data from a live HTTP response** -- with no `# Panics` documentation anywhere, meaning an
unexpected value from any queried PROXI server can crash the calling process.
### src/io/compression.rs
`is_gzipped` only checks the 2-byte magic prefix and says nothing about corrupt-but-valid-
prefix streams (which surface later, elsewhere, as an opaque I/O error). `RestartableGzDecoder`'s
doc mentions it's "not intended to be efficient" but doesn't state the actual cost that
matters to a caller: every seek fully rewinds and re-decompresses from byte zero up to the
target offset, i.e. seeking is O(offset), not O(1) or O(delta). The only place this cost is
acknowledged in prose is an unrelated doc comment on `dispatch.rs`'s
`MZReaderType::open_gzipped_read_seek`, with no cross-reference back to where the mechanism
actually lives.
### src/io/utils.rs
`DetailLevel`'s variants are reasonably documented (a positive example), but there is no
central table anywhere listing which concrete readers actually support which detail
levels, despite each reader having different, undocumented-in-aggregate support.
`PreBufferedStream`'s `Seek`/`Read` trait impls have no doc comments on the impl methods
themselves, despite genuinely non-standard behavior (erroring when seeking outside the
pre-buffered window). `ConcurrentLoader::load`'s one-line doc omits that it panics via
`.unwrap()` if any spawned chunk's file-open call fails, and doesn't state the (useful,
worth advertising) guarantee that results are re-sorted back into original spectrum-index
order despite concurrent loading.
---
## 10. Signal processing operations that need more detail
Pulling together the operations, across this whole crate, that actually transform or
derive numeric content from spectral data (as opposed to just moving bytes/records
around), since these encode algorithmic choices a caller cannot infer from a type
signature alone:
1. **Spectrum averaging and reprofiling** (`src/spectrum/group.rs`, `mzsignal_impl`
module). The public entry point (`SpectrumGroupAveraging`) is well documented, but
everything underneath it (`ArcArrays`, `SpectrumAveragingContext`,
`DeferredSpectrumAveragingIterator`/`SpectrumAveragingIterator`) is essentially
undocumented, including the hard-coded default FWHM used for reprofiling centroid
input before averaging, and two uses of `unsafe` raw-pointer-to-slice reinterpretation
with no safety rationale written down anywhere. See Section 2.
2. **MS^e alternating-scan grouping** (`src/spectrum/group/mse_iter.rs`). The grouping
rule itself (group boundary triggered by returning to the "low energy" configuration)
is a real domain-specific algorithm with no explanation of the underlying acquisition
concept anywhere in the doc comments. See Section 2.
3. **Bruker TDF calibration math** (`src/io/tdf/calibration.rs`). The single most
consequential undocumented algorithm in this entire review: the TOF-index-to-m/z
conversion (cubic polynomial plus iterative Newton-Raphson refinement) and the
scan-number-to-1/K0 conversion have no explanation of the physical model, units, or
accuracy, and there is a code-level admission (a disabled recalibration path with a
comment about the formula not being "consistently good enough") that the m/z values
this crate produces from TDF files may not match Bruker's own reference values -- a
caveat with obvious real-world consequences that is invisible to anyone reading only
the public API docs. See Section 8.
4. **TDF ion-mobility feature consolidation** (`src/io/tdf/arrays.rs`,
`consolidate_peaks`). Merges points across the ion-mobility dimension via `mzsignal`
with unexplained magic-number parameters (minimum point count, gap tolerance) and no
stated units. See Section 8.
5. **mzML/mzMLb/imzML binary array decoding hooks**. Multiple, separately-implemented
"read the raw bytes into typed arrays" code paths (`mzml/reader.rs`'s
`fill_binary_data_array`, `mzmlb/reader.rs`'s HDF5 chunk assembly, `imzml/reader.rs`'s
`load_ibd_arrays`) each have their own undocumented failure modes (numeric-parse
panics; unrepresentable HDF5 integer widths; unsupported IBD compression schemes), and
none of them are documented as a coherent, comparable set anywhere -- a caller who
understands one format's array-decoding limitations has no reason to expect the
others' limitations are different.
6. **mzML random-access index construction and validation** (`src/io/mzml/reader.rs`).
A genuinely sophisticated three-tier algorithm (read trailing index, fall back to full
scan, validate the trailing index against a mid-file probe to detect CRLF/LF drift)
that is completely absent from any public-facing doc, and whose sync/async
implementations diverge sharply in robustness (graceful fallback vs. hard panic). See
Section 5.
7. **Format inference / content sniffing** (`src/io/infer_format/inference.rs`). The
precedence between extension-based and content-based format detection, and the fixed
ordering of content sniffers, is real decision logic that determines which parser a
file is handed to, and it is not documented as a policy anywhere a user could find it
before hitting a surprising misdetection. See Section 9.
8. **USI parsing** (`src/io/usi.rs`). A real grammar with nontrivial tokenization rules
(colon-delimited fields, with trailing fields re-split by a repository-code marker to
handle embedded colons in the interpretation field) and a load-bearing, cross-file
1-based-vs-0-based indexing convention, none of which is written down in prose
anywhere in the file that owns the type. See Section 9.
9. **PROXI spectrum resolution** (`src/io/proxi.rs`). Both the backend-selection strategy
difference between the blocking and async APIs (deterministic-sequential vs.
racing-concurrent) and the extensive unguarded parsing of untrusted network response
data into typed spectra deserve documentation, the latter especially given it is a
direct route from external, unauthenticated network input into process-crashing
panics. See Section 9.
---
## 11. Cross-cutting patterns
A few issues recur so often across independently-written files that they are worth
naming once as crate-wide patterns rather than as N separate findings:
1. **Panic-on-malformed-input via `.expect()`/`.unwrap()` is pervasive** across mzML, MGF,
Thermo, TDF, mzMLb, and PROXI parsing code, almost always undocumented with a
`# Panics` section, despite most of these code paths processing external,
not-necessarily-well-formed input (files from third-party instruments/software, or
live network responses). Where the same crate handles conceptually similar malformed
fields differently (some recover to a typed `Err`, others panic) -- seen concretely in
both mzML's CV-attribute decoding and MGF's header-line parsing -- the inconsistency
itself is undocumented and looks unintentional.
2. **Silent error-swallowing during construction**: both `MzMLReaderType` and
`ImzMLReaderType` discard any error from their metadata-parsing step
(`Ok(())=>{}, Err(_)=>{}`), leaving the caller with a reader that silently has
partial/default metadata and no signal that anything went wrong. Undocumented in both
places.
3. **Enum variants without individual doc comments** is systemic:
`MzMLParserState`/`MzMLParserError`/`MzMLWriterState`, `MGFParserState`/`MGFError`,
`ArraysAvailable`-style outcome enums, and many more all have a block-level summary but
zero per-variant documentation.
4. **Sync/async divergence**: the mzML index-recovery fallback (graceful vs. panicking)
is the clearest, highest-impact example, but the pattern of "the async reader exists
but is documented and tested less thoroughly than its sync counterpart" recurs across
Thermo and mzML.
5. **Caveats left in plain code comments instead of doc comments**: several
correctness-relevant notes (the mzML byte-offset-off-by-2 mystery, the TDF
m/z-model-accuracy admission, the imzML checksum-not-implemented TODO, the FT-only
resolution-array note in Thermo) exist only as `//` comments next to the code, which
means they never reach rendered documentation even though they are exactly the kind of
caller-relevant caveat a reader would want to see before trusting the output.
---
## Suggested priority order
1. **Verify and, if confirmed, fix or clearly document** the highest-impact discrepancies
found in this review: the sync-vs-async mzML index-recovery gap (panics instead of
falling back), the TDF `eprintln!`/`convert_f6` oddities and the disabled-recalibration
accuracy admission, the mzMLb `Float32`-stored/`f64`-returned-style mismatches noted in
the bindata report that this crate's readers ultimately depend on, and the PROXI
blocking-vs-async backend-selection divergence.
2. Add `# Panics` sections to the parsing hot paths most likely to be hit by real
third-party files: mzML/MGF CV-attribute and numeric-field decoding, TDF's
`MsMsType::MRM`/`PRMPASEF` handling, and PROXI's response-to-spectrum conversion (the
last of these is the only one processing live network input).
3. Document the TDF calibration math (Section 8/10, item 3) -- this is the single most
consequential piece of undocumented signal-processing logic in the crate, since it
determines the accuracy of every m/z value read from Bruker files.
4. Document the mzML index-recovery algorithm and the format-inference precedence rules
(Sections 5 and 9) -- both are "how does this crate decide what my data actually is"
logic that a user has no way to audit today.
5. Document `usi.rs`'s grammar and the imzML `.ibd` pairing/checksum validation gap
(Section 7 and 9) -- both are integrity-relevant and currently invisible.
6. Fill in the crate-root gaps that are cheap to fix and high-visibility: name the
sub-crates in `src/lib.rs`'s crate doc, document `mass_charge_ratio`/`neutral_mass` in
`src/utils.rs` (and note its duplication with `mzdata-bindata`'s copy), bring
`src/meta.rs`/`src/prelude.rs` up to `src/params.rs`'s standard, and fix the broken
sentence in `docs/ion_mobility_tutorial.md`.
7. Change the `mzsignal`-feature tutorial examples from ` ```ignore ` to a compiled fence
so the doc build actually verifies the crate's most complex examples.
8. Work through the remaining missing-doc patterns (enum variants, one-line getters,
`SpectrumWriter`'s trait-level `write` return-value contract, `mz_read!`/`mz_write!`'s
missing "discouraged in production" caveat) as lower-urgency cleanup.