# Surviving mutants accepted as of the sweeps below. `mutants-summary` fails only on a mutant that
# is NOT in this list, so red means "this change added a survivor" rather than "the crate still has
# survivors" -- which it always will, because some survivors are provably equivalent mutants and no
# test can kill those.
#
# This is a debt register, not an approval. Entries marked "Debt" are holes that should shrink;
# entries marked "believed equivalent" carry the argument for why no test can close them, and a
# claim of equivalence without an argument does not belong here.
#
# Source: run 32347677860 at e8852c2, 2026-08-20, whole crate, 96 shards.
#   caught 3460 | missed 414 | timeout 55 | unviable 336
# Shard 39 uploaded no artifact (it ran 88 minutes against a ~20-minute median), so ~45 mutants in
# that shard are unmeasured and absent from this list; the summary job now fails on a missing shard
# rather than collating 95 of 96 into a total that looks complete.
#
# Since re-measured locally, and those sections replaced: `src/tree.rs` (2026-08-20 after the
# rebuild-cost fix, and again 2026-08-21 whole-file), `src/clustering/gmm.rs` and
# `src/clustering/nmf.rs` (2026-08-21, entry by entry), `src/clustering/kprototypes.rs` (2026-08-22,
# entry by entry), `src/clustering/vmf.rs` (2026-08-22 entry by entry, then **2026-08-23
# whole-file** after the `log_iv` asymptotic branch), `src/clustering/nmf.rs` +
# `src/clustering/pca.rs` together (**2026-08-24 whole-file**, after `NmfSpec` became
# `ProjectionSpec` and `project_pca` was added), `src/clustering/kmeans.rs` (**2026-08-24
# whole-file**, after the k-means++ sampling weight gained the leaf variance term),
# `src/clustering/hdbscan.rs` (**2026-08-24 whole-file**, after the point-count and dendrogram-walk
# fixes -- see that section for why its count rose from 17 to 21 without a regression), and
# `src/clustering/kmeans.rs` again (**2026-08-27 whole-file**, in one run with
# `src/clustering/xmeans.rs` and `src/model.rs`, after #96 moved the BIC out of this file and #101
# lifted the auto-`k` ceiling). `src/clustering/medoid.rs` (**2026-08-25 whole-file**),
# `src/wasserstein.rs` (**2026-08-26 whole-file**), `src/clustering/xmeans.rs` + `src/model.rs`
# (**2026-08-27**) and `src/order.rs` (**2026-09-05 whole-file**) are first measurements rather than
# replacements -- those modules postdate the whole-crate sweep entirely. The entry-by-entry form is
# sound where the only change to the file was *test* code: adding a test can kill a mutant but can
# never create one, so nothing outside the recorded set needs re-measuring. It is not sound after a
# production edit -- re-measure the file whole then, which also renumbers every `line:col` in that
# section. `src/clustering/kprototypes.rs` is the standing example: it was re-measured entry by
# entry on 2026-08-22 and again (**2026-09-01 whole-file**) once the third, directional block moved
# every line in it.
#
# The entry-by-entry runs used `cargo test --lib`, not the `--test-tool nextest` the CI job uses.
# The difference is `tests/integration_api.rs`, whose four tests assert label counts and a 4-blob
# recovery; none of them reaches a variance floor, a BIC tie, an empty component or the NMF sketch,
# so none can close an entry the 399-test library suite left open.
#
# Tests added on 2026-08-20 after the whole-crate sweep already kill part of the remaining list
# (DenStream prune, both BIC parameter counts). The next run reports those under "Baseline entries
# that are now caught" -- trim them then, from measurement rather than memory.
#
# KNOWN WEAKNESS OF THIS FILE: an entry is keyed by `file:line:col`, so *any* edit that shifts a
# line invalidates every key below it in that file, and the ratchet then reads a moved survivor as
# a new one. Do not reconcile a shifted section by hand -- re-measure that file whole
# (`cargo mutants -f <file>`) and replace its section, which is what was done for `src/tree.rs` on
# 2026-08-20 after the rebuild-cost fix moved its production lines.
#
# A `timeout` is a detection, not a survivor: the mutant made the suite hang (a `rebuild` or `split`
# that no longer terminates, a divergent `ln_gamma` series). Only `missed.txt` feeds this list.
#
# Regenerate: gh run download <id> -p 'mutants-out-*' && cat */missed.txt | sort -u
# Narrow, locally: cargo mutants -f <file> -j2   (-j4 oversubscribes rayon and reports false timeouts)
#
# Run it inside a memory cap. A one-operator mutant of `hdbscan.rs` was measured allocating 15-17 GB
# on a tiny fixture (fixed in d34dd17), and with `systemd-oomd` inactive that reaches the *global*
# OOM killer, which picks a victim machine-wide rather than inside the job. `-j1` under a shared cap,
# because at `-j2` the cgroup may kill the other mutant's test binary and record a survivor as
# caught. A dead scope unit stays loaded, so use a fresh `--unit` name or `systemctl --user
# reset-failed` first; and `systemd-run ... | tail` reports `tail`'s exit code, so check
# `ActiveState` rather than trusting it.
#   systemd-run --user --scope --unit=<fresh> -p MemoryMax=10G -p MemorySwapMax=0 \
#     -- timeout 10800 cargo mutants -f <file> -j1 --test-tool nextest --output /tmp/mut-<name>


# ── src/stats.rs (47) ──
# The initial guess of `inv_reg_lower_gamma` (Numerical Recipes `invgammp`) and the
# convergence guards of `gser` / `gcf`. Believed equivalent: twelve cubically convergent
# Halley steps absorb any perturbation of the guess, and the guards test values the series
# never reaches (FPMIN = 1e-300). The round-trip test asserts P(a, P^-1(a, p)) = p to 1e-12
# relative over 17 x 16 (a, p) points, so a mutant that moved the result would fail it.
src/stats.rs:106:21: replace > with >= in inv_reg_lower_gamma
src/stats.rs:107:21: replace > with >= in inv_reg_lower_gamma
src/stats.rs:115:10: replace > with >= in inv_reg_lower_gamma
src/stats.rs:116:23: replace < with <= in inv_reg_lower_gamma
src/stats.rs:118:31: replace + with * in inv_reg_lower_gamma
src/stats.rs:118:31: replace + with - in inv_reg_lower_gamma
src/stats.rs:118:35: replace * with + in inv_reg_lower_gamma
src/stats.rs:118:35: replace * with / in inv_reg_lower_gamma
src/stats.rs:118:53: replace + with * in inv_reg_lower_gamma
src/stats.rs:118:57: replace * with + in inv_reg_lower_gamma
src/stats.rs:118:57: replace * with / in inv_reg_lower_gamma
src/stats.rs:118:68: replace + with * in inv_reg_lower_gamma
src/stats.rs:118:68: replace + with - in inv_reg_lower_gamma
src/stats.rs:118:72: replace * with + in inv_reg_lower_gamma
src/stats.rs:118:72: replace * with / in inv_reg_lower_gamma
src/stats.rs:119:14: replace < with <= in inv_reg_lower_gamma
src/stats.rs:122:23: replace - with + in inv_reg_lower_gamma
src/stats.rs:122:36: replace * with + in inv_reg_lower_gamma
src/stats.rs:124:21: replace - with + in inv_reg_lower_gamma
src/stats.rs:124:34: replace + with * in inv_reg_lower_gamma
src/stats.rs:124:34: replace + with - in inv_reg_lower_gamma
src/stats.rs:125:14: replace < with <= in inv_reg_lower_gamma
src/stats.rs:126:20: replace / with % in inv_reg_lower_gamma
src/stats.rs:126:20: replace / with * in inv_reg_lower_gamma
src/stats.rs:128:28: replace - with + in inv_reg_lower_gamma
src/stats.rs:128:38: replace / with % in inv_reg_lower_gamma
src/stats.rs:128:38: replace / with * in inv_reg_lower_gamma
src/stats.rs:128:45: replace - with + in inv_reg_lower_gamma
src/stats.rs:128:45: replace - with / in inv_reg_lower_gamma
src/stats.rs:147:21: replace * with + in inv_reg_lower_gamma
src/stats.rs:147:21: replace * with / in inv_reg_lower_gamma
src/stats.rs:147:26: replace + with * in inv_reg_lower_gamma
src/stats.rs:147:26: replace + with - in inv_reg_lower_gamma
src/stats.rs:149:23: replace < with <= in inv_reg_lower_gamma
src/stats.rs:149:23: replace < with == in inv_reg_lower_gamma
src/stats.rs:56:22: replace < with <= in gser
src/stats.rs:56:22: replace < with == in gser
src/stats.rs:73:20: replace < with <= in gcf
src/stats.rs:73:20: replace < with == in gcf
src/stats.rs:77:20: replace < with <= in gcf
src/stats.rs:77:20: replace < with == in gcf
src/stats.rs:83:17: replace - with + in gcf
src/stats.rs:83:17: replace - with / in gcf
src/stats.rs:83:30: replace < with <= in gcf
src/stats.rs:83:30: replace < with == in gcf
src/stats.rs:94:10: replace < with <= in reg_lower_gamma
src/stats.rs:94:14: replace + with * in reg_lower_gamma

# ── src/clustering/vmf.rs (18) ──
# Re-measured 2026-08-23, WHOLE FILE, after the `log_iv` asymptotic branch -- a production edit, so the
# entry-by-entry form was unsound and every line:col here is new.
#   394 mutants in 2h at `cargo mutants -f src/clustering/vmf.rs -j4 -- --lib`
#   caught 322 | missed 18 | unviable 30 | timeout 23
# Two changes against the 19 recorded on 2026-08-22, in opposite directions and both explained:
#   - `73:15 > with ==` (now 108:15) left this list for the TIMEOUT column. `m == kappa*0.5` is true
#     only when kappa/2 is an integer that `m` lands on exactly, so for the fixtures' kappa it never
#     fires and the loop runs to the 200_000 hard cap on every call. That is the same argument the
#     old entry gave -- the run now shows its cost rather than its silence.
#   - `80:43`, the p^6 sign in the new `log_iv_asymptotic`, was missed and is now CLOSED by a golden
#     at nu = 4095 (d = 8192). p = 1/sqrt(1+(kappa/nu)^2) grows towards 1 as kappa/nu falls, so U_2's
#     high powers only become visible at large nu: flipping that sign moves the value by 1.28e-14
#     relative there, against 8.78e-17 for the correct expression, and is invisible at nu <= 2047.
#     Verified to fail on revert.
# The 23 timeouts are almost all in `ln_gamma` and `log_iv_series`: they mutate the series' own
# termination, so the loop runs to its 200_000 cap for every call in a 404-test suite. They are
# reported by the summary job but not gated by it.
#
# Believed equivalent -- the series' stopping rule and two ties that assign the value they already
# hold. The series is all-positive with `sum_exp >= 1`, so a term below e^-40 (about half an ulp of
# the running sum) cannot move it: both `108:23` mutants *widen* the peak test (`k + 0.5` and `2k`
# both exceed `k/2` for every k > 0) and only sum more negligible terms; `108:15` `>=` fires one
# iteration earlier than `>`, which is also a widening; `108:61` mutates the 200_000 hard cap, which
# is now unreachable *by construction* -- `log_iv` enters the series only below the branch point, and
# `kappa_max(dim)` holds the low orders that have no branch below it too, so kappa <= 1e4 always and
# the peak sits at m <= 5000; `108:38` differs only on an exact float coincidence, one term below
# half an ulp. `101:18` `log_a >= max_log` takes the other arm of an if whose two arms coincide at
# equality. `265:19`'s `nd <= *di` writes `*di = nd` when the two are already equal. `357:35` negates
# `cohesion_of`'s accumulator, which is only ever read through `norm`, and ‖−v‖ = ‖v‖.
src/clustering/vmf.rs:101:18: replace > with >= in log_iv_series
src/clustering/vmf.rs:108:15: replace > with >= in log_iv_series
src/clustering/vmf.rs:108:23: replace * with + in log_iv_series
src/clustering/vmf.rs:108:23: replace * with / in log_iv_series
src/clustering/vmf.rs:108:38: replace < with <= in log_iv_series
src/clustering/vmf.rs:108:61: replace > with == in log_iv_series
src/clustering/vmf.rs:108:61: replace > with >= in log_iv_series
src/clustering/vmf.rs:265:19: replace < with <= in spherical_pp
src/clustering/vmf.rs:357:35: replace + with - in cohesion_of
# Believed equivalent -- a loop that never breaks early. `it < 0` is never true for a usize, so
# `spherical_lloyd` runs all `max_iter` rounds instead of stopping at convergence; the update is
# idempotent once the labelling is stationary, so labels, centers and cohesion are bit-identical and
# only the work differs.
src/clustering/vmf.rs:306:27: replace > with < in spherical_lloyd
# Believed equivalent -- zero total responsibility. `ntot = sum_c sum_i n_i r_ic = sum_i n_i`, because
# the E-step normalizes each row of `resp` to sum to one and every leaf mass is positive. So `ntot > 0`
# always holds, `>=` cannot differ from `>`, and the `1/k` fallback that the `478:26` pair rewrites is
# unreachable.
src/clustering/vmf.rs:475:34: replace > with >= in movmf_once
src/clustering/vmf.rs:478:26: replace / with % in movmf_once
src/clustering/vmf.rs:478:26: replace / with * in movmf_once
# Believed equivalent -- `init_kappas` accumulates `-sum n_i mu_i` instead of `+sum`, and the result is
# only ever read through `norm`, so `rbar` is unchanged.
src/clustering/vmf.rs:525:21: replace + with - in init_kappas
# Believed equivalent -- a constant offset in a selection score, the same argument as gmm.rs:632:32.
# `bic = -2 lnL + p ln n`, and `(k-1) -> (k+1)` adds exactly 2 to p for every k while `(k-1) -> (k/1)`
# adds exactly 1, so every score shifts by the same c·ln n and the argmin cannot move.
src/clustering/vmf.rs:559:34: replace - with + in movmf_auto
src/clustering/vmf.rs:559:34: replace - with / in movmf_auto
# Debt: two exact-coincidence guards.
#   486:33  needs a component whose *total* responsibility underflows to exactly 0.0 -- the same
#           constraint that keeps gmm.rs:159:22 and 455:22 open, and unreachable for the same reason.
#   561:18  needs two k with a bit-identical BIC.
src/clustering/vmf.rs:486:33: replace > with >= in movmf_once
src/clustering/vmf.rs:561:18: replace < with <= in movmf_auto

# ── src/clustering/kprototypes.rs (3) ──
# Re-measured **2026-09-01 whole-file** after the module grew a third (directional) block: the
# schema, the block weights and the resultant summary renumbered every line here. 162 mutants,
# 143 caught / 4 missed / 15 unviable / 0 timeout, `--test-tool nextest`. The three below are the
# same three the 2026-08-22 run left open, at their new positions; their arguments are unchanged and
# the directional block does not weaken either of them.
#
# The fourth, `403:51: replace * with + in kpp_init`, was a genuine new hole and is **closed** rather
# than recorded: the D²-weighted seeding weight is a *product*, so a micro sitting exactly on an
# already-chosen prototype has probability zero however heavy it is, and the `+` mutant loses that
# guarantee. `kpp_init_spreads_one_prototype_per_far_group` could not see it -- there every weight is
# 1 against a D² of 10^4, so the sum and the product agree to within a rounding error.
# `kpp_init_never_reseeds_a_site_it_already_holds` puts the mass on the coincident site instead
# (twelve micros of weight 100 at the origin against one of weight 0.01 three units away) and fails
# on its first seed under the mutation.
#   407:19  believed equivalent -- `nd <= *di` writes `*di = nd` when the two are already equal.
#   475:31  believed equivalent -- the mutated `+` is the *first* one, so the objective becomes
#           `-sum ssd_i + sum micro_dist_i`. `sum ssd_i` runs over every micro regardless of the
#           labelling, so it is the same constant for every restart and the argmin is unchanged.
#           Measured against the whole library suite, which includes
#           `more_restarts_never_return_a_worse_partition` -- the test a real sign error would fail.
#   464:31  Debt, and expensive to reach: an orphaned prototype is observable only if a later pass
#           runs *and* the collapsed prototype (origin, mode 0, zero resultant) outranks a real one
#           for some micro. The only route to an orphaned prototype here is a duplicated seed, and
#           `kpp_init` duplicates only once every remaining candidate has D² = 0 -- that is, once k
#           exceeds the number of distinct micro sites. But then every cluster is a set of *identical*
#           micros, its prototype coincides with its members at distance zero, and the origin cannot
#           outrank it. A killing fixture has to empty a cluster by a prototype *update* rather than
#           by seeding, while another cluster keeps enough internal spread that one of its micros is
#           closer to the origin than to its own centroid. The directional block moves this away from
#           reach rather than towards it: a collapsed prototype has a cancelled resultant, whose
#           angular term is the *maximum* `2·mass` against every micro.
src/clustering/kprototypes.rs:407:19: replace < with <= in kpp_init
src/clustering/kprototypes.rs:464:31: replace > with >= in kprototypes
src/clustering/kprototypes.rs:475:31: replace + with - in kprototypes

# ── src/tree.rs (2) ──
# Re-measured 2026-08-21 against the five fixtures added with it: 130 of 156 mutants
# before the local 90-minute cap stopped the run (112 caught / 2 missed / 6 timeout /
# 10 unviable). `660:25` sits in `split`, which the cap never reached, and was measured
# on its own -- it is caught by `a_split_seeds_on_two_distinct_children`, and it is *not*
# the equivalent mutant it looks like: `Radius` and `AverageIntercluster` both give a
# child a nonzero distance to itself, so `(i * 1)..k` can seed both sides on one child.
# All twenty of the twenty-two entries this replaces except the two below are now caught
# -- the grown-threshold arithmetic, both `sibling_pairs` scans, the per-shard leaf
# budget, both rebuild triggers, the compaction margin, `rebuilds()`, the rebalance pass
# and the seed scan. The 6 timeouts
# are genuine non-termination in `rebuild` (a mutated break condition or liveness test)
# and are detections, not survivors.
#
# Both remaining are believed equivalent, and both sit in `rebuild`:
#   319:19  `merged > 0` -> `>=`. Entering the block with `merged == 0` runs
#           `drop_merged` against an all-alive mask, which is a no-op; `widest` is
#           still zero, so `grown` is zero and cannot exceed a non-negative threshold,
#           and `merged_since_rebalance` gains nothing.
#   323:22  `grown > self.threshold` -> `>=`. At equality the assignment stores the
#           value that is already there.
src/tree.rs:319:19: replace > with >= in CFTree<R, C, D, A>::rebuild
src/tree.rs:323:22: replace > with >= in CFTree<R, C, D, A>::rebuild

# ── src/stream.rs (33) ──
# Debt, partly closed on 2026-08-20 (the DenStream prune thresholds are now checked
# against a geometric-series reference). The rest is DbStream and `try_merge`.
src/stream.rs:174:14: replace > with >= in DenStream<R, C>::try_merge
src/stream.rs:174:27: replace / with * in DenStream<R, C>::try_merge
src/stream.rs:220:19: replace > with < in DenStream<R, C>::tick
src/stream.rs:220:19: replace > with == in DenStream<R, C>::tick
src/stream.rs:220:19: replace > with >= in DenStream<R, C>::tick
src/stream.rs:220:42: replace % with + in DenStream<R, C>::tick
src/stream.rs:231:30: replace * with / in DenStream<R, C>::prune
src/stream.rs:233:53: replace * with + in DenStream<R, C>::prune
src/stream.rs:234:32: replace * with / in DenStream<R, C>::prune
src/stream.rs:234:37: replace - with + in DenStream<R, C>::prune
src/stream.rs:234:37: replace - with / in DenStream<R, C>::prune
src/stream.rs:234:44: replace + with * in DenStream<R, C>::prune
src/stream.rs:234:44: replace + with - in DenStream<R, C>::prune
src/stream.rs:234:65: replace / with * in DenStream<R, C>::prune
src/stream.rs:243:22: replace * with + in DenStream<R, C>::cluster
src/stream.rs:243:22: replace * with / in DenStream<R, C>::cluster
src/stream.rs:246:25: replace + with * in DenStream<R, C>::cluster
src/stream.rs:260:61: replace * with + in DenStream<R, C>::cluster
src/stream.rs:260:61: replace * with / in DenStream<R, C>::cluster
src/stream.rs:274:21: delete - in DenStream<R, C>::cluster
src/stream.rs:284:20: delete - in DenStream<R, C>::predict
src/stream.rs:309:33: replace > with >= in DenStream<R, C>::potential_stats
src/stream.rs:483:19: replace > with >= in DbStream<R, C>::tick
src/stream.rs:496:50: replace && with || in DbStream<R, C>::cleanup
src/stream.rs:496:61: replace * with + in DbStream<R, C>::cleanup
src/stream.rs:496:61: replace * with / in DbStream<R, C>::cleanup
src/stream.rs:523:27: replace || with && in DbStream<R, C>::cluster
src/stream.rs:526:24: replace * with / in DbStream<R, C>::cluster
src/stream.rs:552:44: replace < with <= in DbStream<R, C>::nearest
src/stream.rs:563:20: delete - in DbStream<R, C>::predict
src/stream.rs:588:33: replace > with >= in DbStream<R, C>::micro_stats
src/stream.rs:600:10: replace < with <= in pair
src/stream.rs:63:31: replace += with *= in UnionFind::union

# ── src/clustering/scalespace.rs (28) ──
# Debt: mode-counting and bandwidth arithmetic.
src/clustering/scalespace.rs:107:47: replace * with / in bandwidth_range
src/clustering/scalespace.rs:119:20: replace * with / in mean_shift
src/clustering/scalespace.rs:133:20: replace > with >= in mean_shift
src/clustering/scalespace.rs:136:28: replace - with + in mean_shift
src/clustering/scalespace.rs:136:28: replace - with / in mean_shift
src/clustering/scalespace.rs:136:47: replace > with >= in mean_shift
src/clustering/scalespace.rs:159:21: replace * with + in prominence_modes
src/clustering/scalespace.rs:186:21: replace + with * in prominence_modes
src/clustering/scalespace.rs:187:56: replace > with >= in prominence_modes
src/clustering/scalespace.rs:241:5: replace select_scale -> usize with 1
src/clustering/scalespace.rs:250:34: replace > with >= in select_scale
src/clustering/scalespace.rs:254:27: replace && with || in select_scale
src/clustering/scalespace.rs:254:34: replace > with < in select_scale
src/clustering/scalespace.rs:254:34: replace > with == in select_scale
src/clustering/scalespace.rs:254:34: replace > with >= in select_scale
src/clustering/scalespace.rs:263:21: replace + with * in select_scale
src/clustering/scalespace.rs:263:33: replace / with % in select_scale
src/clustering/scalespace.rs:264:25: replace > with < in select_scale
src/clustering/scalespace.rs:264:25: replace > with == in select_scale
src/clustering/scalespace.rs:264:25: replace > with >= in select_scale
src/clustering/scalespace.rs:265:21: replace + with * in select_scale
src/clustering/scalespace.rs:265:33: replace / with % in select_scale
src/clustering/scalespace.rs:268:22: replace - with + in select_scale
src/clustering/scalespace.rs:268:22: replace - with / in select_scale
src/clustering/scalespace.rs:62:56: replace - with + in scale_space
src/clustering/scalespace.rs:93:19: replace < with <= in bandwidth_range
src/clustering/scalespace.rs:96:19: replace < with <= in bandwidth_range
src/clustering/scalespace.rs:99:19: replace > with >= in bandwidth_range

# ── src/clustering/kmeans.rs (5) ──
# Re-measured 2026-08-27 together with `xmeans.rs` and `model.rs`: 417 mutants in 2h03 at -j2 under
# `systemd-run --user --scope --unit=betula-mut-cap -p MemoryMax=10G -p MemorySwapMax=0`, frozen at
# a0b1a3e in a throwaway worktree. 349 caught / 25 missed / 40 unviable / 3 timeouts, of which this
# file contributed 158 mutants and 11 survivors. Six of those eleven are now closed by tests added
# on 2026-08-27, each verified by hand-applying its own mutant to the clean tree and confirming the
# named test fails (`local/scratch/hand_mutants.py`, gitignored):
#   137:22 cop_kmeans          a_weightless_feature_contributes_nothing_and_poisons_nothing
#   332:17 weighted_pick (x2)  weighted_pick_stays_in_range_when_the_scan_runs_off_the_end
#   437:16 update_centers      every_cluster_being_stranded_at_once_is_still_a_reseed
#   450:31 update_centers      every_cluster_being_stranded_at_once_is_still_a_reseed
#   454:38 update_centers      the_stranded_cluster_restarts_on_the_leaf_that_costs_the_most_inertia
#
# The fourteen `in xmeans` entries of the previous list are gone outright: #96 moved that arithmetic
# into `clustering::xmeans::pelleg_moore_bic`, which this run mutated 47 times and missed **none**.
# That is the whole reason the section fell from 25 to 5, and it is the strongest evidence in this
# file that an independent re-derivation is worth more than a fixture: the same expression scored
# 14 survivors inline and zero once a second, separately written formula had to agree with it.
#
# The two `uf_find` mutants (`replace != with ==`) are timeouts, not survivors: they turn the
# union-find path walk into an infinite loop, which the suite detects by hanging.
#
# Believed equivalent, with the argument:
#   228:31  `inertia = inertia + f.ssd() + ...` -> `- f.ssd()`. `Σ_i f.ssd()` does not depend on the
#           assignment, so negating it shifts *every* restart's score by the same constant and the
#           `inertia >= *bi` comparison that picks the best restart is unchanged. The value itself
#           never leaves `cop_kmeans` -- only `assign` is returned.
#   311:19  `if nd < *di { *di = nd }` -> `<=`. The two differ only at `nd == *di`, where the
#           assignment is a no-op. `sq_euclidean` sums squares, so neither side can be `-0.0`.
#   411:21  `else if d < d2 { d2 = d }` -> `<=`. The same no-op, in `nearest_two`.
#
# Debt: the greedy k-means++ trial draw and the sweep's argmax.
#   304:20  `if pot < best_pot` -> `<=`: on two candidates of *exactly* equal potential the later
#           one wins instead of the earlier. Both are optimal by the algorithm's own criterion, so
#           any test would pin an arbitrary choice through the RNG's draw order.
#   589:16  `if bic > best_bic` -> `>=` in `kmeans_auto`: a tie between two different `k` on a score
#           built from a float variance. Not reachable by construction, and `best_bic` starts at
#           `-inf`, which `pelleg_moore_bic` cannot return (the variance is floored at 1e-12).
src/clustering/kmeans.rs:228:31: replace + with - in cop_kmeans
src/clustering/kmeans.rs:304:20: replace < with <= in kmeans_plus_plus
src/clustering/kmeans.rs:311:19: replace < with <= in kmeans_plus_plus
src/clustering/kmeans.rs:411:21: replace < with <= in nearest_two
src/clustering/kmeans.rs:589:16: replace > with >= in kmeans_auto

# ── src/clustering/xmeans.rs (8) ──
# First measurement -- the module postdates every earlier sweep. Same 2026-08-27 run as the section
# above: 102 mutants, 83 caught / 10 missed / 9 unviable. Two of the ten are closed by
# `the_guard_above_the_split_test_refuses_only_what_it_has_to`, added the same day and verified
# against both mutants by hand: `163:26` (`sub.len() < 2` -> `<=`, which would refuse to split any
# two-leaf cluster and stop the recursion at k=2 on a fixture whose score says four) and `163:61`
# (the sign of the term counting the clusters after `j` that have not been re-emitted, which
# over-counts the eventual centre budget and refuses the last split that fits under `k_max`).
#
# Believed equivalent, with the argument:
#   175:22, 175:40, 175:47  the five mutants of `seed ^ ((round as u64) << 32) ^ j as u64`. This is
#           a decorrelation device, not a value: `|`, `&` and `>>` all still hand `kmeans` *a* seed,
#           and the contract the head has to meet is that it recovers the split from any of them --
#           which is what the recovery tests assert. Pinning one would assert an arbitrary integer.
#   137:27  `if start == 1 { 1 } else { SPLIT_N_INIT }` -> `!=`. On the shipped path `k_min` is 1,
#           where the single centre is the weight-mean whatever the seeding does, so the restart
#           count cannot change the output -- only the work. The mutant is observable only through
#           `xmeans(.., k_min > 1, ..)`, and then only on a fixture built so that one k-means++
#           restart falls into a local optimum that four escape: a seed-pinned change detector.
#
# Debt: the two strict comparisons on the score. Both are ties between BIC values computed from
# different partitions, so they need exact float equality between two independent sums.
#   186:50  `BIC(2) > BIC(1)` -> `>=` in Improve-Structure: a tie accepts the split.
#   200:16  `bic > best_bic` -> `>=` across rounds: a tie replaces the retained model with the newer
#           one. Every round that reaches this line added at least one centre, so the two models
#           differ in `k` and their parameter penalties differ by `0.5·(dim+1)·ln nr`.
src/clustering/xmeans.rs:137:27: replace == with != in xmeans
src/clustering/xmeans.rs:175:22: replace ^ with & in xmeans
src/clustering/xmeans.rs:175:22: replace ^ with | in xmeans
src/clustering/xmeans.rs:175:40: replace << with >> in xmeans
src/clustering/xmeans.rs:175:47: replace ^ with & in xmeans
src/clustering/xmeans.rs:175:47: replace ^ with | in xmeans
src/clustering/xmeans.rs:186:50: replace > with >= in xmeans
src/clustering/xmeans.rs:200:16: replace > with >= in xmeans

# ── src/model.rs (0) ──
# Deliberately empty, and kept as a heading so the next reader does not read the absence as "never
# measured". The same 2026-08-27 run covered it for the first time: 157 mutants, 141 caught / 4
# missed / 11 unviable / 1 timeout. All four survivors were in `refine_centers` (the Phase-4 Lloyd
# sweep over raw points) and all four are closed, each verified against its own mutant:
#   300:15, 300:25  each_degenerate_argument_declines_the_sweep_on_its_own
#   316:22          a_point_equidistant_from_two_centres_goes_to_the_earlier_one
#   339:25          a_centre_whose_points_cancel_survives_the_unit_projection
#
# The timeout is `422:18 delete ! in auto_k_ceiling`, and it is a detection rather than an unmeasured
# mutant: dropping the negation hands `AUTO_K_MAX` to the *cut* selectors and the leaf count to the
# sweeps, so `kmeans` and `gmm` auto-`k` fit every `k` up to the leaf count. That is the quadratic
# blow-up the function's own doc comment quantifies, and the suite detects it by hanging.

# ── src/clustering/gmm_toeplitz.rs (23) ──
# Debt. Also the largest timeout pool (17), so some of these are unmeasured rather
# than surviving.
src/clustering/gmm_toeplitz.rs:102:23: replace - with / in levinson_full
src/clustering/gmm_toeplitz.rs:106:14: replace > with >= in levinson_full
src/clustering/gmm_toeplitz.rs:108:21: replace < with <= in levinson_full
src/clustering/gmm_toeplitz.rs:275:30: replace > with >= in fit_component_gs
src/clustering/gmm_toeplitz.rs:276:57: replace - with + in fit_component_gs
src/clustering/gmm_toeplitz.rs:296:41: replace + with - in fit_component_gs
src/clustering/gmm_toeplitz.rs:301:26: replace > with >= in fit_component_gs
src/clustering/gmm_toeplitz.rs:336:24: replace > with >= in fit_component
src/clustering/gmm_toeplitz.rs:338:23: replace * with + in fit_component
src/clustering/gmm_toeplitz.rs:348:62: replace - with + in fit_component
src/clustering/gmm_toeplitz.rs:352:16: replace < with <= in fit_component
src/clustering/gmm_toeplitz.rs:363:17: replace > with >= in argmax
src/clustering/gmm_toeplitz.rs:395:48: replace + with * in gmm_toeplitz_once
src/clustering/gmm_toeplitz.rs:395:48: replace + with - in gmm_toeplitz_once
src/clustering/gmm_toeplitz.rs:423:20: replace > with >= in gmm_toeplitz_once
src/clustering/gmm_toeplitz.rs:442:32: replace / with * in gmm_toeplitz_once
src/clustering/gmm_toeplitz.rs:540:30: replace + with * in gmm_toeplitz_auto_kind
src/clustering/gmm_toeplitz.rs:542:33: replace + with * in gmm_toeplitz_auto_kind
src/clustering/gmm_toeplitz.rs:544:24: replace + with * in gmm_toeplitz_auto_kind
src/clustering/gmm_toeplitz.rs:544:33: replace + with - in gmm_toeplitz_auto_kind
src/clustering/gmm_toeplitz.rs:544:38: replace - with + in gmm_toeplitz_auto_kind
src/clustering/gmm_toeplitz.rs:544:38: replace - with / in gmm_toeplitz_auto_kind
src/clustering/gmm_toeplitz.rs:546:16: replace < with <= in gmm_toeplitz_auto_kind

# ── src/sketch/kll.rs (20) ──
# Debt: compaction levels and capacity arithmetic.
src/sketch/kll.rs:137:9: replace KllSketch::rank -> u64 with 0
src/sketch/kll.rs:137:9: replace KllSketch::rank -> u64 with 1
src/sketch/kll.rs:139:26: replace << with >> in KllSketch::rank
src/sketch/kll.rs:140:15: replace += with *= in KllSketch::rank
src/sketch/kll.rs:140:15: replace += with -= in KllSketch::rank
src/sketch/kll.rs:140:20: replace * with + in KllSketch::rank
src/sketch/kll.rs:140:20: replace * with / in KllSketch::rank
src/sketch/kll.rs:140:46: replace <= with > in KllSketch::rank
src/sketch/kll.rs:196:37: replace < with <= in KllSketch::merge
src/sketch/kll.rs:54:37: replace - with + in KllSketch::capacity
src/sketch/kll.rs:54:37: replace - with / in KllSketch::capacity
src/sketch/kll.rs:55:19: replace / with % in KllSketch::capacity
src/sketch/kll.rs:55:19: replace / with * in KllSketch::capacity
src/sketch/kll.rs:55:45: replace * with + in KllSketch::capacity
src/sketch/kll.rs:55:78: replace + with * in KllSketch::capacity
src/sketch/kll.rs:55:78: replace + with - in KllSketch::capacity
src/sketch/kll.rs:60:18: replace ^= with |= in KllSketch::coin
src/sketch/kll.rs:60:30: replace << with >> in KllSketch::coin
src/sketch/kll.rs:62:18: replace ^= with |= in KllSketch::coin
src/sketch/kll.rs:62:30: replace << with >> in KllSketch::coin

# ── src/clustering/gmm.rs (13) ──
# Re-measured 2026-08-21, entry by entry, each mutation applied at its own line:col and
# run against the full library suite. Six of the nineteen this replaces are now caught:
# both BIC slopes (`selection_sweep`, added 2026-08-20, already killed three of them --
# the list was stale, not the tests), the triangular covariance count, and both
# empty-warm-start fallbacks.
#
# Believed equivalent -- a constant offset in a BIC parameter count. `bic = -2 lnL +
# p*ln n`, so a change that adds the same amount to `p` for every `k` shifts every score
# equally and cannot move the argmin, which is all either `_auto` returns. `(k-1)` ->
# `(k+1)` adds exactly 2; `(k-1)` -> `(k/1)` adds exactly 1 (integer division, k >= 1).
src/clustering/gmm.rs:632:32: replace - with + in gmm_diagonal_auto
src/clustering/gmm.rs:632:32: replace - with / in gmm_diagonal_auto
src/clustering/gmm.rs:660:50: replace - with + in gmm_full_auto
src/clustering/gmm.rs:660:50: replace - with / in gmm_full_auto
#
# Believed equivalent -- assigning at equality. Each of these is `if raw > floor { raw }
# else { floor }` or `if x < floor { x = floor }`; at `raw == floor` both branches store
# the same bits, so no execution can tell them apart.
src/clustering/gmm.rs:105:37: replace > with >= in gmm_diagonal_once
src/clustering/gmm.rs:178:41: replace > with >= in gmm_diagonal_once
src/clustering/gmm.rs:396:34: replace < with <= in gmm_full_once
src/clustering/gmm.rs:491:38: replace < with <= in gmm_full_once
#
# Debt. The M-step guard `nk[c] > 0` needs a component whose *total* responsibility
# underflows to exactly 0.0 -- every micro-cluster more than ~746 nats behind the
# log-sum-exp, including at the component's own mean. Not constructible while the mean
# stays inside the convex hull of the data and the variance is floored at 1e-3*gvar.
# (The *warm-start* guards one loop up, 100:36 and 385:22, are reachable and are pinned.)
src/clustering/gmm.rs:159:22: replace > with >= in gmm_diagonal_once
src/clustering/gmm.rs:455:22: replace > with >= in gmm_full_once
#
# Debt. `<` -> `==` disables the *initialisation* covariance floor, which is a
# conditioning guard that EM erases: measured on a fixture whose init diagonals sit at
# 0.0025-0.01 against a 0.022 floor, the converged log-likelihood moved from
# 7.09723733379987731 to 7.09723733390917122 -- the eleventh significant digit -- and on
# `blobs` it did not move at all. A killing fixture needs the near-singular direction to
# survive to convergence, which is the high-dimensional regime the comment at
# `gmm.rs:482` cites and which no cheap unit fixture reproduces.
src/clustering/gmm.rs:396:34: replace < with == in gmm_full_once
#
# Debt. `score < best_score` -> `<=` differs only when two `k` produce a bit-identical
# BIC. `p` is strictly increasing in `k`, so that needs the log-likelihoods to differ by
# exactly (p_k - p_j)*ln(n)/2 in f64; a fixture built to hit it would be pinned to a bit
# pattern rather than to a property.
src/clustering/gmm.rs:634:18: replace < with <= in gmm_diagonal_auto
src/clustering/gmm.rs:662:18: replace < with <= in gmm_full_auto

# ── src/clustering/hdbscan.rs (21) ──
# Re-measured **2026-08-24 whole-file**, locally and under a memory cap, after the point-count fix
# (835d05f) and the dendrogram-walk fix (d34dd17): 88 mutants in 30 min, 65 caught, 21 missed,
# 1 timeout, 1 unviable. Every line:col below is against that revision.
#
# **21 is not a regression against the old 17.** The old figure came from CI run 32347677860, whose
# shard 39 uploaded no artifact; the six extra entries here are all *additional operators at sites
# the old list already names* (`+= -> -=` where it had only `+= -> *=`, `* -> /` where it had only
# `* -> +`, and so on), which is exactly what a lost shard looks like. The two fixes closed the four
# mutual-reachability entries the old list carried at 117:36 and 151:22/203:15. This whole-file local
# run is the first complete measurement of the file.
#
# `35:30: replace != with == in UnionFind::find` is a **timeout, not a survivor**, and so is absent:
# it makes `find` spin forever on a root, which no test can pass. A timeout is a detection.
#
# The 16 entries from 269:43 down share one measured cause, established by instrumenting the condense
# loop rather than guessed at. All four arms execute heavily on the current fixtures (per run:
# `split` 1-15, `lbig` 0-31, `rbig` 0-7, `neither` 0-10), so this is **covered by execution and not
# by assertion**: excess-of-mass compares `stab[c]` against the sum over `kids[c]`, and on every
# fixture in the suite that comparison has a wide margin -- e.g. 139.5 against children summing to
# 106.4. Perturbing a fall-out term moves the numbers without moving the sign, so the partition is
# unchanged. Each of the 21 was re-applied by hand and the whole `hdbscan::` suite re-run; all 21
# survived and every mutant *not* on this list died, which cross-validates the run.
#
# Strengthening `the_excess_of_mass_boundary_sits_where_the_reference_puts_it` from a cluster-count
# comparison to a full-partition one (ARI against the independent reference at all 16 sweep steps)
# was measured against all 21 and killed **none** of them. Closing this block needs a fixture whose
# excess-of-mass decision is genuinely contested, which is a fixture-search problem, not a missing
# assertion. Recorded as debt rather than tuned to a bit pattern.
#
# Believed equivalent, with the argument:
#   57:31  `self.rank[ra] += 1` -> `*= 1` pins every rank at 0, so `cmp` is always `Equal` and union
#          always attaches `rb` under `ra`. That is still a correct union with path compression --
#          only the tree depth grows. No observable output depends on it.
#   239:26 `node_dist[nd] > 0.0` -> `>= 0.0` reaches `1.0 / 0.0`, which is `f64::INFINITY` -- exactly
#          what the `else` supplies. `node_dist` is a `sqrt` of a sum of squares, so `-0.0` (whose
#          reciprocal would be `-INFINITY`) is unreachable.
# Debt, unproven but unobserved:
#   191:39 and 206:22 are Prim's two tie-breaks. Single-linkage partitions at a threshold are the
#          connected components of the `<= t` graph, which no choice among equal-weight MST edges can
#          change; the condensed tree's *merge order* among tied edges is not covered by that
#          argument, so this is evidence, not a proof.
#   257:15 `nd < m` -> `<=` really does skip the first merge. It is a genuine defect that no fixture
#          in the suite observes.
src/clustering/hdbscan.rs:57:31: replace += with *= in UnionFind::union
src/clustering/hdbscan.rs:191:39: replace < with <= in hdbscan
src/clustering/hdbscan.rs:206:22: replace < with <= in hdbscan
src/clustering/hdbscan.rs:239:26: replace > with >= in hdbscan
src/clustering/hdbscan.rs:257:15: replace < with <= in hdbscan
src/clustering/hdbscan.rs:269:43: replace * with + in hdbscan
src/clustering/hdbscan.rs:284:25: replace += with -= in hdbscan
src/clustering/hdbscan.rs:284:25: replace += with *= in hdbscan
src/clustering/hdbscan.rs:284:35: replace - with / in hdbscan
src/clustering/hdbscan.rs:284:47: replace * with + in hdbscan
src/clustering/hdbscan.rs:284:47: replace * with / in hdbscan
src/clustering/hdbscan.rs:289:25: replace += with -= in hdbscan
src/clustering/hdbscan.rs:289:25: replace += with *= in hdbscan
src/clustering/hdbscan.rs:289:35: replace - with + in hdbscan
src/clustering/hdbscan.rs:289:35: replace - with / in hdbscan
src/clustering/hdbscan.rs:289:47: replace * with + in hdbscan
src/clustering/hdbscan.rs:289:47: replace * with / in hdbscan
src/clustering/hdbscan.rs:294:25: replace += with -= in hdbscan
src/clustering/hdbscan.rs:294:35: replace - with / in hdbscan
src/clustering/hdbscan.rs:294:47: replace * with + in hdbscan
src/clustering/hdbscan.rs:294:47: replace * with / in hdbscan

# ── src/feature.rs (16) ──
# Debt: `SecondMoment::add_scaled` and `trace_under`, plus the FD sketch.
src/feature.rs:39:31: replace + with * in SecondMoment<R>::trace_under
src/feature.rs:39:31: replace + with - in SecondMoment<R>::trace_under
src/feature.rs:47:43: replace + with * in SecondMoment<R>::trace_under
src/feature.rs:47:43: replace + with - in SecondMoment<R>::trace_under
src/feature.rs:495:38: replace / with % in FdSketch<R>::reduce
src/feature.rs:512:41: replace + with - in FdSketch<R>::reduce
src/feature.rs:560:39: replace + with * in <impl ClusterFeature<R> for FdSketch<R>>::variance
src/feature.rs:560:39: replace + with - in <impl ClusterFeature<R> for FdSketch<R>>::variance
src/feature.rs:57:37: replace * with + in SecondMoment<R>::add_scaled
src/feature.rs:65:37: replace * with + in SecondMoment<R>::add_scaled
src/feature.rs:65:37: replace * with / in SecondMoment<R>::add_scaled
src/feature.rs:66:32: replace != with == in SecondMoment<R>::add_scaled
src/feature.rs:68:61: replace + with * in SecondMoment<R>::add_scaled
src/feature.rs:68:61: replace + with - in SecondMoment<R>::add_scaled
src/feature.rs:68:67: replace * with + in SecondMoment<R>::add_scaled
src/feature.rs:68:67: replace * with / in SecondMoment<R>::add_scaled

# ── src/clustering/nmf.rs (12) ──
# Re-measured 2026-08-24, whole file, together with the new `src/clustering/pca.rs` beside it:
# 374 mutants in 2 h at -j4, 308 caught / 19 missed / 47 unviable. A production edit landed here
# (`NmfSpec` -> `ProjectionSpec`, and `project_pca` added), so an entry-by-entry patch would have
# been unsound; the run also renumbers `project`, 696 -> 714.
#
# One entry from the 2026-08-21 list is gone because it is now caught: `106:42 replace * with /`,
# closed by the Eckart-Young reference check. Seven more were measured as survivors in this very run
# and are absent below because tests written straight afterwards kill them -- the five in `pca.rs`
# and the two at `753:39` in `project_pca`. Each of those seven was re-checked by hand-applying the
# mutant and running the module's tests, rather than by trusting that the new fixture must see it.
#
# `src/clustering/pca.rs` has no section: nothing in it survives.
#
# Not measured by this configuration, not survivors. `default = ["parallel"]`, so line 34
# is the `#[cfg(not(feature = "parallel"))]` copy of `build_rows` and is not compiled at
# all -- mutating it cannot fail a test that never runs it. The serial path is covered by
# `cargo test --no-default-features`, which the mutation job does not run.
src/clustering/nmf.rs:34:5: replace build_rows -> Vec<Vec<R>> with vec![]
src/clustering/nmf.rs:34:5: replace build_rows -> Vec<Vec<R>> with vec![vec![Default::default()]]
src/clustering/nmf.rs:34:5: replace build_rows -> Vec<Vec<R>> with vec![vec![]]
#
# Believed equivalent -- the range finder is insensitive to its own sketch, by design.
# Everything these touch is post-multiplied by `X`, so the basis stays inside `range(X)`,
# and two power iterations with re-orthonormalization pull it onto the dominant subspace
# wherever it started. Measured against the Eckart-Young optimum on a slowly decaying
# spectrum (60x40, singular values ∝ 1/k², r = 4, sketch width l = 14), as residual over
# optimum: unmutated 1.000000, `95:38` 1.000000, `84:16` 1.000000, `145:28` 1.000000.
# A ratio that moves by less than 1e-5 is below any tolerance defensible as chosen in
# advance. `85:40` is the same from the other side: the seed only picks the starting basis,
# so two seeds the mix collapses onto one stream return identical triplets anyway and no
# assertion on the output can see the collapse.
src/clustering/nmf.rs:84:16: replace + with * in randomized_svd
src/clustering/nmf.rs:85:40: replace ^ with & in randomized_svd
src/clustering/nmf.rs:85:40: replace ^ with | in randomized_svd
src/clustering/nmf.rs:95:38: replace * with / in randomized_svd
#
# Believed equivalent -- the numerical-rank cutoff separates zero from √ε·σ_max. A Gram
# eigenvalue that is noise comes out at ±ε·λ_max, so `λ.max(0).sqrt()` is either exactly
# zero (non-positive λ) or about 1.5e-8·σ_max. The true cutoff (σ_max·max(m,d)·ε ≈
# 8.8e-15·σ_max) and the mutated one (σ_max·ε/max(m,d) ≈ 5.5e-18·σ_max) both sit strictly
# between those, so both fire on exactly the same triplets.
src/clustering/nmf.rs:145:28: replace * with / in randomized_svd
#
# Believed equivalent -- `fold(zero, |a, b| a - b)` is `-Σ`, not an alternating sum, so it
# negates its whole result. For 96 and 107 the negated quantity is multiplied by `X` on
# the next line and the sign rides into `Q`; for 116 it negates `Q` directly. Either way
# `B = QᵀX` is negated too and `gram_rows(B) = BBᵀ` is not, so the eigenproblem is
# untouched, and the lift gives `u_k -> -u_k` with `v_k = Bᵀu_B/σ -> -v_k`. The returned
# `σ·u·vᵀ` -- the only thing a caller observes -- is invariant.
src/clustering/nmf.rs:96:47: replace + with - in randomized_svd
src/clustering/nmf.rs:107:51: replace + with - in randomized_svd
src/clustering/nmf.rs:116:51: replace + with - in randomized_svd
#
# Believed equivalent, conditionally. NNDSVDar's fill is proportional to the mean of `X`,
# so an all-zero input gives all-zero factors and a zero residual; the mutant then
# evaluates 0.0/0.0 = NaN, and `num_traits::Float::max` returns the *other* argument when
# one is NaN, so `.max(0)` yields 0 and the `sqrt` yields 0 -- bit-identical to the `else`
# branch. The condition is on that fill, which is why
# `a_projection_of_an_all_zero_matrix_reports_no_error_rather_than_infinity` pins it:
# give the fill a floor that does not vanish with the data and this stops being equivalent.
src/clustering/nmf.rs:714:40: replace > with >= in project

# ── src/mixture.rs (15) ──
# Debt: `Mixture::log_joint` term arithmetic.
src/mixture.rs:225:47: replace + with * in Mixture::vmf
src/mixture.rs:225:47: replace + with - in Mixture::vmf
src/mixture.rs:254:9: replace Mixture::n_components -> usize with 0
src/mixture.rs:254:9: replace Mixture::n_components -> usize with 1
src/mixture.rs:290:56: replace * with + in Mixture::log_joint
src/mixture.rs:290:56: replace * with / in Mixture::log_joint
src/mixture.rs:291:37: replace > with >= in Mixture::log_joint
src/mixture.rs:291:49: replace / with % in Mixture::log_joint
src/mixture.rs:291:49: replace / with * in Mixture::log_joint
src/mixture.rs:294:39: replace * with + in Mixture::log_joint
src/mixture.rs:294:45: replace * with + in Mixture::log_joint
src/mixture.rs:294:45: replace * with / in Mixture::log_joint
src/mixture.rs:340:22: replace - with + in Mixture::responsibilities
src/mixture.rs:358:14: replace > with >= in argmax
src/mixture.rs:56:64: replace * with + in StationaryCov<R>::innov

# ── src/sketch/ddsketch.rs (8) ──
# Debt: bucket-index arithmetic.
src/sketch/ddsketch.rs:151:45: replace > with >= in DdSketch::merge
src/sketch/ddsketch.rs:52:9: replace DdSketch::alpha -> f64 with -1.0
src/sketch/ddsketch.rs:52:9: replace DdSketch::alpha -> f64 with 0.0
src/sketch/ddsketch.rs:52:9: replace DdSketch::alpha -> f64 with 1.0
src/sketch/ddsketch.rs:67:27: replace > with == in DdSketch::collapse
src/sketch/ddsketch.rs:67:27: replace > with >= in DdSketch::collapse
src/sketch/ddsketch.rs:82:14: replace > with >= in DdSketch::update
src/sketch/ddsketch.rs:85:21: replace < with <= in DdSketch::update

# ── src/topology.rs (6) ──
# Re-measured 2026-08-26 after the `Link::Bhattacharyya` work renumbered the file: 110 mutants,
# 101 caught / 6 missed / 1 unviable / 2 timeouts, whole file at -j1 under a 10G cap
#   systemd-run --user --scope --unit=mut-topology -p MemoryMax=10G -p MemorySwapMax=0 \
#     -- timeout 10800 cargo mutants -f src/topology.rs -j1 --test-tool nextest --output /tmp/mut-topology
# The three `mapper` entries that stood here are now caught -- the bin-cover regression test added
# with the linkage work kills them. Both timeouts (`132:30: != -> ==` in UnionFind::find and
# `365:45: += -> *=` in find_bridges, each an infinite loop) are detections, not survivors, and are
# not listed.
#
# `329:25: > -> >=` in lens_values is *provably* equivalent, not merely believed so: the guard reads
# `if mean > 0.0 { 1.0 / mean } else { f64::INFINITY }`, `mean` is a mean of Euclidean distances and
# so cannot be negative, and at `mean == 0.0` the taken branch evaluates `1.0 / 0.0`, which is
# `f64::INFINITY` -- the same value the else branch returns.
#
# The other five are believed equivalent, and share one argument. Four of them mutate the elder
# rule's *tie-break*, which only runs when the two components have equal birth values -- and when
# `cmin[ra] == cmin[rb]`, both `points.push((cmin[young], val))` and
# `cmin[root] = cmin[elder].min(cmin[young])` are symmetric in the two, so whichever is called the
# younger the diagram is the same. The fifth drops the union-by-rank increment, which is a balancing
# heuristic: without it every union takes the Equal branch, so the tree is deeper, but `cmin[root]`
# is reassigned through `uf.find` immediately after each union and every later read goes through
# `find` too. Neither is observable in the output; both would need a test asserting on internal
# tree shape, which would pin an implementation detail rather than a property.
src/topology.rs:148:31: replace += with *= in UnionFind::union
src/topology.rs:215:46: replace > with >= in MapperGraph::persistence_diagram
src/topology.rs:215:88: replace > with < in MapperGraph::persistence_diagram
src/topology.rs:215:88: replace > with == in MapperGraph::persistence_diagram
src/topology.rs:215:88: replace > with >= in MapperGraph::persistence_diagram
src/topology.rs:329:25: replace > with >= in lens_values

# ── src/wasserstein.rs (7) ──
# First measurement of this file; the module postdates the whole-crate sweep. 233 mutants,
# 218 caught / 7 missed / 5 unviable / 3 timeouts, whole file at -j1 under an 8G cap:
#   systemd-run --user --scope --unit=mut-wass-full -p MemoryMax=8G -p MemorySwapMax=0 \
#     -- timeout 16200 cargo mutants -f src/wasserstein.rs -j1 --test-tool nextest \
#          --output /tmp/mut-wass-full
#
# Three earlier local runs over this file are VOID and none of their numbers is used here. One of
# them reported three mutants as missed whose logs showed the whole suite green; re-running those
# exact sites on that exact commit returned 7 of 7 caught, and the code and the test that kills them
# are byte-identical across the two commits, so the verdict could not legitimately have changed. What
# the void runs share is that the source tree was edited while they were in flight. The mechanism is
# not established and is not claimed here -- the irreproducibility is. Every one of the seven entries
# below was re-applied by hand after this run and confirmed to leave `cargo test --lib` green.
#
# All three timeouts are detections, not survivors, and are not listed. `266:15` and `268:15`
# (`+=` -> `*=` on the north-west corner's `i` and `j`) leave the index pinned, so the walk never
# reaches `i + 1 == na && j + 1 == nb`; `404:13` (`-=` -> `/=`) leaves `top` pinned, so the basis
# sweep never drains. Each is an infinite loop, which is what a mutation of a loop counter should be.
#
# Two are *provably* equivalent.
# `120:23`: with `+` the accumulator sums `V diag(√λ) Vᵀ`; with `-` it starts at zero and subtracts,
# so it yields exactly the negation, and `a_half` comes out as `-A^½`. Its only use is
# `M = (A^½ B) A^½`, which is quadratic in it, and IEEE-754 sign flips are exact -- so `M`, its
# eigenvalues and the returned trace are bit-identical.
# `440:20`: the backward walk needs one iteration per edge of the parent chain, and a spanning tree
# on `na + nb` nodes has a path of at most `na + nb - 1` edges. `na*nb >= na + nb - 1` for all
# `na, nb >= 1`, since that rearranges to `(na - 1)(nb - 1) >= 0`. The mutated bound is therefore
# never tighter than what the walk needs, and the walk leaves through its `node == start` break.
#
# `398:17` is equivalent for the reason the fixed-capacity stack exists: the guard is meant to be
# dead. Every push is gated on a `seen[..]` flag flipping false to true, so at most `na + nb` pushes
# happen in a call, the initial `stack[0]` among them, and every closure push is preceded in its own
# iteration by a pop -- so `top <= na + nb - 1` whenever the closure runs. `<` and `<=` differ only at
# `top == stack.len()`, which is unreachable, and where the mutation would index out of bounds rather
# than misbehave quietly.
#
# `178:9` is an optimization, not a behaviour: deleting the diagonal-by-diagonal arm sends the pair
# through the `_` arm, which densifies both spreads and takes the Bures route to the same value --
# an existing test asserts that the two paths agree to 1e-9. It is observable in time and allocation
# only. One earlier run detected it as a timeout; this one did not, so its detection is a property of
# the machine's load rather than of the suite, and it is recorded as an equivalent instead.
#
# The last two are believed equivalent, and both are ties.
# `265:48`: `j * 1 == nb` is never true inside the loop, so the mutation deletes the
# `|| j + 1 == nb` disjunct. That disjunct is only ever *taken* when `ra[i] > rb[j]` at the last
# column -- and there `rb[j]` is the whole remaining demand, which on balanced marginals equals the
# whole remaining supply and so is at least `ra[i]`. The other reachable shape, `i + 1 == na` with
# `j + 1 == nb`, has already left through the break above. Dead on any instance the solver is given.
# `293:31`: `<` keeps the earliest cell of a tie on the exit, `<=` the latest. Both are feasible
# leaving cells and the step size `theta` is the same either way, so the optimum does not move; what
# changes is the pivot sequence. Strict `<` is Bland's anti-cycling half, and the iteration cap is
# what actually backstops termination, so separating the two would need a degenerate instance that
# cycles under last-of-tie -- which the 48-instance oracle sweep, half of it integer-cost and so
# deliberately tie-rich, does not produce.
#
# `280:70` differs from `< -SIMPLEX_TOL` only when a reduced cost lands on `-SIMPLEX_TOL` to the ulp.
# The tolerance exists to refuse numerically-zero reduced costs; hitting it exactly is not
# constructible through any public path.
src/wasserstein.rs:120:23: replace + with - in bures_cross
src/wasserstein.rs:178:9: delete match arm (Spread::Diagonal(u), Spread::Diagonal(v)) in gaussian_w2_sq
src/wasserstein.rs:265:48: replace + with * in transport
src/wasserstein.rs:280:70: replace < with <= in transport
src/wasserstein.rs:293:31: replace < with <= in transport
src/wasserstein.rs:398:17: replace < with <= in tree_path
src/wasserstein.rs:440:20: replace + with * in tree_path

# ── src/clustering/community.rs (7) ──
# Debt: remainder after the 2026-08-19 reference tests.
src/clustering/community.rs:104:32: replace > with >= in leiden
src/clustering/community.rs:106:25: replace > with >= in leiden
src/clustering/community.rs:172:15: replace += with *= in detect
src/clustering/community.rs:258:29: replace -= with += in refine
src/clustering/community.rs:258:29: replace -= with /= in refine
src/clustering/community.rs:259:30: replace -= with += in refine
src/clustering/community.rs:259:30: replace -= with /= in refine

# ── src/linalg.rs (4) ──
# Debt.
src/linalg.rs:109:25: replace + with * in jacobi_eigen
src/linalg.rs:110:27: replace + with - in jacobi_eigen
src/linalg.rs:110:37: replace * with / in jacobi_eigen
src/linalg.rs:63:21: replace + with * in solve_upper_t

# ── src/sparse.rs (4) ──
# Debt.
src/sparse.rs:110:18: replace < with <= in summarize_sparse
src/sparse.rs:138:35: replace + with * in nearest_sparse
src/sparse.rs:138:35: replace + with - in nearest_sparse
src/sparse.rs:179:64: delete - in validate_csr

# ── src/clustering/graph.rs (3) ──
# Debt.
src/clustering/graph.rs:104:30: replace > with >= in log_covariances
src/clustering/graph.rs:125:21: replace + with * in knn_affinity_impl
src/clustering/graph.rs:54:23: replace + with - in grassmann_sq

# ── src/clustering/spectral.rs (2) ──
# Debt.
src/clustering/spectral.rs:104:46: replace * with / in spectral_core
src/clustering/spectral.rs:48:10: replace > with >= in spectral

# ── src/clustering/ward.rs (2) ──
# Debt.
src/clustering/ward.rs:167:22: replace > with >= in ward_hac_auto
src/clustering/ward.rs:49:26: replace < with <= in dendrogram

# ── src/clustering/medoid.rs (2) ──
# First measurement of this file. It has no entry from the whole-crate sweep because the module
# postdates it: 161 mutants, 157 caught / 2 missed / 2 unviable / 0 timeouts, whole file at -j1 under
# a 10G cap. The count started at 26 survivors and ten tests closed the gap before this run --
# `Leaves::of` on a massless leaf was not a test hole but a real NaN (`S/n` at `0/0`) that reached
# `partial_cmp(..).unwrap()`, fixed at the producer in 9755163.
#
# Believed equivalent -- both are ties that change nothing observable.
# `168:18`: `if d <= d2[i] { d2[i] = d }` stores the value `d2[i]` already holds when the two are
# equal, so the seeding potential, the draw and the medoid set are identical.
# `232:21`: `loss <= current - 1e-12` differs from `<` only when `loss` is *exactly*
# `current - 1e-12` in f64. The threshold exists to reject ties, and a swap that lands on it to the
# ulp is not constructible through the public path.
src/clustering/medoid.rs:168:18: replace < with <= in seed_medoids
src/clustering/medoid.rs:232:21: replace < with <= in best_swap

# ── src/order.rs (6) ──
# First measurement of this file; the module postdates the whole-crate sweep. 143 mutants,
# 130 caught / 6 missed / 3 unviable / 4 timeouts, whole file in 2h at -j4 under a 12G cap, at
# 68580c5. Line numbers were shifted +5 by hand at `bdff1de`, which added five lines to the module
# doc and no code -- every construct was re-checked against the new line, and the columns did not
# move. The run itself was at:
#   systemd-run --user --scope -p MemoryMax=12G -p MemorySwapMax=0 \
#     -- env TMPDIR=/home/ilgrad/.cache/mutants-tmp \
#          cargo mutants -f src/order.rs -j4 --test-tool nextest
# `TMPDIR` has to point outside both the repo and `/tmp`: the repo makes the worker's copy recursive,
# and `/tmp` is tmpfs here, whose pages count against the cgroup's `MemoryMax`.
#
# All four timeouts are detections, not survivors, and are not listed: `190:27`, `195:27`, `200:27`
# and `205:27` turn a merge-walk cursor's `p += 1` into `p *= 1`, which pins it at zero, so the walk
# never reaches the end of either column list. An infinite loop is what a mutation of a loop counter
# should be.
#
# Two are *provably* equivalent.
# `188:47`: the preceding match arm is guarded on `i == j`, so this arm is only reached with
# `i != j`, where `i <= j` and `i < j` are the same predicate.
# `287:26`: `hi > lo` and `hi >= lo` differ only at `hi == lo` -- `hi < lo` requires `n == 0`, and
# then the loop the span feeds does not run. At `hi == lo` the mutant takes `span = 0.0`, so
# `(v - lo) / span` is `0.0 / 0.0 = NaN`; `NaN.round()` is NaN, `f64::clamp` propagates NaN, and a
# saturating `NaN as u64` is 0 -- exactly the `q` the clean branch computes from
# `(v - lo) / 1.0 * levels == 0.0`. The constant projection contributes no bits either way.
#
# `109:15` is believed equivalent. `n <= 1 || dim == 0` -> `&&` differs in two shapes. With `n <= 1`
# and `dim > 0` the mutant reaches a sort of at most one element, which is the identity whatever the
# comparator does. With `n > 1` and `dim == 0` every row slice is empty, so every projection dots to
# zero, `quantise` sees a constant projection and emits code 0 for every row, and the tie-break
# compares two empty slices and returns `Equal` -- the sort runs on an all-equal key. Swept
# `n = 0..600` at `dim = 0` against the mutated build: the identity at every `n`, so no fixture in
# that family separates it.
#
# The last three are believed equivalent for the reason [`projections`] already documents: `scale` is
# a positive constant common to every entry of the matrix, and [`quantise`] ranges each projection
# over its own observed span, so a positive rescaling cancels before any bit is emitted.
# `259:21` (`/` -> `*`) and `263:57` (`*` -> `/`) both rescale by `sqrt(dim)` rather than its
# reciprocal. What survives the argument is float rounding -- the ranged value differs in the last
# bits and could in principle cross a level boundary -- which is not constructible on demand.
# `259:21` (`/` -> `%`) needed measuring rather than arguing. For `dim >= 2`, `sqrt(dim) > 1` and
# `1.0 % sqrt(dim) == 1.0`, which is the positive constant above. At `dim == 1` it is
# `1.0 % 1.0 == 0.0`, which zeroes the whole matrix: every code is then 0 and the order falls
# entirely to the value tie-break, i.e. ascending in the single column. Measured on 12 distinct
# values at `dim = 1`, the clean key produces that same ascending order, so the two coincide. That
# coincidence is a property of `PROJECTION_SEED` -- the sign of the projection carrying the leading
# bit -- and not a law; `the_key_is_pinned_so_a_change_to_it_cannot_pass_unnoticed` is what goes red
# if the seed moves.
src/order.rs:109:15: replace || with && in canonical_permutation
src/order.rs:188:47: replace < with <= in canonical_permutation_csr
src/order.rs:259:21: replace / with % in projections
src/order.rs:259:21: replace / with * in projections
src/order.rs:263:57: replace * with / in projections
src/order.rs:287:26: replace > with >= in quantise
