Skip to content

BED reporter and feature provider for strain genomic segments - #307

Merged
bgajria merged 5 commits into
masterfrom
strain-segment-record
Aug 5, 2026
Merged

BED reporter and feature provider for strain genomic segments#307
bgajria merged 5 commits into
masterfrom
strain-segment-record

Conversation

@jbrestel

@jbrestel jbrestel commented Jul 31, 2026

Copy link
Copy Markdown
Member

The Java half of the internal strain-genomic-segment record: emits one BED line per segment, in the strain's consensus-sequence coordinates.

Companion PR: VEuPathDB/ApiCommonModel#209 (record class, search, coordinate-conversion queries, and the <reporter> registration that names BedStrainSegmentReporter from this PR). Neither works without the other. Design spec and the end-to-end test report live in that PR.

What's here

  • util/StrainSegmentId.java — the single parse/format authority for the primary key <strain>:<refSeq>:<refStart>-<refEnd>:<f|r>. Validates in the private constructor, so no instance can exist in an invalid state.
  • feature/StrainSegmentFeatureProvider.java — reads the strain coordinates as attributes and emits the BED fields.
  • BedStrainSegmentReporter.java — three-line wiring, same shape as BedDynSpanReporter.
  • Unit tests for both (30 total, passing on cedar).

The two columns

  • chrom is hard-constrained: it must equal <strain>_<refSeq>, the key into the strain consensus FASTA. Read from the strain_seq_id attribute so the model stays the source of truth, then cross-checked against the key recomputed from the primary key — so a future divergence fails loudly instead of silently naming the wrong contig. Verified byte-exact against real FASTA deflines.
  • name is free — under deflineFormat=QUERYONLY seqret emits '>' + name verbatim, so it becomes the FASTA defline. Carries provenance; stays bare unless the caller passes deflineType=full.

The rejection this class exists to perform

The attribute query deliberately leaves strain_start/strain_end unclamped, so a segment inside a deletion reports strain_end < strain_start. Nothing upstream rejects it. This provider rejects three conditions, all before DeflineBuilder/BedLine.bed6 sees them, each naming the primary key and the offending values:

  1. strain_end < strain_start — the deletion inversion;
  2. strain_start < 1 — would emit chromStart = -1;
  3. strain_seq_id disagreeing with the key computed from the PK.

Validation order at the call site is load-bearing and commented as such: neither DeflineBuilder nor bed6 inspects its arguments.

Running the tests

This module inherits surefire 2.12.4, where -Dtest=A+B matches zero tests and still reports BUILD SUCCESS. Use a comma and read the counts:

mvn -Dtest=StrainSegmentIdTest,StrainSegmentFeatureProviderTest -DfailIfNoTests=true test

Also: no wb target compiles ApiCommonWebsite/Model. After editing here run bld ApiCommonWebsite/Model before wb model, or the model build fails with a misleading Implementation class for reporter 'bed' … cannot be found.

jbrestel and others added 5 commits July 31, 2026 12:19
The strain group in PATTERN was [^:_]+, excluding underscore. That was
written on a false premise: that no strain name contains an underscore.
Re-measured against the live database, the opposite is true -- of 6,119
strain names carrying indel data, 1,494 (24%) contain an underscore
(1_01_01, Af293_resequence2, China_LZCH-36, USGS_28834_1_NV,
AFIS_13708_CDC-14), and 0 contain a colon. So parse() was rejecting
roughly a quarter of all legitimate primary keys.

Relax the strain group to [^:]+. Colon is the only true delimiter, which
also aligns the Java parser with the SQL side (which already splits on
':' alone; 1_02_01:bcin_chr_1:1-100000:f resolves correctly there).

The old constraint was also justified by a claim that it kept
getStrainSeqId() reversible. It never did: refSeq contains underscores
too, so A_B + C is indistinguishable from A + B_C regardless of how the
strain grammar is narrowed. Replace that comment with one stating the key
is opaque and one-way -- it exists only to match the dnaseq FASTA defline
{sample}_{chrom_name}; consumers needing the strain parse it out of the
primary key, where ':' delimits unambiguously.

All coordinate and strand validation is unchanged. Adds five tests over
the real strain names above, which fail on the old regex and pass on the
new one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
StrainSegmentFeatureProvider emits one BED6 line per strain genomic segment.
chrom comes from the strain_seq_id attribute rather than being recomputed, so
the model stays the single source of truth for the strain consensus FASTA key;
name carries provenance (both coordinate systems) via DeflineBuilder, honouring
RequestedDeflineFields, and is the bare primary key unless deflineType=full.

It rejects an inverted strain interval (strain_end < strain_start), which
StrainSegmentAttributes.Coords deliberately leaves unclamped so a segment lying
inside a deletion is visible here.  Nothing upstream rejects it, and reaching a
FASTA lookup with one is a silent wrong answer.  A strain_start below 1 is
rejected for the same reason: BedLine would emit a negative chromStart.

The validation is a static seam so it is unit testable without a loaded WDK
model; the test covers the live inverted case
366.1:Pf3D7_10_v3:331757-331757:f -> 331658..331604 (verified in genomicsdb).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cross-check the BED chrom column against the primary key.  chrom still comes
from the strain_seq_id attribute -- the model stays the source of truth -- but
it is now compared to StrainSegmentId.getStrainSeqId() and a mismatch throws,
naming both keys and the primary key.  Today they cannot disagree; the check
exists because if the attribute query is ever rewritten to source the strain
from somewhere other than split_part, the failure is a BED line on the wrong
contig with no error at all.  It is also the first production consumer of
getStrainSeqId(), whose javadoc already promises it is the chrom column.

Test the accept side of the strainStart boundary (1-1): '<' silently becoming
'<=' passed every existing test while breaking every segment starting at
position 1.  Note at the call site that validating before DeflineBuilder and
BedLine.bed6 is load-bearing -- neither inspects its arguments.

Route organism through requiredStringAttribute like the other three required
attributes, so a NULL fails naming the primary key instead of rendering as an
empty defline field.

Docs/tests: name BedReporter.configure/getFieldsByName as the actual validator
of the declared record class and attributes (the provider only declares them);
list the accepted deflineFields vocabulary, which nothing else enumerates;
stop claiming Task 7 end-to-end coverage in the present tense; factor the
reject boilerplate into an assertIntervalRejected helper that still asserts on
message content, following StrainSegmentIdTest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jbrestel
jbrestel marked this pull request as ready for review August 1, 2026 02:14
@bgajria
bgajria merged commit bddb616 into master Aug 5, 2026
1 check passed
@bgajria
bgajria deleted the strain-segment-record branch August 5, 2026 14:23
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.

2 participants