Wave 1 of the perf-rust-optimization heist. Algorithmic-only wins, zero
public-API breakage, identical detection results (290-pair golden gate).
Changes:
- hash.rs: `roll()` now takes a precomputed `window_power: u64` instead of
recomputing `base_pow(window_size - 1)` on every call. Removes an
O(window_size) loop from the inner sliding-window hot path. `base_pow`
is now `pub` so callers can hoist the power once per format group.
- detect.rs: hoist `window_power = base_pow(min_tokens - 1)` once before
the outer file loop in `detect_in_group()`. Pass it into every `roll()`
call.
- detect.rs: switch the per-call `by_format` map from `std::HashMap` to
`FxHashMap` for consistency with the rest of the detection path.
- store.rs: add `Store::reserve(n)` as a default-noop trait method
(performance hint only — correctness must not depend on it). Implement
on `MemoryStore` as `self.inner.reserve(n)`. Trait remains `dyn`-safe
(verified by new test that calls `reserve` through `&mut dyn Store`).
- detect.rs: call `store.reserve(total_windows)` once before the detection
loop, where `total_windows` is the sum of `hashes.len() - min_tokens`
across all prepared files. Eliminates `FxHashMap` rehashing mid-detect.
Tooling:
- Add `rust/scripts/regression_check.sh` — golden-baseline gate run after
each Wave change. Normalizes file paths to `../fixtures/...` form so the
gate is independent of absolute vs relative path emission.
- Add `rust/tests/golden.json` — frozen 290-pair clone set captured from
the pre-Heist baseline.
Verification:
- `cargo test -p cpd-core` — 22 pass
- `scripts/regression_check.sh` — 290 pairs match golden, exit 0