Skip to content

fix(ruvector-mincut): deterministic partition() + witness complement fix - #1003

Draft
ruvnet wants to merge 3 commits into
mainfrom
claude/focused-darwin-9rtmij
Draft

ruvnet wants to merge 3 commits into
mainfrom
claude/focused-darwin-9rtmij

Conversation

@ruvnet

@ruvnet ruvnet commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Hypothesis

The 2026-09-05 nightly run (ADR-345, mincut-gated forgetting) measured RuVectorGraphAnalyzer::partition() returning different, sometimes empty, results across repeated calls on byte-identical graphs, and filed two open "Next Research" items: find the root cause, and check whether the crate's lower-level API avoids it. This PR is that follow-up: given the same graph, when the two root causes are fixed, repeated partition() calls should return byte-identical, correct results — subject to zero regressions in the existing test suite and the original benchmark's verdict changing only if the bugs (not the algorithm's fitness for that task) explain its prior REJECT.

Root cause (two independent bugs)

  1. BoundedInstance::brute_force_min_cut/search_for_cuts (crates/ruvector-mincut/src/instance/bounded.rs) built their vertex/seed enumeration order from HashSet<VertexId> iteration. Rust's default hasher reseeds on every HashSet/instance construction, so first-match tie-breaking among equal-cost minimum cuts was a function of construction-time entropy, not the graph. Fixed with sort_unstable() on both vectors.
  2. WitnessHandle::materialize_partition() (crates/ruvector-mincut/src/instance/witness.rs) computed the cut's second side as 0..=max(membership) minus membership — silently wrong (often empty) whenever the winning side didn't contain the graph's actual highest-ID vertex. Fixed at the one production call site, RuVectorGraphAnalyzer::partition() (crates/ruvector-mincut/src/integration/mod.rs), by computing the complement against the analyzer's real graph vertex set.

crates/ruvector-agent-memory/src/graph_forget.rs's MincutGatedForgetting no longer needs its mincut_trials majority-vote workaround now that the underlying call is deterministic; its default drops 3 → 1 (unit tests: 10 → 1).

Architecture

See the mermaid diagram in the nightly report: docs/research/nightly/2026-09-21-mincut-determinism-fix/README.md.

Files changed

  • crates/ruvector-mincut/src/instance/bounded.rs — determinism fix (bug 1)
  • crates/ruvector-mincut/src/integration/mod.rs — witness complement fix (bug 2)
  • crates/ruvector-agent-memory/src/graph_forget.rs — drop the now-unnecessary retry workaround, doc updates
  • docs/adr/ADR-346-mincut-witness-partition-determinism.md (new)
  • docs/adr/INDEX.md — index entry for ADR-346
  • docs/research/nightly/2026-09-21-mincut-determinism-fix/README.md, gist.md (new)

Benchmark command

# Determinism probe (200 trials on a fixed 19-vertex graph)
cargo run --release -p ruvector-agent-memory --example mincut_determinism_probe --features mincut-forget

# Original 2026-09-05 acceptance benchmark, run unmodified
cargo run --release -p ruvector-agent-memory --example mincut_gated_forgetting_bench --features mincut-forget

Real benchmark results

Three stages, each re-running both commands unmodified:

Stage Determinism probe (200 trials) 84-vertex bench verdict
Baseline (unmodified main) 88/200 (44%) empty/wrong REJECT
Bug 1 fixed only 200/200 (100%) empty — now consistently wrong REJECT
Both bugs fixed 0/200 (0%) empty, 200/200 (100%) correct REJECT (unchanged, expected)

The middle row is the key intermediate result: fixing only the tie-breaking bug made the outcome fully deterministic and fully wrong (it reliably converges on the tie-broken cut that triggers the complement bug), which is strong independent confirmation both bugs are real and both needed fixing.

Latency is flat (~1.0–1.06s/call) across all three stages — both are correctness bugs, not performance bugs; the crate's separately-documented latency cost is untouched and remains open (see Next Research in the report).

cargo test --release -p ruvector-mincut --lib: 512 passed, 0 failed, 5 ignored (unchanged count from baseline). cargo test --release -p ruvector-agent-memory --features mincut-forget --lib: 31 passed, 0 failed. cargo clippy --release -- -D warnings clean on both crates. 30/30 repeated runs of the mincut_trials = 1 bridge-detection tests, no flakes.

Acceptance result

ACCEPT for the determinism/correctness hypothesis (0% empty, 100% correct, reproducible across repeated runs). The original mincut-gated-forgetting production hypothesis (ADR-345) remains REJECT, unchanged and expected — its rejection was independently attributed to the global min-cut not isolating human-perceived "bridge" memories on this benchmark's synthetic data, orthogonal to both bugs fixed here.

Darwin result

Not run. This is a targeted, root-caused bug fix with one clear correct implementation (sort before first-match; compute the complement against the real vertex set) — no meaningful parameter space to evolve. See the nightly report's "Evolution Results" section for the full reasoning.

Flywheel result

Recorded in docs/research/nightly/2026-09-21-mincut-determinism-fix/README.md: root cause, fix, before/after evidence, and three explicit future-research items (a WitnessHandle constructor-level fix, a latency investigation via DynamicMinCut, and a broader audit of fragment//expander//jtree/ for the same bug class) are filed so a future session doesn't have to rediscover them.

Security review

No new attack surface, no new dependencies, no new unsafe code, no parsing of untrusted input. Determinism is a strict improvement for any downstream witness-verification or replay consumer.

Main limitations

  • Only the call path reachable from RuVectorGraphAnalyzer::partition()'s one current production caller was fixed and verified; WitnessHandle::materialize_partition() itself is still wrong for any future direct caller until a deeper constructor-level fix lands (filed as Open Question 1 in ADR-346).
  • Other HashMap/HashSet-heavy modules in the crate (fragment/, expander/, jtree/) were not audited for the same bug class.
  • The crate's separately-identified per-call latency is unaffected by this fix.

Production recommendation

Land as-is. Both fixes are minimal, change no public API signature, are covered by the existing test suite plus this PR's three-stage before/after benchmark, and remove a correctness confound from any current or future consumer of RuVectorGraphAnalyzer/BoundedInstance/WitnessHandle.

Research document / ADR / gist

  • Research report: docs/research/nightly/2026-09-21-mincut-determinism-fix/README.md
  • ADR: docs/adr/ADR-346-mincut-witness-partition-determinism.md
  • Gist: docs/research/nightly/2026-09-21-mincut-determinism-fix/gist.md

🤖 Generated with claude-flow

https://claude.ai/code/session_01MzSGbyatr2eZwuM4JVeP5g


Generated by Claude Code

claude and others added 3 commits September 21, 2026 07:49
…witness partition complement

RuVectorGraphAnalyzer::partition() returned different, sometimes empty,
results across repeated calls on byte-identical graphs (ADR-345,
2026-09-05 nightly finding). Root-caused to two independent bugs:

- BoundedInstance::brute_force_min_cut/search_for_cuts built their
  vertex/seed enumeration order from HashSet<VertexId> iteration, which
  Rust's default hasher reseeds on every instance construction, so
  first-match tie-breaking among equal-cost cuts was nondeterministic.
  Fixed by sort_unstable() on both vectors before use.
- WitnessHandle::materialize_partition() computed the cut's second side
  as 0..=max(membership) minus membership instead of using the real
  graph vertex set, silently returning an empty complement whenever the
  winning side excluded the graph's highest-ID vertex. Fixed at the one
  production call site (RuVectorGraphAnalyzer::partition()) by computing
  the complement against self.graph.vertices() instead.

200-trial determinism probe: 44% empty/wrong before, 0% after (100% after
fixing only the first bug, confirming both bugs are independent and both
needed fixing). 512/512 existing tests unchanged; clippy clean.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01MzSGbyatr2eZwuM4JVeP5g
Now that ruvector-mincut's RuVectorGraphAnalyzer::partition() is
deterministic (previous commit), MincutGatedForgetting no longer needs
to repeat and union boundary detection across multiple trials to work
around nondeterministic tie-breaking. mincut_trials defaults 3 -> 1 in
soft()/hard(); the two unit tests' mincut_trials = 10 workaround is
removed. Verified stable across 30 repeated runs of the compiled test
binary. ruvector-agent-memory lib suite: 31/31 passed in 1.05s, down
from ~10.5s at mincut_trials = 10.

Doc comments updated to record the root cause and fix, preserving the
original 2026-09-05 "Measured limitation" finding rather than deleting
it. The underlying mincut-gated-forgetting hypothesis remains rejected
at production scale (ADR-345's own, unrelated finding), unchanged by
this fix.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01MzSGbyatr2eZwuM4JVeP5g
…sm fix

Documents the 2026-09-21 nightly run that answers ADR-345's two filed
"Next Research" open questions about ruvector-mincut's non-determinism:
root cause, fix, and a three-stage before/after re-run of the unmodified
2026-09-05 benchmark (raw output preserved). Records that the
mincut-gated-forgetting REJECT verdict is unchanged by the fix, and
files the remaining open items (WitnessHandle constructor-level fix,
latency investigation via DynamicMinCut, broader HashMap/HashSet audit)
as explicit future work.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01MzSGbyatr2eZwuM4JVeP5g

This branch has not been deployed

No deployments
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.

2 participants