parlov_elicit/types.rs
1//! Core elicitation types: risk classification, probe specifications, and strategy metadata.
2
3use parlov_core::{ProbeDefinition, Technique};
4use serde::{Deserialize, Serialize};
5
6/// Provenance for Phase 2 chain-derived `ProbeSpec`s.
7///
8/// Attached by `generate_dag_chained_plan` so downstream consumers can trace
9/// the harvested signal that produced this probe — e.g. an `If-Match` probe
10/// whose value was harvested from an `ETag` response header carries
11/// `producer_kind = "Etag"` and `producer_value = "W/\"abc\""`.
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
13#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
14pub struct ChainProvenance {
15 /// Discriminant from `ProducerOutputKind` as a stable string, e.g. `"Etag"`.
16 pub producer_kind: String,
17 /// Serialized representation of the harvested value, e.g. `"W/\"abc123\""`.
18 pub producer_value: String,
19}
20
21/// Risk classification for a probing strategy.
22///
23/// Controls whether a strategy is eligible for execution given the operator's
24/// `max_risk` ceiling in `ScanContext`. Variants are ordered from safest to most
25/// destructive so that `<=` comparisons work correctly for ceiling enforcement.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
27pub enum RiskLevel {
28 /// Read-only probing. No state mutation; safe on any target.
29 Safe,
30 /// Uses non-idempotent HTTP methods (e.g. POST, PATCH, PUT) that may have
31 /// side-effects, but the strategy avoids permanent data loss.
32 MethodDestructive,
33 /// May trigger irreversible server-side state changes (e.g. DELETE, account
34 /// closure, resource exhaustion).
35 OperationDestructive,
36}
37
38/// Metadata describing a strategy: identity and risk rating.
39///
40/// Carried on every `ProbeSpec` so the scheduler and output layer can attribute
41/// each probe pair back to the strategy that generated it without holding a
42/// reference to the strategy object.
43#[derive(Debug, Clone)]
44pub struct StrategyMetadata {
45 /// e.g. `"existence-get-200-404"`
46 pub strategy_id: &'static str,
47 /// e.g. `"GET 200/404 existence"`
48 pub strategy_name: &'static str,
49 /// Risk classification.
50 pub risk: RiskLevel,
51}
52
53/// A baseline + probe pair for standard differential analysis.
54///
55/// The scheduler executes `baseline` and `probe` the required number of times,
56/// collecting `ResponseSurface` values into a `DifferentialSet` for analysis.
57#[derive(Debug, Clone)]
58pub struct ProbePair {
59 /// Control (known-existing).
60 pub baseline: ProbeDefinition,
61 /// Suspect (unknown).
62 pub probe: ProbeDefinition,
63 /// Optional unmutated baseline for control-integrity verification.
64 ///
65 /// Strategies that mutate the resource identifier or path (`case_normalize`,
66 /// `trailing_slash`) populate this with the original unmutated request. The runner
67 /// dispatches it as a third request; the result feeds into `control_integrity`.
68 /// Strategies that don't mutate the path leave it `None` (the gate is then inert
69 /// for those pairs).
70 pub canonical_baseline: Option<ProbeDefinition>,
71 /// Generating strategy.
72 pub metadata: StrategyMetadata,
73 /// Confidence calibration and evidence labeling.
74 pub technique: Technique,
75 /// Phase-2 chain provenance — `None` for phase-1 specs, `Some` when this
76 /// pair was generated by `generate_dag_chained_plan` from a harvested signal.
77 pub chain_provenance: Option<ChainProvenance>,
78}
79
80/// A burst probe: N requests to baseline URL, then N to probe URL.
81///
82/// Used for timing oracles where multiple samples are required for statistical
83/// significance. `burst_count` sets the minimum sample size per side; the
84/// adaptive timing analyzer may request additional rounds.
85#[derive(Debug, Clone)]
86pub struct BurstSpec {
87 /// Control (known-existing).
88 pub baseline: ProbeDefinition,
89 /// Suspect (unknown).
90 pub probe: ProbeDefinition,
91 /// Minimum samples per side before analysis.
92 pub burst_count: usize,
93 /// Generating strategy.
94 pub metadata: StrategyMetadata,
95 /// Confidence calibration and evidence labeling.
96 pub technique: Technique,
97 /// Phase-2 chain provenance — `None` for phase-1 specs, `Some` when this
98 /// burst was generated by `generate_dag_chained_plan` from a harvested signal.
99 pub chain_provenance: Option<ChainProvenance>,
100}
101
102/// The unit of work handed to the probe scheduler.
103///
104/// Each variant encodes a different execution and collection strategy. The
105/// scheduler dispatches on the variant; the analysis layer receives a `DifferentialSet`
106/// regardless of which variant produced it.
107#[derive(Debug, Clone)]
108pub enum ProbeSpec {
109 /// Standard adaptive loop: one baseline request, one probe request.
110 Pair(ProbePair),
111 /// Send `burst_count` requests to the baseline URL, then `burst_count` to
112 /// the probe URL, for statistical timing analysis.
113 Burst(BurstSpec),
114 /// One baseline + one probe; compare full response header sets rather than
115 /// bodies or status codes.
116 HeaderDiff(ProbePair),
117}
118
119impl ProbeSpec {
120 /// Technique metadata.
121 #[must_use]
122 pub fn technique(&self) -> &Technique {
123 match self {
124 Self::Pair(p) | Self::HeaderDiff(p) => &p.technique,
125 Self::Burst(b) => &b.technique,
126 }
127 }
128
129 /// Chain provenance — `Some` when generated by phase-2 DAG walking, else `None`.
130 #[must_use]
131 pub fn chain_provenance(&self) -> Option<&ChainProvenance> {
132 match self {
133 Self::Pair(p) | Self::HeaderDiff(p) => p.chain_provenance.as_ref(),
134 Self::Burst(b) => b.chain_provenance.as_ref(),
135 }
136 }
137
138 /// Returns a clone of this spec with `chain_provenance` set on whichever
139 /// variant it carries. Used by `generate_dag_chained_plan` to attach
140 /// provenance after a consumer returns generic specs.
141 #[must_use]
142 pub fn with_chain_provenance(self, prov: ChainProvenance) -> Self {
143 match self {
144 Self::Pair(mut p) => {
145 p.chain_provenance = Some(prov);
146 Self::Pair(p)
147 }
148 Self::HeaderDiff(mut p) => {
149 p.chain_provenance = Some(prov);
150 Self::HeaderDiff(p)
151 }
152 Self::Burst(mut b) => {
153 b.chain_provenance = Some(prov);
154 Self::Burst(b)
155 }
156 }
157 }
158}
159
160#[cfg(test)]
161#[path = "types_tests.rs"]
162mod tests;