parlov-analysis 0.7.0

Analysis engine trait and signal detection for parlov.
Documentation
//! Analysis engine for parlov: signal detection, statistics, and oracle classification.
//!
//! This crate is pure synchronous computation — no I/O, no async, no network stack. Keeping it
//! isolated from `parlov-probe` means changing statistical thresholds or adding a new oracle
//! pattern does not recompile `reqwest` or `hyper`.

#![deny(clippy::all)]
#![warn(clippy::pedantic)]
#![deny(missing_docs)]

pub mod aggregation;
pub mod existence;
pub mod signals;

pub use aggregation::evidence::{confidence_to_prob, family_multiplier, logit, vector_to_family};
pub use aggregation::{
    compute_modifiers, passes_not_present_gate, EvidenceAccumulator, EvidenceModifiers,
    ModifierResult, PreconditionBlock, PreconditionDecision, StopDecision, StopRule,
};
pub use existence::{burst_result, header_diff_result, SignalFamily};
pub use signals::header::{is_rate_limit_header, rate_limit_diff};

use parlov_core::{DifferentialSet, OracleClass, OracleResult, StrategyOutcome};

/// Errors returned by [`Analyzer::analyze`].
#[derive(Debug, thiserror::Error)]
pub enum AnalyzerError {
    /// The supplied `DifferentialSet` has fewer samples than the analyzer requires.
    ///
    /// Collect at least `required` exchange pairs before calling `analyze`.
    #[error("insufficient samples — collect at least {required} before calling analyze")]
    InsufficientSamples {
        /// Minimum sample count the analyzer needs.
        required: usize,
    },
}

/// Decision returned by [`Analyzer::evaluate`] after inspecting the current sample set.
pub enum SampleDecision {
    /// Enough samples collected; here is the final result alongside its aggregation outcome.
    Complete(Box<OracleResult>, StrategyOutcome),
    /// Differential detected but not yet confirmed stable — collect one more pair.
    NeedMore,
}

/// Analyzes paired baseline/probe exchanges and produces an oracle verdict.
///
/// Implementors must be `Send + Sync` so they can be held in shared state across async tasks.
/// All methods take `&self` — analyzers are stateless with respect to individual probe runs.
pub trait Analyzer: Send + Sync {
    /// Incrementally evaluate a growing `DifferentialSet`.
    ///
    /// Called after each new exchange pair is added. Returns `NeedMore` until enough samples
    /// are collected to determine stability, then `Complete` with the final result.
    fn evaluate(&self, data: &DifferentialSet) -> SampleDecision;

    /// Oracle class this analyzer handles — used for result attribution.
    fn oracle_class(&self) -> OracleClass;

    /// Analyze a complete `DifferentialSet` and return a verdict.
    ///
    /// One-shot wrapper around [`evaluate`][Self::evaluate]. For incremental sampling, call
    /// `evaluate` directly.
    ///
    /// # Errors
    ///
    /// Returns [`AnalyzerError::InsufficientSamples`] when fewer samples than
    /// [`required_samples`][Self::required_samples] were supplied.
    fn analyze(&self, data: &DifferentialSet) -> Result<OracleResult, AnalyzerError> {
        match self.evaluate(data) {
            SampleDecision::Complete(result, _outcome) => Ok(*result),
            SampleDecision::NeedMore => Err(AnalyzerError::InsufficientSamples {
                required: self.required_samples(),
            }),
        }
    }

    /// Minimum number of exchange pairs needed before `analyze` returns `Ok`.
    ///
    /// Default is 1. Analyzers with adaptive sampling loops override this.
    fn required_samples(&self) -> usize {
        1
    }
}