poa-consensus 0.4.0

Banded Partial Order Alignment (POA) consensus for short tandem repeat and amplicon reads
Documentation
# Flanking-Anchor Preprocessing

## The problem

STR reads extracted from a BAM may enter the POA graph with different rotational phases.
For a GAA repeat, a read starting at the first G, one starting at the first A, and one
starting at the second A all represent the same underlying sequence but look different to the
aligner. This creates spurious branches and unreliable consensus.

The root cause is that nothing pins the read to a common anchor point before alignment.

## The fix: extract the repeat segment

```rust
use poa_consensus::extract_flanked_region;

let left_flank  = b"TTTCTTTCTTTC";   // unique sequence left of the repeat
let right_flank = b"GAAGAAGAAGAA";   // unique sequence right of the repeat

for read in &reads {
    if let Some(segment) = extract_flanked_region(read, left_flank, right_flank) {
        // segment is a &[u8] slice into the original read;
        // no allocation, zero-copy
        graph.add_read(segment)?;
    }
    // reads where neither flank is found are silently skipped
}
```

`extract_flanked_region` aligns the left and right flanks to the read using approximate
matching (allowing up to a few mismatches). The returned slice contains only the bases
between the two flanks. This:

1. Anchors both boundaries to the same reference points in all reads.
2. Excludes non-spanning reads (if either flank is not found, the read is skipped).
3. Eliminates rotational phase ambiguity because all reads start and end at the same
   flanking sequence.

## Design rationale

This approach is used by TRGT, LongTR, and HMMSTR. It is more robust than relying on the
POA aligner to resolve phase by itself, because phase ambiguity is a problem at the
preprocessing stage, not an alignment error the DP can correct.

The flanking sequences must be:
- Unique enough to not appear inside the repeat (a 12-mer from unique flanking context is
  usually sufficient)
- Present in all reads that should be included (reads that only partially span the repeat are
  naturally excluded because one or both flanks won't be found)

## Full pipeline example

```rust
use poa_consensus::{
    extract_flanked_region, auto_orient, select_seed, SeedSelection,
    consensus, diagnose, DiagnoseConfig, PoaConfig,
};

// Step 1: orient all reads to the forward strand
let oriented = auto_orient(&raw_reads, 0);
let oriented_slices: Vec<&[u8]> = oriented.iter().map(|r| r.as_ref()).collect();

// Step 2: extract the repeat segment from each read
let left_flank  = b"TTTCTTTCTTTC";
let right_flank = b"GAAGAAGAAGAA";
let segments: Vec<&[u8]> = oriented_slices.iter()
    .filter_map(|r| extract_flanked_region(r, left_flank, right_flank))
    .collect();

// Step 3: select seed and build consensus
let seed_idx = select_seed(&segments, &SeedSelection::Auto)?;
let result   = consensus(&segments, seed_idx, &PoaConfig::default())?;

// Step 4: check result quality
let warnings = diagnose(&result, &DiagnoseConfig::default());
```

## When not to use this

If you do not have reliable flanking sequences (e.g. the flanks are themselves repetitive,
or you are processing reads without a reference), the core POA still works without this
step. The rotational phase bugs (CLAUDE.md bugs #1 and #2) will be present in edge cases,
but for most clean read sets the aligner resolves phase by the third or fourth read and the
consensus is correct.