Skip to main content

parlov_analysis/aggregation/
control.rs

1//! Control-integrity gate for evidence modifiers.
2//!
3//! Detects when a route-mutating technique (`case_normalize`, `trailing_slash`) destroyed the
4//! intended baseline reference. The runner issues a pre-flight canonical (unmutated) baseline
5//! alongside the mutated pair; if the canonical succeeds (2xx) but the mutated baseline fails,
6//! the mutation broke routing — the resulting status-equality Contradictory is invalid and the
7//! outcome must downgrade to `Inapplicable(MutationDestroyedControl)`.
8
9use parlov_core::DifferentialSet;
10
11/// Decision returned by [`control_integrity`].
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum ControlDecision {
14    /// Control reference preserved (or no canonical was issued — gate inert).
15    Reached(f64),
16    /// Mutation destroyed the control — downgrade to Inapplicable.
17    Blocked,
18}
19
20impl ControlDecision {
21    /// Numeric confidence: `Reached(c)` → `c`; `Blocked` → `0.0`.
22    #[must_use]
23    pub fn confidence(self) -> f64 {
24        match self {
25            Self::Reached(c) => c,
26            Self::Blocked => 0.0,
27        }
28    }
29}
30
31/// Computes the control-integrity modifier for a probe pair carrying an optional canonical.
32///
33/// Returns `Reached(1.0)` when:
34/// - no canonical was issued (gate inert — the strategy didn't mutate the path)
35/// - the canonical and the mutated baseline both succeeded (mutation reached the same controller)
36/// - both canonical and mutated baseline failed (mutation didn't break routing further than auth
37///   or other gates already did)
38///
39/// Returns `Blocked` when:
40/// - canonical succeeded (2xx) but mutated baseline failed (non-2xx) — mutation broke routing
41/// - canonical returned 301 or 308 — server canonicalized away from the mutated path,
42///   indicating the mutated request is not the same resource
43#[must_use]
44pub fn control_integrity(differential: &DifferentialSet) -> ControlDecision {
45    let Some(canonical) = differential.canonical.as_ref() else {
46        return ControlDecision::Reached(1.0);
47    };
48    let Some(mutated) = differential.baseline.first() else {
49        return ControlDecision::Reached(1.0);
50    };
51    let canonical_status = canonical.response.status.as_u16();
52    if matches!(canonical_status, 301 | 308) {
53        return ControlDecision::Blocked;
54    }
55    let canonical_ok = canonical.response.status.is_success();
56    let mutated_ok = mutated.response.status.is_success();
57    if canonical_ok && !mutated_ok {
58        return ControlDecision::Blocked;
59    }
60    ControlDecision::Reached(1.0)
61}
62
63#[cfg(test)]
64#[path = "control_tests.rs"]
65mod tests;