Skip to content

Bring work packages A-F onto main (reviews docs/29, docs/30, docs/31) - #68

Merged
RobbinBouwmeester merged 15 commits into
mainfrom
review/e-followup
Sep 8, 2026
Merged

Bring work packages A-F onto main (reviews docs/29, docs/30, docs/31)#68
RobbinBouwmeester merged 15 commits into
mainfrom
review/e-followup

Conversation

@RobbinBouwmeester

Copy link
Copy Markdown
Member

Brings work packages A to F onto main. They are already reviewed and merged, but into each other rather than into main: the stack was chained child-to-parent, #61 went to main, and the remaining six merged into perf/ms2pip-worker-throughput, review/a-data-integrity, review/b-workers, review/c-desktop, review/d-provenance and review/e-followup respectively. main therefore carries #61 alone.

Because the branches were stacked, review/e-followup already contains every package. Verified before opening this: each of the seven package heads (ab7049e, 7587265, fdcbf3b, 3cd4363, d7874f2, 21b8ac7, 2853ff2) is an ancestor of this branch, main's tree is identical to the #61 head so main holds nothing this branch lacks, and the merge against the shared base is clean. 14 commits, 66 files.

What lands

PR Package Content
#62 A, engine data integrity typed pooled source, NULL rejection in the streamed library loader, failure-preserving artifact publication, the Met-excision length boundary, the rescore memory ceiling checked before allocation on the real f32 layout, predictor misses dropped with their pair, numeric configuration domains, tracked-file scanners
#63 B, workers a single-class entrapment fold is an error, MBR transfer q with the +1 pseudocount and a permuted-null summary, the rescore-selected peak joined, augment_library.py keeping imported base ids, benchmark fold hashing after the decoy prefix
#64 C, desktop and output ownership start guard plus results-folder reservation, case-insensitive run names, the converter probe carrying the request's configuration and the engine's msconvert fallback, one terminal-state writer, Dependabot and cargo audit for the desktop lockfile
#65 D, provenance and reporting boundary-continuous LOESS, a replayable experiment manifest, the audit on precursor_q with a single-source guard, is_transferred and transfer_q in both TSV reports
#66 E, follow-up review the fine-tune batch sentinel, portable run names, cancellation lifetime, conversion ownership, the remaining numeric domains, library-rewrite coverage, audit acceptance, nested reservations, the debug-binary stack
#67 F, whole-repository review the prescan no-calibration sentinel, the malformed transfer flag, script-resolution order, the NaN calibration query, one rescorer standardisation, the output-over-input guard across eight stages, and four regressions from A, D and E

Three review records land with it: docs/29_code_review_2026-09-07.md, docs/30_code_review_2026-09-08.md and docs/31_code_review_2026-09-08_full.md, none of which are on main today. Ten second-tier findings are listed in docs/31 as deliberately not fixed.

Validation already on record

  • Every one of review A: data integrity (docs/29 findings 1, 2, 4, 9, 11, 17, 18, 21) #62 to review F: whole-repository review docs/31 (F1-F10) #67 was CI-green at its final head before merging.
  • Full default HYE experiment, six AIF files through run-experiment with the shipped library configuration: 80,803 experiment-wide stripped peptides at 1%, 1.00% decoy fraction, 1:54 wall, recorded in docs/30.
  • E. coli FASTA single run through DeepLC 4.1.1 and MS2PIP 4.2.0, exercising the predictor-coverage checks against the real workers: no candidate dropped.
  • LOESS merge checks on HYE B01 for both calibration changes: neutral in docs/29 (two RT sources) and identical to the row in docs/31.

After merging

The other five review branches contain nothing this one lacks and can be deleted. A 0.2.0 version bump is the next step before tagging, since the unreleased section changes user-visible behaviour: the default MS2PIP model, out-of-range configurations now failing at load, two new TSV columns, the renamed audit rejection code, and predictor misses dropping candidates.

🤖 Generated with Claude Code

RobbinBouwmeester and others added 15 commits September 7, 2026 19:07
Fixes from the 2026-09-07 code review, now tracked as docs/29 and indexed:

- quant read the pooled-table guard column `source` as i32 while rescore writes u32,
  and treated the type error as an absent column, so the refusal never ran on engine
  output. It reads the declared type and errors on a present column of another type (#1).
- The streamed library loader checked finiteness on physical Arrow buffers and turned
  NULL cells into NaN, "" and candidate 0. Every required fragment column now rejects
  NULLs before its values are read (mumdia_io::table::require_no_nulls), with a
  fixture per column (#2).
- AtomicPath no longer removes the destination before the rename, so a failed
  publication leaves the previous artifact; temporary names carry a process-wide
  counter; the failure and replacement cases are tested (#4).
- Met excision is judged on the excised form alone, so an N-terminal peptide of
  max_len + 1 residues yields its excised form (#9).
- rescore.max_feature_matrix_gib is checked before allocation, from the footers and the
  selected feature count, on the flat f32 layout, with checked arithmetic (#11).
- A candidate DeepLC or MS2PIP returned nothing for is dropped together with its pair
  (base peptide, charge, modification set) instead of receiving iRT 0.0 or the native
  intensities; the counts are in the library report; the sidecar clients reject ids
  that were not requested or appear twice (#17).
- Numeric configuration domains are validated at load, documented zero meanings kept
  (#18).
- gen_config_reference.py and check_workflows.py scan tracked files via git ls-files,
  with the filesystem glob as the fallback outside a checkout (#21).

Verified: cargo test --workspace (193 + 41 + 10 + ...), clippy -D warnings, ci/smoke.sh
on the release build with byte-identical fixture output, gen_config_reference --check,
gen_cli_reference --check against the fresh binary, check_workflows, check_doc_refs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…no_nulls

rustdoc under -D warnings rejects an intra-doc link to a private item (rustdoc::private_intra_doc_links), which failed the fmt + clippy job.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- entrapment_worker: a training fold with a single class is an error that names the
  condition; the final model scores the decoys only. It used to skip the fold and score
  its held-out rows with a model trained on those rows, so in-sample scores entered the
  entrapment FDR (#3).
- mbr_worker: the transfer q carries the engine's +1 pseudocount, so a pool no permuted
  residual undercuts no longer gets q = 0 (#7); the summary names the permuted-null draws
  inside the accepted window instead of a decoy fraction over a population that cannot
  contain a decoy (#6); the per-run apex is the peak rescore selected
  (selected_peak_rank), falling back to the highest prelim_score peak, when a competed
  table carries several peaks per candidate (#8).
- augment_library: an added form of a sequence the imported library already has keeps
  that sequence's base_peptide_id; only new sequences get fresh ids (assign_base_ids,
  unit-tested without the engine) (#12).
- fs_lib: the fold key hashes the base sequence with the DECOY_ prefix stripped, and
  every benchmark row records the code revision, fold rule, feature count, seed and
  training recipe (#20). docs/28 states the difference from the earlier studies.
- Tests: single-class fold refusal; three-candidate pool not accepted at 1% and its q is
  1/3; the transfer measured on the rescore-selected peak; base-id reuse. conftest gains
  optional int columns for the scored fixture and top-K columns for the psms fixture.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… per request, publish one terminal state

Code review C (docs/29 findings 5, 13, 14).

Output ownership (#5). A repeated Start in the desktop application launched a
second engine into the same results folder: the start flow had several awaits
and no in-progress guard, and the backend launched every request. The frontend
now refuses a Start while one is in progress or while the run it follows is
still running. The backend reserves a run's results folder by canonical path
before spawning the engine and releases it when the run's end is published, so
a request for an active folder is refused with the owning run named. The CLI's
--run-names check compares names without regard to case on every platform,
because RunA and runa are one directory on Windows, macOS and most network
shares.

Converter preflight (#13). The desktop asked `doctor --json` without the
request's configuration, so a converter named in convert.thermo_raw_parser or
convert.msconvert was reported missing and the search refused; it also required
ThermoRawFileParser for Thermo .raw even when msconvert, the engine's fallback
for a parser left at auto, was present. The probe now carries the configuration
and the verdict is the engine's rule: only msconvert present runs with a note,
an explicitly configured parser that is missing blocks.

Cancel race (#14). `cancel` and the process waiter both wrote the terminal
status, whichever ran second won, and the cancellation flag was never read, so
a stopped run could be shown as failed with the last log line as its error. The
waiter is the only writer now: it reads the intent after reaping the engine and
publishes cancelled, done (the engine had already finished) or failed; until
then the snapshot carries cancel_requested and the interface shows Stopping.

Also: Dependabot for /desktop and a cargo audit of desktop/Cargo.lock in CI
(one documented ignore, RUSTSEC-2024-0429 via Tauri 2's gtk 0.18).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…precursor-q audit, transfer basis in TSVs, replayable experiment manifest

Code review D (docs/29 findings 10, 15, 16, 19) plus stale guide statements.

LOESS (#10). Outside the anchor range the smoother switched from the boundary
local fit to the global least-squares line, and the two need not agree: on
y = 200 + 10x^2 (span 0.3) the prediction jumped from 193.4 at x = 1e-6 to 38.3
at x = 0 and from 1173.5 to 1018.4 at the top, about 155 s discontinuities for
gradient-edge peptides against a narrow extraction window. The map now continues
the boundary local fit (value and slope) and is continuous at both ends; the
global line remains the degenerate fallback only. Endpoint continuity and the
local-slope extrapolation are tested on the review's reproduction.

Experiment manifest (#15). Records config_json next to config_hash, the model
identities that produced the artifacts (RT source, fragment predictor, the
classifier that actually ran from the rescore report, feature schema id, MBR
strategy), the configured and effective quant.q_filter, and hashes the inputs at
the start of the run rather than after it.

Audit (#16). The passed_precursor_fdr gate and FAILED_PRECURSOR_FDR reason read
precursor_q, the unit the label names, falling back to the PSM q_value only on a
table without it and recording q_unit in the metrics JSON. A pooled scored table
is refused, since the audit keys on candidate_id. The coarse extraction bucket
NO_PEAK_GROUP is DID_NOT_SURVIVE_EXTRACTION: extract does not write the table
that would separate presence, matched-fraction and gate failures, so the old
name claimed a cause the audit cannot see.

Report (#19). peptides.tsv and proteins.tsv, single-run and experiment-wide,
carry is_transferred and transfer_q so the acceptance basis of a match-between-
runs row is visible next to its untouched grouped q; the MBR worker's augmented
scored table gains transfer_q. The rule is unchanged and now documented: a
tighter report threshold does not revoke a transfer, and a transfer is not
protein-group confidence.

Guide corrections: retain_top_peaks alternatives do reach features, compete and
rescore (peak_rank, selected_peak_rank); the rescore matrix is flat f32; the
desktop comments no longer say run-experiment writes no report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rtable run names, cancel lifetime, conversion ownership, numeric domains, rewrite coverage, audit acceptance, nested reservations, main-thread stack

Code review E (docs/30_code_review_2026-09-08.md, findings R1 to R9) plus the
cleanup notes on model identity and the MBR worker's empty outputs.

R1. Enabling DeepLC fine-tuning with its own defaults was rejected at load,
because finetune_batch = 0 is the documented automatic batch size and review A's
validation demanded a positive batch. Only the epoch count is bounded now.

R2. run-experiment --run-names accepted `a` and `a.`, one directory on Windows,
and the second run overwrote the first with exit 0. Names ending in a dot or a
space, containing <>:"|?* or a control character, or naming a Windows reserved
device are rejected on every platform before anything is written.

R3. A desktop stop could sweep temporary files that belonged to the next run in
the same folder: the sweep ran after the reservation was released, and a stop on
a finished run swept too. Cancellation is intent and kill only, inert once the
run is terminal, and holds the pid lock across the kill; the waiter retires the
pid under that lock, sweeps, then releases, so a stop still killing finishes
before the folder changes hands. The progress thread writes only while active.

R4. Two searches converting the same vendor file concurrently shared one
temporary output and one could publish the other's bytes. Each conversion writes
a unique partial file under a lock beside the destination; a concurrent
converter waits and reuses the result; a lock whose holder stopped writing for
fifteen minutes is broken.

R5. Domain checks for the numeric settings review A left unchecked, with the
boundaries tested as accepted.

R6. The fine-tune and re-prediction worker zipped predictions with peptidoforms
without checking the count and silently kept the imported iRT for anything it
did not predict. A count mismatch is an error; rows keeping their imported value
are counted in <lib_out>.summary.json and the engine warns when there are any.
Rows are not dropped: candidate_id is the contiguous index the fragment table
references, and renumbering both tables for a rare event is a worse risk.

R7. The audit's reported flag repeated the precursor gate; it follows the
rejection reason now, a decoy past both gates is REMOVED_DURING_REPORTING, and a
present precursor_q column of the wrong type is an error rather than a fallback.

R8. The desktop reservation compared exact folders only; ancestors and
descendants of an active folder are refused, siblings are not.

R9. The Windows debug binary overflowed its 1 MiB main-thread stack on
--version. The CLI runs on a thread with a 256 MiB reservation, and
tests/cli_version.rs runs the built binary.

Model identities carry the installed DeepLC and MS2PIP versions. With nothing to
transfer the MBR worker writes a full-schema empty transfer table and the
requested augmented scored table unflagged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ansfer flag, script resolution, NaN calibration query, rescorer standardisation, output-over-input, and four regressions from A/D/E

Package F of docs/31_code_review_2026-09-08_full.md: the six confirmed correctness
and security defects, plus the regressions packages A, D and E introduced.

F1. prescan read the infinite-bounds sentinel that rt-im-train writes for
"calibration unavailable, search the whole gradient" as "cannot be screened" and
dropped the candidate. On the documented FASTA/MS2PIP path, where no confident
seed exists and every window row is infinite, that discarded the entire library
and exited 0 with a zero-row survivors table; the single-label bail could not see
it, being gated on a survivor count. An unbounded window now screens over the
whole gradient, a candidate with no run_windows row is treated the same, both are
counted and warned about, and screening every candidate away is an error.

F2. A present-but-wrong-typed is_transferred was swallowed as "no transfers",
silently removing every match-between-runs identification from peptides.tsv and
proteins.tsv while the parquet still carried them. mbr_worker.py writes that
column through pandas, so a nullable dtype or int8 reaches the stage. Present
columns are read in their declared type and a mismatch is an error; only an
absent column falls back. This is the absent-versus-malformed rule quant and
audit already follow.

F3. sidecar::resolve_script tried the working directory before the directory
beside the binary, the ordering python::resolve_script_dir was hardened against
and documents at length. That resolver only claims a directory holding
mbr_worker.py or deeplc_worker.py, so a directory with any of the other ten
workers reached resolve_script still relative and was executed from the working
directory. An absolute directory is taken as given, then the executable's
directory, then <exe>/scripts, and the working directory last.

F4. Loess::predict indexed before the start of its grid for a non-finite query:
both boundary comparisons are false, partition_point returns 0, and grid_x[j - 1]
underflows. One library row with a null predicted_irt is enough, because the
parquet reader maps a null f32 to NaN. It returns NaN now, and rt-im-train treats
a non-finite library iRT as "no calibrated RT" and counts those rows.

F5. The rescorer's in-memory TSV backend standardised with median/IQR while the
parquet and streaming backends used mean/std, so one pool scored differently
depending on rescore.handoff and on the 4 GB streaming threshold, with nothing
logged. All three use mean/std, which leaves the shipped parquet default and
every published benchmark unchanged. The MUMDIA_NN_FOLD_KEYS companion is
length-checked instead of sliced short, which used to leave the tail rows in no
fold, unscored, emerging from the rank normalisation at a plausible mid-rank
score that satisfies the completeness contract. The backend-size estimate counts
feature columns by name rather than subtracting a hardcoded three.

F6. refuse_output_over_input existed and was wired into two of eighteen stages,
so compete --features f.parquet --out f.parquet replaced the widest artifact of
the run with the competed subset at exit 0. It guards every output of
search-seed, rt-im-train, extract, features, compete, rescore, quant and audit.

F7 (regression, package D). The LOESS boundary extrapolation slope was the
pointwise local slope at the sparsest, most one-sided point of the fit:
unbounded, free to be negative and so to invert the iRT-to-RT map, multiplying a
distance that is unbounded by construction. It is the secant of the fitted curve
over its end decile, clamped non-negative and to at most four times the global
least-squares slope. The test uses noisy anchors rather than a noiseless
quadratic.

F8 (regression, package E). A stop arriving between the reap and the end of
publish_exit could pass a recycled process id to the tree kill. The waiter
retires the id the instant wait returns, before it reads the output directory.

F9 (regression, package E). The conversion lock spun without pause on an
undeletable stale lock, mistook clock skew and a peer's partial file for evidence
about its own holder, could be held by two processes at once, and left every
interrupted conversion's partial mzML behind for ever. Take-overs are bounded and
paced, waiting has a deadline, the holder is identified by a token it reads back
and only removes a lock that is still its own, a future modification time counts
as fresh, the partial-file probe matches this destination only, and abandoned
partials are swept under the lock.

F10 (regression, package A). Dropping an unpredicted candidate with everything
sharing its pair key also removed positional isomers that predicted correctly,
bounded only by the library being emptied. The direct misses and the collateral
are counted separately and exceeding 2% of the library is an error naming the
sidecar. The key stays position-free deliberately: a positional key would stop
matching a reverse decoy to its target, trading a sensitivity defect for an FDR
one, and a test pins that trade.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
review F: whole-repository review docs/31 (F1-F10)
@RobbinBouwmeester
RobbinBouwmeester merged commit c98c073 into main Sep 8, 2026
12 checks passed
@RobbinBouwmeester
RobbinBouwmeester deleted the review/e-followup branch September 8, 2026 17:39
@RobbinBouwmeester RobbinBouwmeester mentioned this pull request Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant