Skip to content

fix(clip): clip chimeric templates, soft-only mate window, drop --sort-order (R2-CLIP-02/03/04) - #502

Merged
nh13 merged 1 commit into
mainfrom
nh/fix-clip-round2-mate-supp-window
Jul 11, 2026
Merged

nh13 merged 1 commit into
mainfrom
nh/fix-clip-round2-mate-supp-window

Conversation

@nh13

@nh13 nh13 commented Jul 9, 2026 •

Copy link
Copy Markdown
Member

Round-2 fgbio-parity fixes for clip, stacked on #492 (CLIP-01 / setMateInfo). Auto-retargets to main when #492 merges. Tracker: reports/2026-07-08-fgbio-behavioral-parity-tracker-round2.md §R2.4 (PR 4).

Findings

R2-CLIP-02 (S1) — chimeric/split templates left completely unclipped. The clip loop was if len==1 {frag} else if len==2 {pair} with no else, so any template with a secondary/supplementary read (len > 2, i.e. any chimeric/split-read pair) was passed through unclipped with no mate fixing. Now the primary R1/R2 are located by SAM flags (fgbio ClipBam's Template.r1/r2), clipped, and each supplementary alignment has its mate info repaired via a port of htsjdk 5.0.0 SamPairUtil.setMateInformationOnSupplementalAlignment — mate ref/pos/strand, mate-unmapped flag, TLEN = -matePrimary.TLEN, MC (when the mate is mapped), and MQ (unconditionally, as of htsjdk 5.0.0). Both the single-threaded and threaded paths are refactored identically.

R2-CLIP-03 (S2) — --clip-bases-past-mate used a soft+hard mate window. The mate window was bounded with unclipped_{start,end} (soft and hard clips); fgbio uses mate.unSoftClipped{Start,End} (soft only — hard-clipped bases are physically absent). When the mate carries hard clips the window was too wide and the read was under-clipped. Added soft-only unsoftclipped_{start,end}[_raw] helpers and used them in both the typed and raw clippers.

R2-CLIP-04 (S1) — --sort-order was a no-op. It only relabeled the SO header field; neither path re-sorted, so --sort-order coordinate produced a BAM labeled coordinate but physically query-grouped. Per the tracker decision the flag is removed (consistent with round-1 FILT-03); rely on a downstream fgumi sort.

R2-CLIP-01 (mate-info recompute) was already resolved by #492's setMateInfo port — verified, no further change.

fgbio-oracle parity (§0 step 2/4)

Oracle = fgbio 4.1.0 ClipBam (default clipping mode Hard). fgumi compare bams --mode content:

Scenario Before After
CHIM — len-3 chimeric template, --clip-overlapping-reads DIFFER (fgumi left 100M/100M unclipped; supp mate-info stale, no MC/MQ) MATCH (75M25H/25H75M; supp gets MC:Z:25H75M MQ:i:60, mate-pos 175, TLEN 150)
HARDMATE — FR dovetail, reverse mate 50M50H, --clip-bases-past-mate DIFFER (fgumi 110M10H — past the soft+hard end 209) MATCH (60M60H — past the mate's soft end 159)
SORTFLAG — clip … --sort-order coordinate ACCEPTED (no-op) REJECTED (unexpected argument '--sort-order')

Fixture (added to the PR-12 corpus): reports/fgbio-parity-fixtures/r2-clip-02-03-04.sh with before/after evidence.

Tests / CI

  • New unit tests: soft-only unsoftclipped_* helpers (typed + raw, hard-clip-ignoring); find_primary_pair_indices (secondary/supplementary skipped, order-independent, lone fragment).
  • Removed the three field-only tests that pinned the deleted --sort-order flag.
  • cargo ci-fmt / cargo ci-lint clean; full suite 2213 passed.

Merge order

Merge #492 → this. It auto-retargets to main once #492 lands.

Summary by CodeRabbit

  • New Features
    • Added soft-clip–aware “unsoftclipped” position utilities for typed and raw records.
    • Clip now discovers primary R1/R2 using SAM flags and repairs mate-pair metadata, including for supplementary alignments.
  • Bug Fixes
    • Mate-boundary extension now uses soft-clipping-only coordinates (hard-clipped bases are no longer considered).
  • Documentation
    • Removed the --sort-order option; clip output preserves input order (coordinate-sorted output requires fgumi sort).
  • Tests
    • Expanded clip --threads integration coverage with exact CIGAR/flag assertions, including secondary-only and supplementary mate repair scenarios.

@nh13
nh13 temporarily deployed to github-actions July 9, 2026 00:31 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Jul 9, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Clip now uses soft-clip-only mate boundaries and flag-based primary selection. It preserves input order, repairs primary and supplementary mate fields, and adds threaded integration coverage for paired, fragment, secondary-only, and supplementary records.

Changes

Clip boundary and primary-template handling

Layer / File(s) Summary
Soft-only mate boundary utilities
crates/fgumi-sam/src/record_utils.rs, crates/fgumi-sam/src/clipper.rs
Adds typed and raw soft-clip-only boundary helpers, updates mate-end clipping, and tests hard-clip and unmapped-record behavior.
Primary and supplementary template processing
src/lib/commands/clip.rs
Removes --sort-order, preserves input order, selects primary records by SAM flags, and repairs primary and supplementary mate fields in both execution paths.
Threaded clipping integration coverage
tests/integration/test_clip_command.rs
Adds threaded tests for paired reads, fragments, secondary-only records, and supplementary mate repair with CIGAR, flag, and mate metadata assertions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ClipExecute
  participant process_fn
  participant find_primary_pair_indices
  participant RawRecordClipper
  participant fix_supplemental_mate_info
  ClipExecute->>process_fn: process template
  process_fn->>find_primary_pair_indices: locate primary R1 and R2
  find_primary_pair_indices-->>process_fn: return primary indices
  process_fn->>RawRecordClipper: clip primary records
  RawRecordClipper-->>process_fn: return clipped records
  process_fn->>fix_supplemental_mate_info: repair supplementary mate fields
Loading

Possibly related PRs

Suggested labels: raw-bam

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main clip changes: chimeric template clipping, soft-only mate windows, and removal of --sort-order.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch nh/fix-clip-round2-mate-supp-window

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 9, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.89474% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.55%. Comparing base (cbc7d72) to head (6783d10).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
src/lib/commands/clip.rs 97.52% 7 Missing ⚠️
crates/fgumi-sam/src/record_utils.rs 98.87% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #502      +/-   ##
==========================================
+ Coverage   91.17%   92.55%   +1.38%     
==========================================
  Files          78      165      +87     
  Lines       51916    99363   +47447     
==========================================
+ Hits        47333    91967   +44634     
- Misses       4583     7396    +2813     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@nh13
nh13 force-pushed the nh/fix-clip-round2-mate-supp-window branch from 831d9c3 to 1a641f2 Compare July 9, 2026 01:05
@nh13
nh13 temporarily deployed to github-actions July 9, 2026 01:05 — with GitHub Actions Inactive
@nh13
nh13 force-pushed the nh/fix-clip-unmap-when-fully-clipped branch from c7535af to 389e375 Compare July 9, 2026 05:33
@nh13
nh13 force-pushed the nh/fix-clip-round2-mate-supp-window branch from 1a641f2 to fea962a Compare July 9, 2026 16:55
@nh13
nh13 temporarily deployed to github-actions July 9, 2026 16:55 — with GitHub Actions Inactive
@nh13
nh13 force-pushed the nh/fix-clip-unmap-when-fully-clipped branch from 389e375 to fb85484 Compare July 9, 2026 17:13
@nh13
nh13 force-pushed the nh/fix-clip-round2-mate-supp-window branch from fea962a to 541f93f Compare July 9, 2026 20:33
@nh13
nh13 temporarily deployed to github-actions July 9, 2026 20:33 — with GitHub Actions Inactive
@nh13
nh13 force-pushed the nh/fix-clip-unmap-when-fully-clipped branch from fb85484 to 6794f10 Compare July 10, 2026 03:41
@nh13
nh13 force-pushed the nh/fix-clip-round2-mate-supp-window branch from 541f93f to 06e0eee Compare July 10, 2026 03:44
@nh13
nh13 temporarily deployed to github-actions July 10, 2026 03:45 — with GitHub Actions Inactive
Base automatically changed from nh/fix-clip-unmap-when-fully-clipped to main July 10, 2026 04:08
@nh13
nh13 force-pushed the nh/fix-clip-round2-mate-supp-window branch from 06e0eee to 4faa90e Compare July 10, 2026 04:14
@nh13
nh13 temporarily deployed to github-actions July 10, 2026 04:14 — with GitHub Actions Inactive
@nh13

nh13 commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/commands/clip.rs (1)

630-710: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Threaded fixed-position clipping duplicates clip_pair's branching logic inline.

The R1/R2 fixed-position clip selection here re-implements Clip::clip_pair's is_r1_first/is_r2_last branching (minus metrics) rather than delegating to it. Currently consistent (verified against 463-565), but any future tweak to clip_pair risks silently diverging from the threaded path since there's no shared code path enforcing parity beyond tests. Consider extracting the shared fixed-position-clip decision into a small &self-free helper both paths can call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/commands/clip.rs` around lines 630 - 710, Extract the fixed-position
clipping decision logic currently duplicated in the threaded loop and
Clip::clip_pair into a shared free helper, using the existing read-orientation
checks and R1/R2 threshold parameters. Update both the threaded pair branch and
clip_pair to call this helper, while preserving their separate metrics and other
clipping behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/fgumi-sam/src/record_utils.rs`:
- Around line 516-533: The typed unsoftclipped_end function lacks the
empty-CIGAR handling present in unsoftclipped_end_raw. Add a guard after
converting the record CIGAR operations that returns None when the operations are
empty, and update the function documentation to state this behavior.

In `@tests/integration/test_clip_command.rs`:
- Around line 269-286: The combined fixed, overlap, and mate-extension clipping
test uses weak range-based assertions instead of validating the expected output.
In the loop over records in this test, assert each read’s identity and exact
expected CIGAR, following the independent exact-CIGAR oracle used by
test_clip_command_threads_mode_fragment, while retaining the check that both
expected reads are present.

---

Outside diff comments:
In `@src/lib/commands/clip.rs`:
- Around line 630-710: Extract the fixed-position clipping decision logic
currently duplicated in the threaded loop and Clip::clip_pair into a shared free
helper, using the existing read-orientation checks and R1/R2 threshold
parameters. Update both the threaded pair branch and clip_pair to call this
helper, while preserving their separate metrics and other clipping behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: aa4f6e9b-20a5-4f01-b48b-81b89d714821

📥 Commits

Reviewing files that changed from the base of the PR and between cbc7d72 and 4faa90e.

📒 Files selected for processing (4)
  • crates/fgumi-sam/src/clipper.rs
  • crates/fgumi-sam/src/record_utils.rs
  • src/lib/commands/clip.rs
  • tests/integration/test_clip_command.rs

Comment thread crates/fgumi-sam/src/record_utils.rs
Comment thread tests/integration/test_clip_command.rs Outdated
@nh13

nh13 commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

Addressed the two inline findings:

  • unsoftclipped_end (fgumi-sam record_utils.rs) now guards an empty CIGAR and returns None, matching its raw sibling unsoftclipped_end_raw; added a regression test asserting None for a mapped, empty-CIGAR record.
  • The combined fixed + overlap + mate-extension clip test now asserts the exact post-clip CIGARs for both reads (R1 1H49M100H, R2 50H49M1H) instead of a range check. Deriving the exact CIGAR surfaced that the fixture left TLEN unset, so is_fr_pair_raw rejected the pair and overlap/mate-extension clipping never actually ran — the old assertion passed on fixed clipping alone. Setting a non-zero TLEN makes the pair a real FR pair so all three clip types now genuinely execute.

On the outside-diff-range note about the threaded fixed-position clipping duplicating clip_pair (clip.rs ~630-710): leaving as-is. The two paths differ intentionally — the threaded R2 slot is guaranteed to be a last-segment read by find_primary_pair_indices, so it always uses the read-two thresholds, whereas clip_pair must branch on is_r2_last for the general case. Extracting a shared helper would have to re-introduce that branch and would obscure the invariant, for a maintainability-only gain the review itself rated a poor tradeoff.

@nh13
nh13 force-pushed the nh/fix-clip-round2-mate-supp-window branch from 4faa90e to f210aa2 Compare July 10, 2026 18:28
@nh13
nh13 temporarily deployed to github-actions July 10, 2026 18:28 — with GitHub Actions Inactive
@nh13

nh13 commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nh13

nh13 commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nh13

nh13 commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nh13
nh13 force-pushed the nh/fix-clip-round2-mate-supp-window branch from f210aa2 to 8dd3177 Compare July 11, 2026 03:11
@nh13
nh13 temporarily deployed to github-actions July 11, 2026 03:11 — with GitHub Actions Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/integration/test_clip_command.rs`:
- Around line 166-388: Add an end-to-end --threads integration test covering a
template with primary R1, primary R2, and supplementary R1 records, verifying
Clip::execute repairs the supplementary mate metadata after clipping. Extend
OutputRecord/read_output_records or add a helper to independently decode and
assert supplementary mate reference/position/strand, MC, MQ, and template length
against expected primary-pair values; ensure the test exercises
execute_threads_mode wiring and record ordering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d8e7bde3-0945-4f1f-9186-720548f5d30e

📥 Commits

Reviewing files that changed from the base of the PR and between 4faa90e and 8dd3177.

📒 Files selected for processing (4)
  • crates/fgumi-sam/src/clipper.rs
  • crates/fgumi-sam/src/record_utils.rs
  • src/lib/commands/clip.rs
  • tests/integration/test_clip_command.rs

Comment thread tests/integration/test_clip_command.rs
…t-order (R2-CLIP-02/03/04)

Three round-2 clip parity fixes, stacked on #492 (CLIP-01 / setMateInfo):

- R2-CLIP-02: the clip loop was `if len==1 {frag} else if len==2 {pair}` with no
  else, so any template with a secondary/supplementary read (len > 2) was left
  COMPLETELY unclipped with no mate fixing. Now locate the primary R1/R2 by SAM
  flags (fgbio `Template.r1`/`r2`), clip them, and repair mate info on every
  supplementary alignment via a port of htsjdk 5.0.0
  `SamPairUtil.setMateInformationOnSupplementalAlignment` (mate ref/pos/strand,
  mate-unmapped flag, TLEN = -matePrimary.TLEN, MC when mate mapped, and MQ
  unconditionally). Both the single-threaded and threaded paths are refactored.

- R2-CLIP-03: `--clip-bases-past-mate` bounded the mate window with unclipped
  start/end (soft + HARD clips); fgbio uses `mate.unSoftClipped{Start,End}` (soft
  only, since hard-clipped bases are physically absent). Added soft-only
  `unsoftclipped_{start,end}[_raw]` helpers and used them in both clippers, so a
  hard-clipped mate no longer widens the window and under-clips.

- R2-CLIP-04: `--sort-order` on clip only relabeled the SO header field (a no-op);
  removed the flag entirely (rely on a downstream `fgumi sort`).

fgbio-oracle parity (fgbio 4.1.0 ClipBam, default Hard):
- CHIM (len-3 chimeric template, --clip-overlapping-reads): before DIFFER
  (fgumi left 100M/100M unclipped, supp mate-info stale) -> after MATCH
  (75M25H/25H75M, supp gets MC/MQ/mate-pos/TLEN like fgbio).
- HARDMATE (FR dovetail, reverse mate 50M50H, --clip-bases-past-mate): before
  DIFFER (fgumi 110M10H) -> after MATCH (60M60H, past mate's soft end).
- SORTFLAG: --sort-order now rejected ("unexpected argument").

Fixture: reports/fgbio-parity-fixtures/r2-clip-02-03-04.sh (+ before/after evidence).
Tests: soft-only helper unit tests (typed + raw), find_primary_pair_indices
partition test; full suite 2213 passed; ci-fmt/ci-lint clean.
@nh13
nh13 force-pushed the nh/fix-clip-round2-mate-supp-window branch from 8dd3177 to 6783d10 Compare July 11, 2026 04:28
@nh13
nh13 temporarily deployed to github-actions July 11, 2026 04:28 — with GitHub Actions Inactive
@nh13

nh13 commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

Re: the outside-diff suggestion to extract the duplicated fixed-position-clip decision from the threaded loop (clip.rs:630-710) and Clip::clip_pair into a shared free helper — declining this one.

The finding itself is tagged 🔵 Trivial / ⚖️ Poor tradeoff, and I agree with that self-assessment: the threaded path and clip_pair are currently consistent, and the "shared decision" is just the is_r1_first / read-one-vs-read-two threshold selection — small enough that extracting it into a &self-free helper adds an indirection layer without removing meaningful duplication. Parity between the two paths is already pinned by the exact-post-clip-CIGAR integration tests (test_clip_command_threads_mode_primary_pair_all_options and the new supplementary-mate-repair test), so a future divergence would be caught by tests rather than silently pass. Leaving the two call sites as-is.

@nh13

nh13 commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/commands/clip.rs`:
- Around line 305-332: The match handling primary read indices omits templates
containing only a primary R2, so they bypass clipping and mate-info repair. In
both match sites using find_primary_pair_indices, add a (None, Some(i2)) arm
mirroring the existing (Some(i1), None) handling: clip records[i2], and perform
any corresponding lone-read mate-info repair required by that path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c6292d24-1aa6-4484-be58-b9daf5d49bf3

📥 Commits

Reviewing files that changed from the base of the PR and between 8dd3177 and 6783d10.

📒 Files selected for processing (4)
  • crates/fgumi-sam/src/clipper.rs
  • crates/fgumi-sam/src/record_utils.rs
  • src/lib/commands/clip.rs
  • tests/integration/test_clip_command.rs

Comment thread src/lib/commands/clip.rs
@nh13
nh13 merged commit e8b73b6 into main Jul 11, 2026
10 checks passed
@nh13
nh13 deleted the nh/fix-clip-round2-mate-supp-window branch July 11, 2026 05:48
@nh13 nh13 mentioned this pull request Jul 11, 2026

This branch was previously deployed

1 inactive deployment
github-actions — 6783d100 Deployed Jul 11, 2026 by nh13 via coverage #2260
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