Skip to main content

parlov_analysis/
lib.rs

1//! Analysis engine for parlov: signal detection, statistics, and oracle classification.
2//!
3//! This crate is pure synchronous computation — no I/O, no async, no network stack. Keeping it
4//! isolated from `parlov-probe` means changing statistical thresholds or adding a new oracle
5//! pattern does not recompile `reqwest` or `hyper`.
6
7#![deny(clippy::all)]
8#![warn(clippy::pedantic)]
9#![deny(missing_docs)]
10
11pub mod aggregation;
12pub mod existence;
13pub mod signals;
14
15pub use aggregation::evidence::{confidence_to_prob, family_multiplier, logit, vector_to_family};
16pub use aggregation::{
17    compute_modifiers, passes_not_present_gate, EvidenceAccumulator, EvidenceModifiers,
18    ModifierResult, PreconditionBlock, PreconditionDecision, StopDecision, StopRule,
19};
20pub use existence::{burst_result, header_diff_result, SignalFamily};
21pub use signals::header::{is_rate_limit_header, rate_limit_diff};
22
23use parlov_core::{DifferentialSet, OracleClass, OracleResult, StrategyOutcome};
24
25/// Errors returned by [`Analyzer::analyze`].
26#[derive(Debug, thiserror::Error)]
27pub enum AnalyzerError {
28    /// The supplied `DifferentialSet` has fewer samples than the analyzer requires.
29    ///
30    /// Collect at least `required` exchange pairs before calling `analyze`.
31    #[error("insufficient samples — collect at least {required} before calling analyze")]
32    InsufficientSamples {
33        /// Minimum sample count the analyzer needs.
34        required: usize,
35    },
36}
37
38/// Decision returned by [`Analyzer::evaluate`] after inspecting the current sample set.
39pub enum SampleDecision {
40    /// Enough samples collected; here is the final result alongside its aggregation outcome.
41    Complete(Box<OracleResult>, StrategyOutcome),
42    /// Differential detected but not yet confirmed stable — collect one more pair.
43    NeedMore,
44}
45
46/// Analyzes paired baseline/probe exchanges and produces an oracle verdict.
47///
48/// Implementors must be `Send + Sync` so they can be held in shared state across async tasks.
49/// All methods take `&self` — analyzers are stateless with respect to individual probe runs.
50pub trait Analyzer: Send + Sync {
51    /// Incrementally evaluate a growing `DifferentialSet`.
52    ///
53    /// Called after each new exchange pair is added. Returns `NeedMore` until enough samples
54    /// are collected to determine stability, then `Complete` with the final result.
55    fn evaluate(&self, data: &DifferentialSet) -> SampleDecision;
56
57    /// Oracle class this analyzer handles — used for result attribution.
58    fn oracle_class(&self) -> OracleClass;
59
60    /// Analyze a complete `DifferentialSet` and return a verdict.
61    ///
62    /// One-shot wrapper around [`evaluate`][Self::evaluate]. For incremental sampling, call
63    /// `evaluate` directly.
64    ///
65    /// # Errors
66    ///
67    /// Returns [`AnalyzerError::InsufficientSamples`] when fewer samples than
68    /// [`required_samples`][Self::required_samples] were supplied.
69    fn analyze(&self, data: &DifferentialSet) -> Result<OracleResult, AnalyzerError> {
70        match self.evaluate(data) {
71            SampleDecision::Complete(result, _outcome) => Ok(*result),
72            SampleDecision::NeedMore => Err(AnalyzerError::InsufficientSamples {
73                required: self.required_samples(),
74            }),
75        }
76    }
77
78    /// Minimum number of exchange pairs needed before `analyze` returns `Ok`.
79    ///
80    /// Default is 1. Analyzers with adaptive sampling loops override this.
81    fn required_samples(&self) -> usize {
82        1
83    }
84}