Skip to content

feat(verify): stream suspects to disk via an external-sort sink (#156)#165

Merged
elfensky merged 2 commits into
developfrom
feature/verify-stream
Jul 11, 2026
Merged

feat(verify): stream suspects to disk via an external-sort sink (#156)#165
elfensky merged 2 commits into
developfrom
feature/verify-stream

Conversation

@elfensky

Copy link
Copy Markdown
Owner

Stream verify suspects to disk via an external-sort sink (#156)

verify accumulated every suspect in a list before writing, so peak RSS scaled with the finding count — the last part of the verify path whose footprint grew with the corpus. Issue #156 measured this at ~4 GB under high suspect count.

The change

SuspectSink (in verify/report.py) replaces the in-memory list with an external merge-sort — each full chunk is sorted and spilled to a temp file, write k-way-merges the runs, and a running Counter/hard/total tally feeds the summary. run_verify and run_orbit_pass stream into it instead of building lists (orbit outliers were the main offender). Peak memory is now one chunk regardless of suspect count. Mirrors the existing grouping.ExternalSorter.

Byte-identity preserved by construction

suspects.jsonl / summary.{json,md} bytes are unchanged:

  • the per-suspect serialization is extracted into one shared primitive (_suspect_line) used by both the sink and the retained list renderers;
  • summary.json stays sort_keys=True, summary.md sorts its tally, so both are input-order-independent;
  • heapq.merge's key-tie ordering (earlier run first, then the in-memory tail) reproduces the exact stable add-order that sorted(key=_sort_key) gave the list path.

tests/test_verify_stream.py locks this: TestByteEquivalence asserts the streamed output equals the list renderers across the empty / no-spill / multi-spill paths (including a tie-key pair), and TestConstantMemory forces 10 spills at chunk_size=10 and asserts the buffer never exceeds one chunk.

Validation

  • 787 tests pass; ruff check + ruff format --check clean.
  • Capstone at scale: verify --orbit over the full cleaned corpus (232,281,894 records / ~30 GB) held peak RSS flat at 181 MB (dedup, already streamed, 144 MB). 88,041 suspects found, byte-deterministic output. (This run's suspect count sat under the 200K chunk, so the in-memory tail path was exercised; the spill/merge path is covered by the unit tests above — a --all orbit run would exercise spills on real data.)

Closes #156.

🤖 Generated with Claude Code

elfensky and others added 2 commits July 11, 2026 14:16
Verify accumulated every suspect in a list before writing, so peak RSS
scaled with the finding count — ~280K orbit outliers on the full corpus
would all sit in RAM at once, breaking the constant-memory invariant.

Add SuspectSink: an external merge-sort (spill each full chunk to a temp
file, k-way-merge on write) plus a running Counter/hard/total tally, so
peak memory is one chunk regardless of suspect count. run_verify and
run_orbit_pass now stream into the sink instead of building lists.

Output is byte-identical to the old list writer by construction: the
serialization is extracted into one shared primitive (_suspect_line) used
by both paths, summary.json stays sort_keys=True, summary.md sorts its
tally, and heapq.merge's key-tie ordering (earlier run first, then the
in-memory tail) reproduces the stable add-order that sorted(key=_sort_key)
gave the list path. A new byte-equivalence test locks this against the
retained list renderers across the empty, no-spill, and multi-spill paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CHANGELOG gains a Changed entry; ARCHITECTURE's verify/report row now
describes the external-sort SuspectSink alongside the list renderers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@elfensky
elfensky merged commit d5a8c64 into develop Jul 11, 2026
2 checks passed
@elfensky
elfensky deleted the feature/verify-stream branch July 11, 2026 15: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.

1 participant