# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-refuse-to-treat-an-llm-agent-s-repeated-answer-as-independent-corroboration-in-byzantine-consensus.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-refuse-to-treat-an-llm-agent-s-repeated-answer-as-independent-corroboration-in-byzantine-consensus.warrant.json
# Markdown SHA-256:   5adaf9e585f9c69673ba782c8a6a28e2184afc8e573cd62c827b7d96bde4f932
# Code AST SHA-256:   05b0e92ceb2ecc58ee6eff340b43065e85f50fc2b73b1cba590f2836c75b4072
# Verified Classes:   CorrelatedVoteError, Vote
# Defensive Guards:   6 fail-closed invariants (zero external deps)

import hashlib
from dataclasses import dataclass
from typing import Iterable


class CorrelatedVoteError(RuntimeError):
    """Raised when a vote cannot be counted as independent evidence."""


@dataclass(frozen=True)
class Vote:
    operator: str
    weights_hash: str
    seed: int | None
    prompt_hash: str
    claim_embedding: tuple[float, ...]


def _cosine(a: tuple[float, ...], b: tuple[float, ...]) -> float:
    dot = sum(x * y for x, y in zip(a, b))
    na = sum(x * x for x in a) ** 0.5
    nb = sum(y * y for y in b) ** 0.5
    if na == 0 or nb == 0:
        raise CorrelatedVoteError("zero-norm embedding; vote is uninformative")
    return dot / (na * nb)


def count_independent_votes(
    votes: Iterable[Vote],
    f: int,
    similarity_band: tuple[float, float] = (0.85, 0.95),
) -> int:
    """
    Count votes that are genuinely independent under the BFT invariant.
    Raises CorrelatedVoteError if the count is fragile or if any vote
    duplicates another's (weights_hash, seed) or (weights_hash, prompt_hash).
    """
    seen_pairs: set[tuple[str, int | None]] = set()
    seen_prompts: set[tuple[str, str]] = set()
    independent: list[Vote] = []

    for v in votes:
        if v.seed is None:
            raise CorrelatedVoteError(
                f"vote from {v.operator} lacks seed provenance; "
                "cannot establish independence"
            )
        if (v.weights_hash, v.seed) in seen_pairs:
            raise CorrelatedVoteError(
                f"duplicate (weights_hash, seed) from {v.operator}; "
                "same parametric draw counted twice"
            )
        if (v.weights_hash, v.prompt_hash) in seen_prompts:
            raise CorrelatedVoteError(
                f"duplicate prompt on same weights from {v.operator}; "
                "prompt-level correlation"
            )
        seen_pairs.add((v.weights_hash, v.seed))
        seen_prompts.add((v.weights_hash, v.prompt_hash))
        independent.append(v)

    # Fragility check: does the quorum outcome depend on the threshold?
    lo, hi = similarity_band
    for a_i in range(len(independent)):
        for b_i in range(a_i + 1, len(independent)):
            sim = _cosine(
                independent[a_i].claim_embedding,
                independent[b_i].claim_embedding,
            )
            if lo <= sim <= hi:
                raise CorrelatedVoteError(
                    f"fragile quorum: votes {a_i},{b_i} similarity {sim:.3f} "
                    f"inside ambiguity band {similarity_band}; commit would "
                    "depend on arbitrary equivalence threshold"
                )

    n_weights = len({v.weights_hash for v in independent})
    if n_weights < 2 * f + 1:
        raise CorrelatedVoteError(
            f"only {n_weights} distinct weight-sets among {len(independent)} "
            f"votes; need >= 2f+1 = {2 * f + 1} for claimed fault bound f={f}"
        )
    return len(independent)
