Sequence-variant test corpus — an executable spec for variant × modification handling (L0)#1084
Conversation
The nut: an executable spec for how mzLib *should* read, expand, and digest proteins that carry sequence variants and modifications together — one of the most bug-prone corners in the library. Each case is a tiny protein described as one row in a table; the test builds its XML, runs the real LoadProteinXML -> variant-expansion -> top-down Digest pipeline, and compares the resulting proteoforms to a hand-written expected answer. Expected answers are derived from the intended biology, never captured from current code, so the corpus catches bugs instead of enshrining them. Details: - cases.tsv = one row per case (base sequence, mods, variants, protease, caps, extensible opts tail); expected/<id>.txt = the intended forms, one per line in canonical "least-modified-first" order, in mzLib's real full-sequence notation. - VariantCorpusTests asserts count + membership + run-twice determinism. - L0 foundation (substitutions + PTMs, top-down, non-binding caps): identity, single mod, single substitution, sub-x-mod chemical-legality drop (3 forms not 4), out-of-range pruning. All five green against master. - Later layers extend the same table: indels + source-dependent encoding, processing, bottom-up digestion, multi-variant/order, VCF depth, round-trip. Test-only; no production code touched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1084 +/- ##
=======================================
Coverage 91.46% 91.46%
=======================================
Files 427 427
Lines 52777 52777
Branches 6325 6325
=======================================
Hits 48273 48273
Misses 3268 3268
Partials 1236 1236 🚀 New features to boost your workflow:
|
| string tmp = Path.Combine(Path.GetTempPath(), $"corpus_{row.Id}_{Guid.NewGuid():N}.xml"); | ||
| try | ||
| { | ||
| File.WriteAllText(tmp, xml); |
There was a problem hiding this comment.
Should we use Protein DB Writer here?
There was a problem hiding this comment.
Why store these in this manner instead of an xml database?
Adds Corpus_Node_RoundTrip: serializes the consensus protein through the real ProteinDbWriter and re-reads it before digestion, so the corpus now exercises encode+decode (invariant 6 / smith-chem-wisc#1083) as well as read+digest. The writer never sits upstream of the ground truth -- the input XML is still the hand-authored, bible-grounded one, and the round-trip reuses the same expected/<id>.txt oracle. A writer that drops a variant or a variant-adjacent mod fails here while Corpus_Node stays green. Green across L0 (F00-F04); becomes the tripwire for variant-borne-mod and indel serialization at L1. Addresses review feedback (@nbollis): use ProteinDbWriter in the harness. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @nbollis — both good calls. Addressed in ed7e509. On On storing the corpus as
Happy to fold any of this into the description if useful. |
LoadRows parsed the spec table by magic index (c[7], int.Parse(c[8])...), which obscures the columns and silently breaks on any reorder or added column. Build the name->index map from the header once and construct Row with named fields (S(base), I(expected_count)); fail loudly listing any missing required column. cases.tsv is a deliberately growing table, so header-binding is the robust reading. Addresses review feedback (@nbollis): read the table into the typed object in a way that documents the columns. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the external cases.tsv + expected/*.txt (and their csproj copy-to-output directives) with a CorpusCase record yielded inline from GetCases(), matching the house style in Transcriptomics/TestDigestion.cs. Input spec and expected forms now sit together on one compiler-checked, IDE-navigable record; no runtime file I/O, no file-not-found fragility. ExpectedCount is kept alongside ExpectedForms as an independent over/under-generation guard (asserted equal), mirroring RnaDigestionTestCase keeping both DigestionProductCount and Sequences. Verdict/Reason ride along as fields, preserving the spec documentation the table used to carry. README updated to describe the record schema. Both families green (10/10). Addresses review feedback (@nbollis): encode the cases into a class. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Good call — converted the corpus to inline
Both families green (10/10). Thanks for the two nudges — the writer round-trip and the typed cases both made this materially better. Description updated to match. |
The nut
Handling proteins that carry sequence variants and modifications at the same time is one of mzLib's most bug-prone corners: a variant can delete a modification's target residue, shift every downstream coordinate (indels), or create/destroy a protease cut site — and the modifications have to follow correctly through all of it. This PR adds an executable specification for how that should behave: a small set of cases, each a tiny protein, run through the real load → variant-expand → digest pipeline and checked against a hand-authored expected answer.
The key discipline: expected answers are derived from the intended biology, never captured from current code — so the corpus catches bugs rather than enshrining them.
What's here (L0 foundation, test-only)
CorpusCaserecords inVariantCorpusTests.GetCases()— each case (base sequence, mods, variants, protease, caps, expected forms, verdict/reason) is an inline, compiler-checked record yielded to the tests via[TestCaseSource], matching the house style inTranscriptomics/TestDigestion.cs. Input and expected answer sit together; no external data files, no csproj copy-to-output plumbing.ExpectedForms— the intended proteoforms, canonical least-modified-first order, in mzLib's real full-sequence notation (e.g.[Biological:Phosphorylation on T]).ExpectedCountrides alongside as an independent over/under-generation guard (mirrorsRnaDigestionTestCase).Corpus_Node— builds an XML per case,LoadProteinXML(variants expand at load), digests top-down, and asserts count + membership + run-twice determinism.Corpus_Node_RoundTrip— serializes the consensus protein through the realProteinDbWriterand re-reads it before digesting, so the corpus also exercises encode+decode (invariant 6 / Salvage variant bugfixes from closed #974/#973 (XML-writer-consensus-only, variant-list pruning, homozygous depth, mods-on-variants) #1083). The writer never sits upstream of the ground truth — the input XML is still hand-authored, so a symmetric write/read bug can't pass silently.Why a corpus first
It turns a minefield into a measured map. As we add layers — indels + source-dependent encoding, post-translational processing, bottom-up digestion, multi-variant combinatorics, VCF depth/zygosity, read/write round-trips — the failing cases become the exact punch-list, and the natural boundaries for small, isolated fix-PRs.
Status
Draft / RFC. Deliberately small and test-only (no production code touched). This is the approach — looking for buy-in on the direction before building out the later layers. Review feedback folded in: cases now use
ProteinDbWriter(round-trip) and live as typed records rather than an external table. L1 (indels + variant-borne mods) is where the first real red is expected.🤖 Generated with Claude Code