Skip to main content

dsfb_database/grammar/
mod.rs

1//! Motif grammar layer.
2//!
3//! The grammar consumes a [`crate::residual::ResidualStream`] and emits a
4//! sequence of *episodes*: typed, time-bounded structural events that
5//! correspond to operator-recognisable database health states. The grammar
6//! is **deterministic** (same input → same output, bytewise) and
7//! **observer-only** (no engine state is touched).
8//!
9//! Each motif class is a small state machine over a single residual class,
10//! parameterised by:
11//!   * the DSFB observer (drift / slew thresholds via `dsfb::DsfbObserver`)
12//!   * an envelope (deterministic threshold band)
13//!   * a minimum dwell time (debounce against single-sample blips)
14//!
15//! Parameters are loaded from `spec/motifs.yaml` (see [`MotifGrammar::from_yaml`])
16//! so that the paper, the crate, and the operator's deployment all share the
17//! same numbers.
18
19pub mod envelope;
20pub mod motifs;
21pub mod replay;
22
23use crate::residual::{ResidualClass, ResidualStream};
24use serde::{Deserialize, Serialize};
25
26/// One of the five motif classes the paper claims.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
28pub enum MotifClass {
29    PlanRegressionOnset,
30    CardinalityMismatchRegime,
31    ContentionRamp,
32    CacheCollapse,
33    WorkloadPhaseTransition,
34}
35
36impl MotifClass {
37    pub const ALL: [MotifClass; 5] = [
38        Self::PlanRegressionOnset,
39        Self::CardinalityMismatchRegime,
40        Self::ContentionRamp,
41        Self::CacheCollapse,
42        Self::WorkloadPhaseTransition,
43    ];
44
45    pub fn name(&self) -> &'static str {
46        match self {
47            Self::PlanRegressionOnset => "plan_regression_onset",
48            Self::CardinalityMismatchRegime => "cardinality_mismatch_regime",
49            Self::ContentionRamp => "contention_ramp",
50            Self::CacheCollapse => "cache_collapse",
51            Self::WorkloadPhaseTransition => "workload_phase_transition",
52        }
53    }
54
55    pub fn residual_class(&self) -> ResidualClass {
56        match self {
57            Self::PlanRegressionOnset => ResidualClass::PlanRegression,
58            Self::CardinalityMismatchRegime => ResidualClass::Cardinality,
59            Self::ContentionRamp => ResidualClass::Contention,
60            Self::CacheCollapse => ResidualClass::CacheIo,
61            Self::WorkloadPhaseTransition => ResidualClass::WorkloadPhase,
62        }
63    }
64}
65
66/// A single motif episode: a typed structural event with an explicit
67/// boundary. The CLI emits these as JSON; the paper figures plot them.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct Episode {
70    pub motif: MotifClass,
71    pub channel: Option<String>,
72    pub t_start: f64,
73    pub t_end: f64,
74    /// Peak |residual| observed inside the episode (for ranking / plotting).
75    pub peak: f64,
76    /// EMA-smoothed residual at episode boundary (for traceability into the
77    /// DSFB observer's trust state).
78    pub ema_at_boundary: f64,
79    /// Aggregate trust weight sum across channels at boundary (always 1.0
80    /// up to floating-point tolerance — included for audit).
81    pub trust_sum: f64,
82}
83
84/// Per-motif tunable parameters. Loaded from `spec/motifs.yaml`.
85#[derive(Debug, Clone, Deserialize, Serialize)]
86pub struct MotifParams {
87    /// DSFB EMA smoothing.
88    pub rho: f64,
89    /// DSFB trust softness.
90    pub sigma0: f64,
91    /// Drift envelope: |EMA residual| above this enters drift state.
92    pub drift_threshold: f64,
93    /// Slew envelope: instantaneous |residual| above this enters boundary
94    /// state.
95    pub slew_threshold: f64,
96    /// Minimum dwell time in seconds; episodes shorter than this are
97    /// discarded as blips.
98    pub min_dwell_seconds: f64,
99}
100
101impl MotifParams {
102    /// Conservative defaults used in tests and as the paper's published
103    /// baseline — every reported number can be reproduced by leaving these
104    /// alone.
105    pub fn default_for(class: MotifClass) -> Self {
106        match class {
107            MotifClass::PlanRegressionOnset => Self {
108                rho: 0.9,
109                sigma0: 0.05,
110                drift_threshold: 0.20,
111                slew_threshold: 0.50,
112                min_dwell_seconds: 5.0,
113            },
114            MotifClass::CardinalityMismatchRegime => Self {
115                rho: 0.9,
116                sigma0: 0.05,
117                drift_threshold: 0.5, // log10: 3.16x sustained mismatch
118                slew_threshold: 1.0,  // 10x instantaneous
119                min_dwell_seconds: 2.0,
120            },
121            MotifClass::ContentionRamp => Self {
122                rho: 0.85,
123                sigma0: 0.01,
124                drift_threshold: 0.05,
125                slew_threshold: 0.5,
126                min_dwell_seconds: 1.0,
127            },
128            MotifClass::CacheCollapse => Self {
129                rho: 0.9,
130                sigma0: 0.02,
131                drift_threshold: 0.10,
132                slew_threshold: 0.30,
133                min_dwell_seconds: 5.0,
134            },
135            MotifClass::WorkloadPhaseTransition => Self {
136                rho: 0.9,
137                sigma0: 0.02,
138                drift_threshold: 0.15,
139                slew_threshold: 0.35,
140                min_dwell_seconds: 30.0,
141            },
142        }
143    }
144}
145
146/// The whole grammar: one parameter set per motif class.
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct MotifGrammar {
149    pub plan_regression_onset: MotifParams,
150    pub cardinality_mismatch_regime: MotifParams,
151    pub contention_ramp: MotifParams,
152    pub cache_collapse: MotifParams,
153    pub workload_phase_transition: MotifParams,
154}
155
156impl Default for MotifGrammar {
157    fn default() -> Self {
158        Self {
159            plan_regression_onset: MotifParams::default_for(MotifClass::PlanRegressionOnset),
160            cardinality_mismatch_regime: MotifParams::default_for(
161                MotifClass::CardinalityMismatchRegime,
162            ),
163            contention_ramp: MotifParams::default_for(MotifClass::ContentionRamp),
164            cache_collapse: MotifParams::default_for(MotifClass::CacheCollapse),
165            workload_phase_transition: MotifParams::default_for(
166                MotifClass::WorkloadPhaseTransition,
167            ),
168        }
169    }
170}
171
172impl MotifGrammar {
173    pub fn params(&self, class: MotifClass) -> &MotifParams {
174        match class {
175            MotifClass::PlanRegressionOnset => &self.plan_regression_onset,
176            MotifClass::CardinalityMismatchRegime => &self.cardinality_mismatch_regime,
177            MotifClass::ContentionRamp => &self.contention_ramp,
178            MotifClass::CacheCollapse => &self.cache_collapse,
179            MotifClass::WorkloadPhaseTransition => &self.workload_phase_transition,
180        }
181    }
182
183    pub fn from_yaml(yaml: &str) -> anyhow::Result<Self> {
184        Ok(serde_yaml::from_str(yaml)?)
185    }
186}
187
188/// The thing that runs the grammar over a stream and emits episodes.
189pub struct MotifEngine {
190    grammar: MotifGrammar,
191}
192
193impl MotifEngine {
194    pub fn new(grammar: MotifGrammar) -> Self {
195        Self { grammar }
196    }
197
198    /// Run all five motif state machines over the stream. Output is
199    /// time-ordered and deterministic for a given (stream, grammar) pair.
200    pub fn run(&self, stream: &ResidualStream) -> Vec<Episode> {
201        let mut all = Vec::new();
202        for class in MotifClass::ALL {
203            let params = self.grammar.params(class).clone();
204            let eps = motifs::run_motif(class, &params, stream);
205            all.extend(eps);
206        }
207        all.sort_by(|a, b| {
208            a.t_start
209                .partial_cmp(&b.t_start)
210                .unwrap_or(std::cmp::Ordering::Equal)
211        });
212        all
213    }
214}