Skip to content

fix(str): take the marker range from the majority value shape - #25

Merged
JamesKane merged 1 commit into
mainfrom
fix/str-marker-mixed-shape
Jul 29, 2026
Merged

fix(str): take the marker range from the majority value shape#25
JamesKane merged 1 commit into
mainfrom
fix/str-marker-mixed-shape

Conversation

@JamesKane

Copy link
Copy Markdown
Owner

Reported from the live report: DYS413 showed as multi-copy but with a single reported value.

DYS413  multi-copy  —  —   23   23   23   867  867  25      —  —  Not scored
                        min modal max              distinct

min = modal = max = 23 alongside 25 distinct values is self-contradictory.

Cause

DYS413 has 866 copy-vector calls (23-23, 21-23, 23-25, …) and exactly one bare 23 — a kit that collapsed the pair into a single count. The range picked whichever shape was non-null, with the scalar winning ties:

(Some(v), _) => v.to_string(),   // scalar wins, hides the vector

So one observation out of 867 replaced the entire copy-vector range, and the page presented a single sample's value as the corpus range. The mirror case was equally broken: a mostly-simple marker with one stray vector would have shown a one-sample vector range.

Fix

The range now comes from whichever shape the majority of observations use, and the other shape's statistics are nulled out, so a row can no longer carry both. DYS413 now reads:

min modal max notes
before 23 23 23
after 14-24 23-23 25-25 1 other shape

All three are real observations (14-24 ×1, 23-23 ×545, 25-25 ×1), and 23-23 is the genuine mode.

The minority observation is counted rather than silently dropped, and surfaced in the notes cell — same reasoning as null_alleles and complex_count, and it explains why distinct can exceed what the displayed range accounts for.

Fixed in the aggregation rather than the template, so /api/v1/reports/str-markers is correct too — it had the same bug.

Notes

  • Migration 0072 adds mixed_shape_count. Existing rows keep the old values until the next du-jobs run-once str-marker-stats, which the deploy should run anyway.
  • Regression test covers both directions (majority-vector with a stray scalar, and majority-scalar with a stray vector).

Verification

  • cargo clippy --workspace --locked -- -D warnings clean
  • du-db 115/115 against a fresh, empty database in parallel
  • du-web 65/65
  • Verified against the real corpus: DYS413's new endpoints confirmed as actual observations

🤖 Generated with Claude Code

DYS413 was reported as `23 / 23 / 23` with 25 distinct observed values — a
contradiction on the face of the report.

The marker has 866 copy-vector calls (23-23, 21-23, …) and exactly one bare `23`
from a kit that collapsed the pair. The range picked whichever shape was
non-null, with the scalar winning, so that single observation replaced the entire
copy-vector range: the page presented one sample's value as the corpus range for
867 observations. The mirror case was equally broken — a mostly-simple marker
with one stray vector would have shown a one-sample vector range.

The range now comes from whichever shape the majority of observations use, and
the other shape's statistics are nulled out so a row can no longer carry both.
DYS413 reads 14-24 / 23-23 / 25-25, all real observations.

The minority count is kept rather than silently dropped, and shown in the report's
notes cell — the same reasoning as null_alleles and complex_count, and it explains
why `distinct` can exceed what the displayed range accounts for.

Fixed in the aggregation rather than the template so the JSON API is correct too.
Regression test covers both directions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@JamesKane
JamesKane merged commit ab351fe into main Jul 29, 2026
1 check passed
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