parlov_analysis/aggregation/
precondition.rs1use parlov_core::{Applicability, BlockFamily, DifferentialSet, ProbeExchange, Technique};
12
13use super::auth_equivalence::auth_gate_decision;
14use super::auth_types::{AuthGateDecision, CredentialBlockKind};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum AuthBlockLayer {
19 Origin,
21 Proxy,
23 Network,
25 LoginRedirect,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum PreconditionBlock {
35 AuthGateBeforeTechnique {
38 credential_state: CredentialBlockKind,
40 layer: AuthBlockLayer,
42 },
43 MethodGateBeforeResource,
45 BlockedByParser,
47 ApplicabilityMarkerMissing,
49 SurfaceMismatch,
52 MutationDestroyedControl,
55}
56
57impl PreconditionBlock {
58 #[must_use]
60 pub fn block_family(self) -> BlockFamily {
61 match self {
62 Self::AuthGateBeforeTechnique { .. } => BlockFamily::Authorization,
63 Self::MethodGateBeforeResource => BlockFamily::Method,
64 Self::BlockedByParser => BlockFamily::Parser,
65 Self::ApplicabilityMarkerMissing | Self::MutationDestroyedControl => {
66 BlockFamily::TechniqueLocal
67 }
68 Self::SurfaceMismatch => BlockFamily::Surface,
69 }
70 }
71
72 #[must_use]
74 pub fn as_str(self) -> &'static str {
75 match self {
76 Self::AuthGateBeforeTechnique {
77 credential_state,
78 layer,
79 } => auth_block_reason(credential_state, layer),
80 Self::MethodGateBeforeResource => "method-level rejection before resource lookup",
81 Self::BlockedByParser => "parser/validator rejection before technique evaluated",
82 Self::ApplicabilityMarkerMissing => "technique applicability marker not observed",
83 Self::SurfaceMismatch => {
84 "differential observed on a surface this technique does not test"
85 }
86 Self::MutationDestroyedControl => {
87 "mutation broke baseline route — control reference destroyed"
88 }
89 }
90 }
91}
92
93#[must_use]
95fn auth_block_reason(credential_state: CredentialBlockKind, layer: AuthBlockLayer) -> &'static str {
96 match (credential_state, layer) {
97 (CredentialBlockKind::NoCredential, AuthBlockLayer::Origin) => {
98 "auth gate fired before technique (no credential provided)"
99 }
100 (CredentialBlockKind::CredentialRejected, AuthBlockLayer::Origin) => {
101 "auth gate fired before technique (credential rejected — token invalid/expired)"
102 }
103 (CredentialBlockKind::InsufficientScope, AuthBlockLayer::Origin) => {
104 "auth gate fired before technique (credential lacks required scope)"
105 }
106 (CredentialBlockKind::UnknownAuthFailure, AuthBlockLayer::Origin) => {
107 "auth gate fired before technique (specific failure not identified)"
108 }
109 (CredentialBlockKind::NotApplicable, AuthBlockLayer::Origin) => {
110 "auth gate fired before technique reached oracle layer"
111 }
112 (_, AuthBlockLayer::Proxy) => "proxy auth required before technique reached origin",
113 (_, AuthBlockLayer::Network) => "network/captive-portal auth required",
114 (_, AuthBlockLayer::LoginRedirect) => "login-redirect fired before technique",
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq)]
121pub enum PreconditionDecision {
122 Reached(f64),
124 Blocked(PreconditionBlock),
126}
127
128impl PreconditionDecision {
129 #[must_use]
131 pub fn confidence(self) -> f64 {
132 match self {
133 Self::Reached(c) => c,
134 Self::Blocked(_) => 0.0,
135 }
136 }
137
138 #[must_use]
140 pub fn block_reason(self) -> Option<PreconditionBlock> {
141 match self {
142 Self::Blocked(reason) => Some(reason),
143 Self::Reached(_) => None,
144 }
145 }
146}
147
148fn same_method_gate(differential: &DifferentialSet) -> bool {
150 let Some((b, p)) = first_pair(differential) else {
151 return false;
152 };
153 b.response.status.as_u16() == 405 && p.response.status.as_u16() == 405
154}
155
156fn same_parser_failure(differential: &DifferentialSet) -> bool {
163 let Some((b, p)) = first_pair(differential) else {
164 return false;
165 };
166 let bs = b.response.status.as_u16();
167 let ps = p.response.status.as_u16();
168 matches!(bs, 400 | 422) && bs == ps
169}
170
171fn first_pair(differential: &DifferentialSet) -> Option<(&ProbeExchange, &ProbeExchange)> {
173 let b = differential.baseline.first()?;
174 let p = differential.probe.first()?;
175 Some((b, p))
176}
177
178#[must_use]
190pub fn precondition_confidence(
191 technique: &Technique,
192 differential: &DifferentialSet,
193) -> PreconditionDecision {
194 match auth_gate_decision(differential) {
195 AuthGateDecision::Gate(reason) => return PreconditionDecision::Blocked(reason),
196 AuthGateDecision::DoNotGate => return PreconditionDecision::Reached(1.0),
197 AuthGateDecision::NoAuthInvolvement => {}
198 }
199 if same_method_gate(differential) && !technique.method_relevant {
200 return PreconditionDecision::Blocked(PreconditionBlock::MethodGateBeforeResource);
201 }
202 if same_parser_failure(differential) && !technique.parser_relevant {
203 return PreconditionDecision::Blocked(PreconditionBlock::BlockedByParser);
204 }
205 let Some((b, p)) = first_pair(differential) else {
206 return PreconditionDecision::Reached(1.0);
207 };
208 let app = (technique.applicability)(&b.response, &p.response);
209 if app == Applicability::Missing {
210 return PreconditionDecision::Blocked(PreconditionBlock::ApplicabilityMarkerMissing);
211 }
212 PreconditionDecision::Reached(app.confidence())
213}
214
215#[cfg(test)]
216#[path = "precondition_tests.rs"]
217mod tests;