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}