Skip to content

Extract Filter Spread into a pure, tested Core module#651

Merged
Hirogen merged 14 commits into
Developmentfrom
feature/filter-spread-extraction
Jul 9, 2026
Merged

Extract Filter Spread into a pure, tested Core module#651
Hirogen merged 14 commits into
Developmentfrom
feature/filter-spread-extraction

Conversation

@Hirogen

@Hirogen Hirogen commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

LogWindow.cs carried two divergent private copies of the "filter spread" arithmetic (the back/fore context lines included around each filter hit) — one for the serial filter and pipes, one in Core for the parallel filter. Neither had a single test, and they had already drifted apart. This PR consolidates all of it into one pure module, LogExpert.Core.Classes.Filter.FilterSpread, pinned by 18 table-driven NUnit tests.

What changed

  • New: FilterSpread (Core) — four pure operations, no dependency on controls, callbacks, readers, or locks:
    • Expand — hit → back-spread lines, hit, fore-spread lines (deduplicated, clamped)
    • TrimHistory — duplicate-suppression window (2 × 99)
    • ShiftLines / RebuildHistory — multi-file rollover adjustment
  • All four call sites delegate: the parallel filter (Filter.cs), the Log Window's serial filter, Filter Pipe processing, and rollover shifting. The old copies are deleted; call sites keep their own locking, progress, and GUI refresh.
  • One constant: the UI spread knobs' maximum and the suppression window both derive from FilterSpread.SPREAD_MAX (99), so they can no longer drift apart.

Behavior changes (intentional, both test-pinned)

  1. Line 0 is now valid back-spread context. Previously lineNum - i > 0 silently excluded the first line of the file from every hit's leading context.
  2. The parallel filter no longer emits duplicate context lines at spread > 50. Its dedup-history window was 2 × 50 while the UI allows spreads up to 99; the window is now 2 × 99 everywhere, matching the serial filter.

An equivalence-guard test pins the shared per-hit accumulation recipe so the serial and parallel paths are held to identical output from now on.

Deliberately untouched: FilterPipe.ShiftLineNums (different semantics — -1 placeholders preserve index mapping) and the painting/grid code.

Testing

  • 18 new tests in LogExpert.Tests/Filter/FilterSpreadTests.cs: expansion edge table (line 0, EOF clamp, 0/0 shortcut, asymmetric/overlapping spreads, hit already in history), trim boundary at 2 × 99, rollover shift/drop/rebuild, IList trim, and the accumulation equivalence guard.

Hirogen and others added 11 commits July 9, 2026 18:22
Move the back/fore spread expansion and duplicate-suppression history
trimming out of the multi-threaded filter into FilterSpread, a pure
static module in LogExpert.Core, pinned by table-driven NUnit tests.

Two deliberate behavior changes, both test-pinned:
- Line 0 is now a valid back-spread context line (was excluded by an
  off-by-one boundary check).
- The history window is unified at 2x99 (the UI knob's maximum spread);
  the Core filter previously used 2x50, so parallel filtering with
  spread > 50 could emit duplicate context lines.

The Log Window's serial filter, Filter Pipe processing and rollover
shifting migrate in follow-up tickets (see tickets.md).
The Log Window's single-threaded filter path now accumulates per-hit
results through FilterSpread.Expand/TrimHistory instead of its inline
copy of the spread arithmetic. Locking, hit recording and the count
capture stay at the call site.

Behavior change on the serial path (sanctioned by the spec, pinned by
ticket 1's tests): line 0 is now a valid back-spread context line. The
history window is unchanged (was already 2x99 here).

Adds an equivalence-guard test pinning the shared accumulation recipe
(hit -> Expand -> append -> TrimHistory) with a worked example, so the
serial and parallel call sites are held to identical output.

The private GetAdditionalFilterResults copy remains for the Filter Pipe
path (ticket 3); it and SPREAD_MAX are deleted in ticket 5.
ProcessFilterPipes now expands hits and trims the pipe history through
FilterSpread instead of the Log Window's private copy. Pipe sequencing
(OpenFile / per-line WriteToPipe / CloseFile) is unchanged; the per-line
history trim is observably equivalent (history is only mutated here, so
it never exceeds the window by more than one entry per add).

Behavior change on the pipe path (sanctioned by the spec, pinned by the
module tests): line 0 is now a valid back-spread context line, so pipe
tabs receive the same context as the filter view. The trim window is
unchanged (this path already used 2x99).

TrimHistory widens from List<int> to IList<int> because the pipe
history is an IList; trimming now removes from the front in a loop,
pinned by a Collection<int>-backed test.

Pulled forward from ticket 5: the private GetAdditionalFilterResults
copy in LogWindow is deleted - both its callers are now migrated, so it
was dead code. SPREAD_MAX stays (knobs + ShiftFilterLines, tickets 4-5).
ShiftFilterLines now delegates to two new pure module operations:
- ShiftLines: subtract the rollover offset, drop lines that rolled off
  the start of the virtual file.
- RebuildHistory: rebuild the duplicate-suppression history from the
  tail of the shifted results, capped at SPREAD_MAX (99, not the 2x99
  trim window - preserving the original rollover behavior).

The call site keeps its lock scope (result-list shift inside the lock,
hit-list shift and history rebuild outside, exactly as before) and the
GUI refresh. Both operations are TDD-pinned: plain shift, rolled-off
lines dropping out, tail rebuild at the 99 cap, shorter-than-window
results, and empty input.

Note: four pre-existing commented-out lines inside the rewritten method
body were removed along with the rewrite (old grid-refresh experiments).

FilterPipe.ShiftLineNums is deliberately not migrated - it has
different semantics (-1 placeholders preserve index mapping).
…et 5)

The Log Window no longer carries any Filter Spread logic or constants:
its private SPREAD_MAX is deleted and both spread knobs' maximum now
references FilterSpread.SPREAD_MAX, so the knob range and the
duplicate-suppression window derive from one value and cannot drift
apart again.

CONTEXT.md gains a Filtering section defining Filter Spread (Back
Spread / Fore Spread), its ownership by the Core module, and the
99 / 2x99 derivation. Note: FilterParams itself does not clamp its
values; the 99 cap is enforced at the UI knobs.

This completes the Filter Spread extraction epic (tickets.md): all
three call sites delegate, the arithmetic exists once in Core, and both
sanctioned behavior changes are test-pinned.
Comment thread src/LogExpert.Core/Classes/Filter/FilterSpread.cs Fixed
@Hirogen
Hirogen merged commit 8a06589 into Development Jul 9, 2026
1 check passed
@Hirogen
Hirogen deleted the feature/filter-spread-extraction branch July 9, 2026 17:45
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