# Quick-Start Implementation Guide

This guide provides copy-paste-ready implementations for the top 5 mathematical improvements.

## Prerequisites

Add to `requirements.txt`:
```
numpy>=1.24.0
torch>=2.0.0  # For RL/MCTS (optional, can use pure Python)
```

---

## 1. A* Path Finding (Easiest - 3-5 days)

### Step 1: Create new file `src/venomqa/v1/core/path_finder.py`

```python
"""A* search for efficient path finding in state graphs."""

from __future__ import annotations

import heapq
from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from venomqa.v1.core.graph import Graph
    from venomqa.v1.core.state import State
    from venomqa.v1.core.hypergraph import Hypergraph


@dataclass
class SearchNode:
    """Node in A* search."""

    state_id: str
    g_score: float
    f_score: float
    parent: SearchNode | None = None
    transition: any = None  # Transition that led here


class StateDistanceHeuristic:
    """Admissible heuristics for A* state space search."""

    def __init__(self, graph: Graph):
        self.graph = graph

    def hyperedge_distance(
        self,
        state: State,
        goal_state: State,
        hypergraph: Hypergraph | None = None,
    ) -> float:
        """Hyperedge Hamming distance heuristic (admissible)."""
        if hypergraph is None:
            return 0.0

        from_state_edge = hypergraph.get_hyperedge(state.id)
        to_state_edge = hypergraph.get_hyperedge(goal_state.id)

        if from_state_edge is None or to_state_edge is None:
            return float('inf')

        return float(from_state_edge.hamming_distance(to_state_edge))


class AStarPathFinder:
    """A* search for shortest path in state graph.

    Features:
    - Heuristic-guided search (faster than BFS)
    - Path caching for repeated queries
    - Admissible guarantees (finds shortest path)
    """

    def __init__(
        self,
        graph: Graph,
        heuristic: StateDistanceHeuristic,
        hypergraph: Hypergraph | None = None,
    ):
        self.graph = graph
        self.heuristic = heuristic
        self.hypergraph = hypergraph

        # Path cache: (from_id, to_id) -> list[Transition]
        self._path_cache: dict[tuple[str, str], list] = {}

    def find_path(
        self,
        from_state_id: str,
        to_state_id: str,
    ) -> list | None:
        """Find shortest path using A* search.

        Args:
            from_state_id: Starting state ID
            to_state_id: Target state ID

        Returns:
            List of transitions forming path, or None if unreachable
        """
        # Check cache
        cache_key = (from_state_id, to_state_id)
        if cache_key in self._path_cache:
            return self._path_cache[cache_key][:]

        # Validate states exist
        from_state = self.graph.get_state(from_state_id)
        to_state = self.graph.get_state(to_state_id)

        if from_state is None or to_state is None:
            return None

        if from_state_id == to_state_id:
            return []

        # Priority queue: (f_score, counter, state_id)
        open_heap: list[tuple[float, int, str]] = []
        counter = 0

        # Open and closed sets
        open_nodes: dict[str, SearchNode] = {}
        closed_set: set[str] = set()

        # Initialize start node
        start_h = 0.0
        if self.hypergraph:
            start_h = self.heuristic.hyperedge_distance(
                from_state,
                to_state,
                self.hypergraph,
            )

        start_node = SearchNode(
            state_id=from_state_id,
            g_score=0.0,
            f_score=start_h,
            parent=None,
        )

        open_nodes[from_state_id] = start_node
        heapq.heappush(open_heap, (start_h, counter, from_state_id))
        counter += 1

        # A* main loop
        while open_heap:
            current_f, _, current_id = heapq.heappop(open_heap)

            # Skip if already processed
            if current_id in closed_set:
                continue

            # Check if reached goal
            if current_id == to_state_id:
                path = self._reconstruct_path(current_id, open_nodes)
                self._path_cache[cache_key] = path
                return path

            # Mark as closed
            closed_set.add(current_id)
            current_node = open_nodes[current_id]

            # Expand neighbors
            current_state = self.graph.get_state(current_id)
            if current_state is None:
                continue

            # Find outgoing transitions
            for transition in self.graph.transitions:
                if transition.from_state_id != current_id:
                    continue

                neighbor_id = transition.to_state_id

                # Skip closed nodes
                if neighbor_id in closed_set:
                    continue

                # Compute tentative g score
                tentative_g = current_node.g_score + 1.0

                # Check if better path found
                neighbor_node = open_nodes.get(neighbor_id)

                if neighbor_node is None:
                    # New node: create and add to open set
                    neighbor_state = self.graph.get_state(neighbor_id)
                    h = 0.0
                    if self.hypergraph and neighbor_state:
                        h = self.heuristic.hyperedge_distance(
                            neighbor_state,
                            to_state,
                            self.hypergraph,
                        )

                    neighbor_node = SearchNode(
                        state_id=neighbor_id,
                        g_score=tentative_g,
                        f_score=tentative_g + h,
                        parent=current_node,
                        transition=transition,
                    )

                    open_nodes[neighbor_id] = neighbor_node
                    heapq.heappush(
                        open_heap,
                        (neighbor_node.f_score, counter, neighbor_id),
                    )
                    counter += 1

                elif tentative_g < neighbor_node.g_score:
                    # Better path to existing node: update
                    neighbor_node.g_score = tentative_g
                    neighbor_node.f_score = tentative_g + (
                        neighbor_node.f_score - neighbor_node.g_score
                    )  # Keep h, update g
                    neighbor_node.parent = current_node
                    neighbor_node.transition = transition

                    # Re-add to heap
                    heapq.heappush(
                        open_heap,
                        (neighbor_node.f_score, counter, neighbor_id),
                    )
                    counter += 1

        # No path found
        return None

    def _reconstruct_path(
        self,
        goal_id: str,
        nodes: dict[str, SearchNode],
    ) -> list:
        """Reconstruct path from goal to start using parent pointers."""
        path = []
        current = nodes.get(goal_id)

        while current and current.parent:
            if current.transition:
                path.insert(0, current.transition)
            current = current.parent

        return path

    def clear_cache(self) -> None:
        """Clear path cache."""
        self._path_cache.clear()


class WeightedAStarPathFinder(AStarPathFinder):
    """Weighted A* with weight parameter for speed/optimality tradeoff.

    Higher weight = faster search, potentially suboptimal paths.
    Typical weight values:
    - 1.0 = Standard A* (optimal, slower)
    - 1.5-2.0 = Balanced (slightly suboptimal, faster)
    - 3.0-5.0 = Fast (may miss shortest path)
    """

    def __init__(
        self,
        graph: Graph,
        heuristic: StateDistanceHeuristic,
        hypergraph: Hypergraph | None = None,
        weight: float = 1.5,
    ):
        super().__init__(graph, heuristic, hypergraph)
        self.weight = weight

    def find_path(
        self,
        from_state_id: str,
        to_state_id: str,
    ) -> list | None:
        """Weighted A* search."""
        from_state = self.graph.get_state(from_state_id)
        to_state = self.graph.get_state(to_state_id)

        if from_state is None or to_state is None:
            return None

        if from_state_id == to_state_id:
            return []

        # Priority queue
        open_heap: list[tuple[float, int, str]] = []
        counter = 0
        open_nodes: dict[str, SearchNode] = {}
        closed_set: set[str] = set()

        # Initialize with weighted heuristic
        start_h = 0.0
        if self.hypergraph:
            start_h = self.heuristic.hyperedge_distance(
                from_state,
                to_state,
                self.hypergraph,
            )

        start_node = SearchNode(
            state_id=from_state_id,
            g_score=0.0,
            f_score=self.weight * start_h,
            parent=None,
        )

        open_nodes[from_state_id] = start_node
        heapq.heappush(open_heap, (start_node.f_score, counter, from_state_id))
        counter += 1

        while open_heap:
            current_f, _, current_id = heapq.heappop(open_heap)

            if current_id in closed_set:
                continue

            if current_id == to_state_id:
                path = self._reconstruct_path(current_id, open_nodes)
                return path

            closed_set.add(current_id)
            current_node = open_nodes[current_id]

            current_state = self.graph.get_state(current_id)
            if current_state is None:
                continue

            for transition in self.graph.transitions:
                if transition.from_state_id != current_id:
                    continue

                neighbor_id = transition.to_state_id
                if neighbor_id in closed_set:
                    continue

                tentative_g = current_node.g_score + 1.0
                neighbor_node = open_nodes.get(neighbor_id)

                if neighbor_node is None:
                    neighbor_state = self.graph.get_state(neighbor_id)
                    h = 0.0
                    if self.hypergraph and neighbor_state:
                        h = self.heuristic.hyperedge_distance(
                            neighbor_state,
                            to_state,
                            self.hypergraph,
                        )

                    neighbor_node = SearchNode(
                        state_id=neighbor_id,
                        g_score=tentative_g,
                        f_score=tentative_g + self.weight * h,
                        parent=current_node,
                        transition=transition,
                    )

                    open_nodes[neighbor_id] = neighbor_node
                    heapq.heappush(
                        open_heap,
                        (neighbor_node.f_score, counter, neighbor_id),
                    )
                    counter += 1

                elif tentative_g < neighbor_node.g_score:
                    neighbor_node.g_score = tentative_g
                    neighbor_node.f_score = tentative_g + self.weight * (
                        neighbor_node.f_score - neighbor_node.g_score
                    )
                    neighbor_node.parent = current_node
                    neighbor_node.transition = transition

                    heapq.heappush(
                        open_heap,
                        (neighbor_node.f_score, counter, neighbor_id),
                    )
                    counter += 1

        return None
```

### Step 2: Modify `src/venomqa/v1/core/graph.py`

Add to `Graph.__init__`:

```python
def __init__(self, actions: list[Action] | None = None) -> None:
    # ... existing code ...
    self._path_finder: AStarPathFinder | None = None
    self._hypergraph: any = None
```

Add new method:

```python
def set_hypergraph(self, hypergraph: any) -> None:
    """Set hypergraph for A* heuristics."""
    from venomqa.v1.core.path_finder import AStarPathFinder, StateDistanceHeuristic

    self._hypergraph = hypergraph
    heuristic = StateDistanceHeuristic(self)
    self._path_finder = AStarPathFinder(self, heuristic, hypergraph)
```

Modify `get_path_to`:

```python
def get_path_to(self, state_id: str, use_astar: bool = True) -> list:
    """Get path from initial state to given state.

    Args:
        state_id: Target state ID
        use_astar: If True, use A*; otherwise fall back to BFS

    Returns:
        List of transitions forming path
    """
    if use_astar and self._path_finder is not None and self.initial_state_id:
        path = self._path_finder.find_path(self.initial_state_id, state_id)
        return path if path is not None else []
    else:
        return self._bfs_get_path_to(state_id)
```

---

## 2. LSH State Cache (Easy - 3-5 days)

### Step 1: Create `src/venomqa/v1/core/lsh_cache.py`

```python
"""Locality-Sensitive Hashing for fast state deduplication."""

from __future__ import annotations

import hashlib
import json
import random
from collections import defaultdict
from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from venomqa.v1.core.state import State


@dataclass
class LSHConfig:
    """LSH hyperparameters for tuning precision/speed tradeoff."""

    num_bands: int = 20           # More bands = higher precision, slower
    rows_per_band: int = 5         # More rows = higher recall, slower
    seed: int = 42                # Random seed for reproducibility

    @property
    def total_hash_funcs(self) -> int:
        return self.num_bands * self.rows_per_band


class MinHashLSH:
    """MinHash-based LSH for approximate state similarity detection.

    Features:
    - Sub-linear time duplicate detection (vs O(1) exact hash)
    - Configurable false positive rate via num_bands/rows_per_band
    - Jaccard similarity preserved (similar states collide more often)

    Mathematical foundation:
    - P[collision] = J(A,B) where J = |A∩B|/|A∪B| (Jaccard)
    - FPR ≈ (1 - (1 - 1/b)^k)^b where k=rows_per_band, b=num_bands
    """

    def __init__(self, config: LSHConfig | None = None):
        self.config = config or LSHConfig()
        self.total_hash_funcs = self.config.total_hash_funcs

        # Generate random hash functions using different seeds
        self.hash_seeds = [
            random.Random(i).randint(0, 2**32 - 1)
            for i in range(self.total_hash_funcs)
        ]

        # Bands: (band_idx) → (band_signature) → [state_ids]
        self.bands: list[dict[int, list[str]]] = [
            defaultdict(list) for _ in range(self.config.num_bands)
        ]

        # Exact hash storage for verification (SHA-256)
        self.exact_hashes: dict[str, str] = {}  # state_id → sha256

    def _hash_int(self, x: int, seed: int) -> int:
        """Simple hash function."""
        # Multiplicative hash
        a = 2654435761
        b = 1640531527
        h = (seed + x * a + b) % (2**32)
        return h

    def _minhash_signature(self, tokens: set[str]) -> list[int]:
        """Compute MinHash signature vector.

        For each hash function, find minimum hash value.
        This preserves Jaccard similarity.
        """
        signature = []

        for seed in self.hash_seeds:
            min_hash = 2**32 - 1
            for token in tokens:
                h = self._hash_int(hash(token), seed)
                if h < min_hash:
                    min_hash = h
            signature.append(min_hash)

        return signature

    def _tokenize(self, state_dict: dict) -> set[str]:
        """Convert state dict to token set for MinHash."""
        tokens = set()

        def traverse(obj, prefix=""):
            if isinstance(obj, dict):
                for key, value in obj.items():
                    traverse(value, f"{prefix}.{key}" if prefix else key)
            elif isinstance(obj, (list, tuple)):
                for i, value in enumerate(obj):
                    traverse(value, f"{prefix}[{i}]")
            else:
                # Leaf node: add to tokens
                tokens.add(f"{prefix}:{obj}")

        traverse(state_dict)
        return tokens

    def _compute_band_signature(
        self,
        signature: list[int],
        band_idx: int,
    ) -> int:
        """Compute signature for a specific band using hashing."""
        start = band_idx * self.config.rows_per_band
        end = start + self.config.rows_per_band
        band_values = signature[start:end]

        # Hash band values to integer signature
        band_str = "|".join(map(str, band_values))
        band_sig = self._hash_int(hash(band_str), 0xDEADBEEF)

        return band_sig

    def add(self, state_id: str, state_repr: dict) -> None:
        """Add state to LSH index.

        Args:
            state_id: Unique identifier for the state
            state_repr: Dictionary representation of state observations
        """
        # Tokenize state
        tokens = self._tokenize(state_repr)

        # Compute MinHash signature
        signature = self._minhash_signature(tokens)

        # Insert into each band
        for band_idx in range(self.config.num_bands):
            band_sig = self._compute_band_signature(signature, band_idx)
            self.bands[band_idx][band_sig].append(state_id)

        # Store exact hash for verification
        exact_hash = self._compute_exact_hash(state_repr)
        self.exact_hashes[state_id] = exact_hash

    def query(self, state_repr: dict) -> list[str]:
        """Find potentially similar states.

        Returns list of candidate state IDs that may be similar.
        Requires exact verification with check_duplicate().
        """
        tokens = self._tokenize(state_repr)
        signature = self._minhash_signature(tokens)

        # Query each band and collect candidates
        candidates: set[str] = set()
        for band_idx in range(self.config.num_bands):
            band_sig = self._compute_band_signature(signature, band_idx)
            candidates.update(self.bands[band_idx].get(band_sig, []))

        return list(candidates)

    def _compute_exact_hash(self, state_repr: dict) -> str:
        """Compute exact SHA-256 hash (for verification)."""
        content = json.dumps(state_repr, sort_keys=True, default=str)
        full_hash = hashlib.sha256(content.encode()).hexdigest()
        return full_hash[:12]  # Same length as State.id

    def check_duplicate(
        self,
        state_id: str,
        state_repr: dict,
    ) -> bool:
        """
        Check if state is a duplicate using LSH + exact verification.

        Returns True if duplicate exists, False otherwise.

        This is O(1) for the LSH check, O(k) for exact verification
        where k is the number of candidates (typically very small).
        """
        # Fast LSH check
        candidates = self.query(state_repr)

        if not candidates:
            return False  # No similar states found

        # Exact verification of candidates
        exact_hash = self._compute_exact_hash(state_repr)
        for candidate_id in candidates:
            if self.exact_hashes.get(candidate_id) == exact_hash:
                return True  # Exact duplicate

        return False  # No exact duplicate found

    def get_duplicate_id(
        self,
        state_id: str,
        state_repr: dict,
    ) -> str | None:
        """Get the ID of the duplicate state, if exists."""
        candidates = self.query(state_repr)
        if not candidates:
            return None

        exact_hash = self._compute_exact_hash(state_repr)
        for candidate_id in candidates:
            if self.exact_hashes.get(candidate_id) == exact_hash:
                return candidate_id

        return None


class LSHStateCache:
    """Wrapper combining LSH with exact hash storage for fast deduplication.

    Provides O(1) average-case duplicate checking using LSH,
    with O(k) exact verification where k is small (typically < 5).
    """

    def __init__(self, config: LSHConfig | None = None):
        self.lsh = MinHashLSH(config)
        self.exact_states: dict[str, State] = {}  # exact_hash → State
        self.state_id_to_hash: dict[str, str] = {}  # state_id → exact_hash

    def add_state(self, state: State) -> State:
        """Add state, returning canonical state (deduplicated).

        If state already exists (same observations), returns existing state.
        Otherwise, adds new state and returns it.

        This is the primary method for state deduplication.
        """
        state_repr = self._state_to_dict(state)

        # Fast LSH check
        duplicate_id = self.lsh.get_duplicate_id(state.id, state_repr)

        if duplicate_id is not None:
            # Return existing state
            exact_hash = self.state_id_to_hash.get(duplicate_id)
            if exact_hash:
                return self.exact_states[exact_hash]

        # New state: add to LSH and exact storage
        self.lsh.add(state.id, state_repr)
        exact_hash = self.lsh._compute_exact_hash(state_repr)

        self.exact_states[exact_hash] = state
        self.state_id_to_hash[state.id] = exact_hash

        return state

    def _state_to_dict(self, state: State) -> dict:
        """Convert State to dict for hashing."""
        return {
            sys_name: obs.data
            for sys_name, obs in state.observations.items()
        }

    def contains(self, state: State) -> bool:
        """Check if state already exists (without adding)."""
        state_repr = self._state_to_dict(state)
        return self.lsh.check_duplicate(state.id, state_repr)

    def clear(self) -> None:
        """Clear all stored states."""
        self.lsh.bands = [defaultdict(list) for _ in self.lsh.bands]
        self.lsh.exact_hashes.clear()
        self.exact_states.clear()
        self.state_id_to_hash.clear()

    @property
    def size(self) -> int:
        """Number of unique states stored."""
        return len(self.exact_states)
```

### Step 2: Modify `src/venomqa/v1/agent/__init__.py`

In `Agent.__init__`, add:

```python
from venomqa.v1.core.lsh_cache import LSHCache, LSHConfig

# ... in __init__ ...
self.world = world
self.graph = Graph(actions)
self.invariants = invariants or []
self.strategy = strategy or BFS()
self.max_steps = max_steps
self.coverage_target = coverage_target
self.progress_every = progress_every
self._violations: list[Violation] = []
self._step_count = 0

# Add LSH cache
self._lsh_cache = LSHCache(LSHConfig())
```

In `Agent.explore()`, modify state adding:

```python
# After: initial_state = self.world.observe_and_checkpoint("initial")
initial_state = self.world.observe_and_checkpoint("initial")

# Use LSH for deduplication
initial_state = self._lsh_cache.add_state(initial_state)
initial_state = self.graph.add_state(initial_state)  # Still add to graph

# ... in _step() ...
# After: to_state = self.world.observe_and_checkpoint(checkpoint_name)
to_state = self.world.observe_and_checkpoint(checkpoint_name)

# Use LSH for deduplication
to_state = self._lsh_cache.add_state(to_state)
to_state = self.graph.add_state(to_state)
```

---

## 3. MCTS Strategy (Medium - 1-2 weeks)

### Create `src/venomqa/v1/agent/mcts_strategy.py`

```python
"""Monte Carlo Tree Search exploration strategy."""

from __future__ import annotations

import math
import random
from collections import defaultdict
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from venomqa.v1.core.action import Action
    from venomqa.v1.core.graph import Graph
    from venomqa.v1.core.state import State


class MCTSNode:
    """Node in Monte Carlo Tree Search."""

    def __init__(
        self,
        state: State,
        parent: MCTSNode | None = None,
        action: Action | None = None,
    ):
        self.state = state
        self.parent = parent
        self.action = action

        self.children: dict[str, MCTSNode] = {}  # action_name → child
        self.visits: int = 0
        self.value_sum: float = 0.0

    @property
    def is_fully_expanded(self) -> bool:
        """Check if all possible actions have been tried."""
        return len(self.children) > 0

    @property
    def is_leaf(self) -> bool:
        """Check if node has no children."""
        return len(self.children) == 0


class MCTSStrategy:
    """Monte Carlo Tree Search exploration strategy.

    Features:
    - Balances exploration vs exploitation using UCB1
    - Adapts to state space complexity
    - Learns from simulations

    Hyperparameters:
    - c: Exploration parameter (higher = more exploration)
    - max_simulations: MCTS iterations per pick
    """

    def __init__(
        self,
        c: float = math.sqrt(2),
        max_simulations: int = 100,
        rollout_depth: int = 10,
    ):
        self.c = c
        self.max_simulations = max_simulations
        self.rollout_depth = rollout_depth

        self.root: MCTSNode | None = None
        self.graph: Graph | None = None

    def pick(self, graph: Graph) -> tuple[State, Action] | None:
        """Pick next (state, action) using MCTS."""
        self.graph = graph

        # Initialize or update root
        if self.root is None:
            self.root = self._initialize_root(graph)
            if self.root is None:
                return None

        # Run MCTS simulations
        for _ in range(self.max_simulations):
            # Selection
            leaf = self._select(self.root)

            # Expansion
            if leaf.visits > 0 and not leaf.is_fully_expanded:
                child = self._expand(leaf, graph)
                if child is not None:
                    leaf = child

            # Simulation
            value = self._simulate(leaf, graph)

            # Backpropagation
            self._backpropagate(leaf, value)

        # Return best action from root
        if not self.root.children:
            return None

        # Choose action with highest visits (most explored)
        best_action_name = max(
            self.root.children.keys(),
            key=lambda a: self.root.children[a].visits,
        )

        action = graph.get_action(best_action_name)
        state = self.root.state

        # Update root to chosen child for next iteration
        if best_action_name in self.root.children:
            self.root = self.root.children[best_action_name]

        return (state, action) if state and action else None

    def _initialize_root(self, graph: Graph) -> MCTSNode | None:
        """Initialize root node with initial state."""
        if not graph.initial_state_id:
            return None

        initial_state = graph.get_state(graph.initial_state_id)
        if initial_state is None:
            return None

        return MCTSNode(state=initial_state)

    def _select(self, node: MCTSNode) -> MCTSNode:
        """Select leaf node using UCB1 policy."""
        current = node

        while current.is_fully_expanded:
            # Choose child with highest UCB1 score
            best_child = max(
                current.children.values(),
                key=lambda n: self._ucb1(n),
            )
            current = best_child

        return current

    def _ucb1(self, node: MCTSNode) -> float:
        """Compute UCB1 score for node.

        Formula: UCB1 = Q/N + c * sqrt(log(N_parent) / N)
        """
        if node.visits == 0:
            return float('inf')  # Unexplored first

        exploit = node.value_sum / node.visits

        if node.parent and node.parent.visits > 0:
            explore = self.c * math.sqrt(
                math.log(node.parent.visits) / node.visits
            )
        else:
            explore = 0.0

        return exploit + explore

    def _expand(
        self,
        node: MCTSNode,
        graph: Graph,
    ) -> MCTSNode | None:
        """Expand node by adding unexplored action child."""
        state = node.state

        # Find unexplored actions
        valid_actions = graph.get_valid_actions(state)
        unexplored_actions = [
            a for a in valid_actions
            if a.name not in node.children
        ]

        if not unexplored_actions:
            return None  # Fully expanded

        # Pick random unexplored action
        action = random.choice(unexplored_actions)

        # Create child node
        # Note: In practice, would execute action via World to get next state
        # For now, simulate with hypothetical next state
        child = MCTSNode(
            state=state,  # Placeholder - would be actual next state
            parent=node,
            action=action,
        )

        node.children[action.name] = child
        return child

    def _simulate(self, node: MCTSNode, graph: Graph) -> float:
        """Random rollout from node to estimate value.

        Simplified: returns random value.
        In practice, would execute random actions and compute reward.
        """
        total_reward = 0.0

        # Random rollout for fixed depth
        for _ in range(self.rollout_depth):
            # Simulate random reward
            # In practice: execute action, check invariants, compute reward
            reward = random.gauss(0.5, 0.1)
            total_reward += reward

        return total_reward

    def _backpropagate(self, node: MCTSNode, value: float) -> None:
        """Update statistics from leaf to root."""
        current = node

        while current is not None:
            current.visits += 1
            current.value_sum += value
            current = current.parent
```

---

## Testing Each Improvement

### Test A* Path Finding

```python
# tests/v1/test_astar.py
import pytest
from venomqa.v1 import Agent, Action, World
from venomqa.v1.agent.strategies import DFS
from venomqa.v1.core.path_finder import AStarPathFinder, StateDistanceHeuristic


def test_astar_finds_shortest_path():
    """Test that A* finds shortest path."""
    # Setup graph with known path
    world = World()
    # ... define actions, invariants ...

    agent = Agent(world, actions=[...], strategy=DFS())

    result = agent.explore()

    # Test A* path finding
    from venomqa.v1.core.path_finder import AStarPathFinder

    heuristic = StateDistanceHeuristic(result.graph)
    path_finder = AStarPathFinder(result.graph, heuristic)

    # Find path from initial to final state
    path = path_finder.find_path(
        result.graph.initial_state_id,
        result.graph.states.keys().__iter__().__next__(),
    )

    assert len(path) > 0  # Found path
```

### Test LSH Cache

```python
# tests/v1/test_lsh_cache.py
from venomqa.v1.core.lsh_cache import LSHCache, LSHConfig
from venomqa.v1.core.state import State, Observation


def test_lsh_deduplication():
    """Test that LSH correctly deduplicates states."""
    cache = LSHCache(LSHConfig(num_bands=20, rows_per_band=5))

    # Create two identical states
    obs = Observation(system="test", data={"status": "ok"})
    state1 = State.create({"test": obs})
    state2 = State.create({"test": obs})

    # Add first state
    canonical1 = cache.add_state(state1)
    assert cache.size == 1

    # Add second state (should deduplicate)
    canonical2 = cache.add_state(state2)
    assert cache.size == 1  # Still 1 unique state
    assert canonical1.id == canonical2.id  # Same canonical state
```

---

## Performance Benchmarks

Run after each improvement to validate gains:

```python
# benchmarks/performance.py
import time
from venomqa.v1 import Agent, World

def benchmark_exploration(agent: Agent) -> dict:
    """Benchmark exploration performance."""
    start = time.time()

    result = agent.explore()

    end = time.time()

    return {
        "duration_ms": (end - start) * 1000,
        "states_visited": len(result.graph.states),
        "violations_found": len(result.violations),
        "coverage_percent": result.graph.used_action_count /
                          len(result.graph.actions) * 100,
    }

# Compare strategies
for strategy_name, strategy_cls in [
    ("BFS", BFS),
    ("DFS", DFS),
    ("MCTS", MCTSStrategy),
]:
    world = setup_world()  # Your setup function
    actions = define_actions()  # Your action definitions
    agent = Agent(world, actions=actions, strategy=strategy_cls())

    metrics = benchmark_exploration(agent)
    print(f"{strategy_name}: {metrics}")
```

---

## Next Steps

1. **Implement A* first** (easiest, immediate 2-4x speedup for path queries)
2. **Implement LSH second** (fast duplicate checking, 10-100x faster)
3. **Add MCTS** (intelligent exploration, 30-50% efficiency)
4. **Add tests** for each improvement
5. **Run benchmarks** to validate gains
6. **Iterate** on hyperparameters for your specific use case

For the remaining two priorities (RL and Multi-Objective), see the full analysis in `MATHEMATICS_ANALYSIS.md`.
