Skip to main content

dsfb_debug/
types.rs

1//! DSFB-Debug: core domain types — public API contract.
2//!
3//! Every type in this module is `Copy + Clone + Debug + PartialEq`
4//! with no heap allocation. This is the load-bearing engineering
5//! claim of the no_std core: the entire residual evaluation pipeline
6//! moves Copy values across function boundaries. No Box, no Rc, no
7//! Vec on hot paths. Every public surface accepts and returns
8//! `&[T]` slices only — the observer-only contract is type-enforced.
9//!
10//! # Mapping to paper §5
11//!
12//! | Type | Paper concept | Section |
13//! |------|---------------|:------:|
14//! | `SignTuple` | residual signature σ(k) = (‖r‖, ṙ, r̈) | §5.3 |
15//! | `GrammarState` | 4-state automaton | §5.5 |
16//! | `ReasonCode` | residual policy reason | §5.5 |
17//! | `MotifClass` | typed motif (32 variants) | §5.6 |
18//! | `Provenance` | evidence ladder (3-tier) | §5.6 |
19//! | `SemanticDisposition` | bank lookup outcome | §5.6 |
20//! | `MatchConfidence` | per-episode evidence packet | §11.y |
21//! | `HeuristicEntry` | bank record (motif IP claim) | §4.x |
22//! | `DebugEpisode` | aggregated structural episode | §7 |
23//! | `BenchmarkMetrics` | RSCR / fault-recall / FP rate | §13 |
24//! | `AuditRecord` | NIST SP 800-53 AU-3 record | §15 |
25//!
26//! # Standards alignment
27//!
28//! - **NIST SP 800-53 AU-3** (audit record content): `AuditRecord`
29//!   fields cover the required event_type / when / where / source /
30//!   outcome content axes.
31//! - **NIST SP 800-53 AU-2** (auditable events):
32//!   `HeuristicEntry.primary_witness_detectors` defines the named
33//!   auditable events that must fire for typed confirmation.
34//! - **ISO/IEC 25010** (Analysability): typed `MotifClass` + per-
35//!   episode evidence packet support the Analysability quality
36//!   characteristic.
37//! - **IEEE 1012-2016** (V&V): the confuser-boundary mechanism
38//!   provides independent validation of typed disposition.
39//!
40//! # Stability guarantee
41//!
42//! All `pub struct` fields are additive across versions; existing
43//! fields retain their type and semantics. A new field added in a
44//! later phase always carries a default-friendly meaning (e.g.
45//! `affinity_tiers: 0` falls back to the reason-code-derived mask;
46//! `primary_witness_detectors: &[]` disables Phase 8). This
47//! preserves Theorem 9 deterministic replay across upgrades.
48
49/// Residual sign tuple σ(k) = (‖r‖, ṙ, r̈)
50/// Paper §5.3
51#[derive(Copy, Clone, Debug, PartialEq)]
52pub struct SignTuple {
53    /// ‖r(k)‖ — instantaneous deviation magnitude
54    pub norm: f64,
55    /// ṙ(k) — finite-difference drift rate (window-to-window)
56    pub drift: f64,
57    /// r̈(k) — second difference / slew (curvature of trajectory)
58    pub slew: f64,
59}
60
61impl SignTuple {
62    pub const ZERO: Self = Self { norm: 0.0, drift: 0.0, slew: 0.0 };
63}
64
65/// Grammar state — Paper §5.5
66#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
67pub enum GrammarState {
68    /// Within envelope, drift inward or bounded. Normal operation.
69    Admissible = 0,
70    /// Approaching envelope with sustained outward drift. Early-warning.
71    Boundary = 1,
72    /// Exited envelope. Structural fault / actionable incident.
73    Violation = 2,
74}
75
76/// Grammar reason codes — Paper §5.5 Table
77#[derive(Copy, Clone, Debug, PartialEq, Eq)]
78pub enum ReasonCode {
79    /// Normal operation — no structural concern
80    Admissible,
81    /// Approaching SLO boundary — monitoring recommended
82    BoundaryApproach,
83    /// Memory leak, latency creep, cache degradation, dependency slowdown
84    SustainedOutwardDrift,
85    /// Crash, spike, exception storm, deployment regression, cascading timeout
86    AbruptSlewViolation,
87    /// Periodic load saturation, GC pressure, cron-triggered spikes
88    RecurrentBoundaryGrazing,
89    /// Confirmed SLO breach — actionable incident
90    EnvelopeViolation,
91    /// Outward drift followed by self-correction
92    DriftWithRecovery,
93    /// Transient single-step boundary touch — dismissed by persistence gate
94    SingleCrossing,
95}
96
97/// Policy states — developer-facing output
98/// Paper §5: Silent / Watch / Review / Escalate
99#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
100pub enum PolicyState {
101    /// No motif activated; persistence or corroboration gate failed
102    Silent = 0,
103    /// Structural activity below escalation threshold
104    Watch = 1,
105    /// Motif confirmed; developer review warranted
106    Review = 2,
107    /// Motif confirmed + violation-class; immediate attention
108    Escalate = 3,
109}
110
111/// Motif classes for software debugging — Paper §5.6.
112///
113/// Names are anchored to IEEE 24765 vocabulary and the
114/// Avizienis–Laprie–Randell dependability tree (fault → error → failure).
115/// No ad-hoc terminology: every variant decomposes into established
116/// software-engineering vocabulary.
117#[derive(Copy, Clone, Debug, PartialEq, Eq)]
118pub enum MotifClass {
119    // ===== Tier-1: original 10 motifs (FrameworkDesign provenance) =======
120
121    /// Sustained monotonic memory-consumption drift.
122    /// IEEE 24765: "memory leak"; A-L-R: latent fault → error.
123    MemoryLeakDrift,
124    /// Step-change latency propagating across dependency chain.
125    /// IEEE 24765: "fault propagation"; A-L-R: error → service-failure.
126    CascadingTimeoutSlew,
127    /// Abrupt shift coinciding with deployment event.
128    /// IEEE 24765: "regression"; A-L-R: design fault → error.
129    DeploymentRegressionSlew,
130    /// Oscillatory approach to latency SLO boundary.
131    /// IEEE 24765: "performance degradation"; A-L-R: marginal-state error.
132    CacheDegradationGrazing,
133    /// Slow positive drift in queue depth + latency.
134    /// IEEE 24765: "resource exhaustion"; A-L-R: error build-up.
135    ConnectionPoolExhaustionDrift,
136    /// Periodic slew events from garbage collection.
137    /// IEEE 24765: "stop-the-world pause"; A-L-R: transient error.
138    GcPressureOscillation,
139    /// Sustained positive drift in error rate.
140    /// IEEE 24765: "error escalation"; A-L-R: error → multi-failure regime.
141    ErrorRateEscalation,
142    /// Gradual latency increase in upstream dependency.
143    /// IEEE 24765: "performance degradation upstream"; A-L-R: external fault.
144    DependencySlowdown,
145    /// CPU/memory/disk approaching ceiling.
146    /// IEEE 24765: "resource saturation"; A-L-R: latent → manifest fault.
147    ResourceSaturation,
148    /// Message queue depth growing monotonically.
149    /// IEEE 24765: "back-pressure accumulation"; A-L-R: error build-up.
150    QueueBackpressure,
151
152    // ===== Tier-2: TADBench / TrainTicket fault cases =====================
153
154    /// Retry-storm cascade: client retries amplify upstream load.
155    /// Evidence: TADBench retry-storm fault case.
156    /// IEEE 24765: "retry-induced amplification"; A-L-R: cascading error.
157    RetryStormCascade,
158    /// Circuit breaker open-state shift; downstream calls return immediately.
159    /// Evidence: TADBench circuit-breaker fault case.
160    /// IEEE 24765: "fault tolerance mechanism state change".
161    CircuitBreakerOpenShift,
162    /// Database lock contention with rising queue + latency.
163    /// Evidence: TADBench db-lock fault case.
164    /// IEEE 24765: "concurrency fault"; A-L-R: synchronisation error.
165    DatabaseLockContention,
166    /// Authentication failure spike (auth-backend partial outage).
167    /// Evidence: TADBench auth-fail fault case.
168    /// IEEE 24765: "authentication subsystem failure".
169    AuthenticationFailureSpike,
170    /// Step shift coinciding with version-config change.
171    /// Evidence: TrainTicket-Anomaly version-config fault class.
172    /// IEEE 24765: "configuration regression"; A-L-R: design-time fault.
173    ConfigDriftRegression,
174
175    // ===== Tier-3: AIOps Challenge categories =============================
176
177    /// Packet-loss-induced error escalation (network-layer fault).
178    /// Evidence: AIOps Challenge packet_loss category.
179    /// IEEE 24765: "communication failure (lower layer)".
180    PacketLossErrorEscalation,
181    /// Network-delay-induced upstream latency inflation.
182    /// Evidence: AIOps Challenge network_delay category.
183    /// IEEE 24765: "communication-path performance fault".
184    NetworkDelayDependencyInflation,
185    /// Disk-I/O saturation; concave-up latency drift.
186    /// Evidence: AIOps Challenge disk_exhaustion category.
187    /// IEEE 24765: "storage subsystem saturation".
188    DiskIoSaturation,
189    /// CPU saturation; latency drift with rising envelope occupancy.
190    /// Evidence: AIOps Challenge cpu_exhaustion category.
191    /// IEEE 24765: "compute resource saturation".
192    CpuSaturation,
193    /// JVM heap pressure; sustained latency drift with rising variance.
194    /// Refines `MemoryLeakDrift` for JVM-specific signatures.
195    /// Evidence: AIOps Challenge memory_exhaustion category.
196    JvmHeapPressure,
197    /// JVM GC pause: distinct stop-the-world latency spikes.
198    /// Refines `GcPressureOscillation` for JVM-specific signatures.
199    /// Evidence: AIOps Challenge jvm_resource_exhaustion category.
200    JvmGcPause,
201
202    // ===== Tier-4: MultiDimension-Localization patterns ==================
203
204    /// Drift propagating along the service-call graph (multi-hop).
205    /// Evidence: MultiDim-Localization root-cause-graph cases.
206    /// IEEE 24765: "graph-structured fault propagation".
207    ServiceGraphDriftPropagation,
208    /// Multi-metric correlated anomaly without dominant single signal.
209    /// Evidence: MultiDim-Localization high-dim cluster cases.
210    /// IEEE 24765: "compound fault signature".
211    HighDimAnomalyCluster,
212    /// Historically-correlated metrics decorrelate; structural regime shift.
213    /// Evidence: MultiDim-Localization correlation-collapse cases.
214    /// IEEE 24765: "structural model invalidation".
215    MetricCorrelationCollapse,
216
217    // ===== Tier-5: DeepTraLog log + trace fusion patterns ================
218
219    /// Log-frequency outward drift on a service.
220    /// Evidence: DeepTraLog log-volume anomalies.
221    /// IEEE 24765: "diagnostic-output anomaly".
222    LogVolumeAnomaly,
223    /// Log timing departs from trace timing pattern.
224    /// Evidence: DeepTraLog log-trace temporal-mismatch cases.
225    /// IEEE 24765: "instrumentation-temporal divergence".
226    LogTraceTemporalDecorrelation,
227    /// Log severity distribution shifts (more WARN/ERROR proportionally).
228    /// Evidence: DeepTraLog severity-shift cases.
229    /// IEEE 24765: "diagnostic severity escalation".
230    LogSeverityEscalation,
231
232    // ===== Tier-6: cross-cutting structural motifs =======================
233
234    /// Concave-up approach to a ceiling; generalises ResourceSaturation.
235    /// IEEE 24765: "asymptotic resource saturation".
236    SaturationTrending,
237    /// Short-duration high-slew event that self-resolves.
238    /// IEEE 24765: "transient-only error"; A-L-R: transient fault.
239    EpisodicTransientSpike,
240    /// Outward drift followed by return to baseline.
241    /// Refines `ReasonCode::DriftWithRecovery` into a motif class.
242    /// IEEE 24765: "self-healing transient drift".
243    RegressiveDriftWithRecovery,
244    /// First-time approach to the admissibility envelope without
245    /// recurrence or persistent drift evidence. Catches the
246    /// `ReasonCode::BoundaryApproach` reason code so it isn't an
247    /// orphan in the canonical bank (validated by
248    /// `tests::no_orphan_reason_codes_in_canonical_bank`).
249    /// IEEE 24765: "marginal-state transient"; A-L-R: dormant fault.
250    EnvelopeBoundaryApproach,
251    /// Envelope breach without abrupt slew evidence — the
252    /// "value stepped past the threshold but the trajectory shape
253    /// shows no slew" case. Catches the
254    /// `ReasonCode::EnvelopeViolation` reason code so it isn't an
255    /// orphan in the canonical bank.
256    /// IEEE 24765: "threshold breach (smooth)"; A-L-R: error → manifest.
257    EnvelopeBreach,
258}
259
260/// Semantic disposition from heuristics bank lookup
261#[derive(Copy, Clone, Debug, PartialEq, Eq)]
262pub enum SemanticDisposition {
263    /// Known motif matched in heuristics bank
264    Named(MotifClass),
265    /// Endoductive mode — structure characterized but no named match
266    Unknown,
267}
268
269/// Confidence-bearing motif match result.
270///
271/// Returned by `HeuristicsBank::match_episode_with_confidence`. The
272/// `margin` field is `(top_score - runner_up_score) / top_score`,
273/// clamped to `[0.0, 1.0]`. Operators reading the margin can calibrate
274/// trust:
275///   - margin > 0.5 → top motif clearly dominates; act on it
276///   - margin in (0.2, 0.5] → moderate confidence; surface runner-up
277///   - margin <= 0.2 → top and runner-up are competitive; surface both
278///   - top_score == 0.0 → no motif matched (`disposition == Unknown`)
279#[derive(Copy, Clone, Debug, PartialEq)]
280pub struct MatchConfidence {
281    pub disposition: SemanticDisposition,
282    pub top_score: f64,
283    pub runner_up_score: f64,
284    pub runner_up_motif: Option<MotifClass>,
285    pub margin: f64,
286    /// Phase 3 — fraction of the matched motif's affinity tiers that
287    /// actually fired in the episode range, in [0, 1]. Populated only
288    /// by `match_episode_with_tier_affinity`; the legacy
289    /// `match_episode_with_consensus` leaves this at 0.0. Used by the
290    /// adaptive margin gate (Path 3): when `tier_consensus_factor > 0.5`,
291    /// the gate is halved — strong tier evidence justifies lower
292    /// margin requirement.
293    pub tier_consensus_factor: f64,
294
295    /// Phase 5.6 — explicit confuser motif declared by the matched
296    /// motif's `HeuristicEntry`. May differ from `runner_up_motif` if
297    /// the score-based runner-up is not the declared confuser. `None`
298    /// when no confuser is declared.
299    pub confuser_motif: Option<MotifClass>,
300
301    /// Phase 5.6 — score of the declared confuser computed in the same
302    /// scoring pass as the matched motif. 0.0 if no confuser is
303    /// declared.
304    pub confuser_score: f64,
305
306    /// Phase 5.6 — margin of top motif over its declared confuser:
307    /// `(top_score - confuser_score) / top_score`, clamped to [0, 1].
308    /// Used by fusion's confuser-aware typed-confirmation gate.
309    /// 0.0 if no confuser is declared.
310    pub margin_vs_confuser: f64,
311}
312
313/// Provenance tag — where this interpretation came from
314#[derive(Copy, Clone, Debug, PartialEq, Eq)]
315pub enum Provenance {
316    /// Built into the initial heuristics bank by DSFB design
317    FrameworkDesign,
318    /// Derived from benchmark dataset observation
319    DatasetObserved,
320    /// Confirmed in production deployment
321    FieldValidated,
322}
323
324/// Heuristics bank entry — typed, provenance-aware motif record.
325///
326/// The struct is `Copy + Clone + Debug + PartialEq`; all fields are
327/// stack-only (`'static` string slices, primitives, enums). The bank is
328/// initialised at compile time as an array of `HeuristicEntry`, so
329/// every field must be `const`-friendly.
330///
331/// The seven original fields (`motif_class` … `slew_threshold`) preserve
332/// the v0.1 wire shape. The thirteen additional fields enable
333/// episode-level multi-feature scoring (`match_episode`), per-motif
334/// provenance ladders (`evidence_dataset`, `evidence_dataset_doi`),
335/// production-engineer dashboard hints (`dashboard_hint`), and
336/// taxonomy anchors (`taxonomy_ref`). All are documented in
337/// `docs/heuristics_bank.md`.
338#[derive(Copy, Clone, Debug, PartialEq)]
339pub struct HeuristicEntry {
340    pub motif_class: MotifClass,
341    pub reason_code: ReasonCode,
342    /// NOT an attribution — a candidate interpretation hypothesis
343    pub candidate_interpretation: &'static str,
344    pub provenance: Provenance,
345    pub recommended_action: PolicyState,
346    /// Minimum drift persistence (fraction of window) to trigger
347    pub drift_threshold: f64,
348    /// Minimum slew magnitude to trigger
349    pub slew_threshold: f64,
350
351    // ===== additive fields (Session 3) =====
352    //
353    // Episode-level feature thresholds. The signal-level `lookup`
354    // ignores these (so v0.1 callers see no behaviour change); the new
355    // `match_episode` consults them.
356    /// Minimum boundary-density (fraction of episode windows in
357    /// Boundary state) for this motif to trigger.
358    pub boundary_density_threshold: f64,
359    /// Minimum number of contributing signals (e.g. multi-service motifs
360    /// require ≥ 2). Use 1 for "any".
361    pub min_correlation_count: u16,
362    /// Maximum number of contributing signals (e.g. single-service
363    /// motifs require ≤ 1). Use `u16::MAX` for "unbounded".
364    pub max_correlation_count: u16,
365    /// Minimum episode duration in windows.
366    pub min_duration_windows: u16,
367    /// Maximum episode duration in windows. Use `u16::MAX` for "unbounded".
368    pub max_duration_windows: u16,
369
370    /// Per-motif scoring weights for `match_episode`. Default 1.0
371    /// reproduces the v0.1 unit-weighted behaviour; differential
372    /// weights let one motif emphasise drift while another emphasises
373    /// slew or correlation.
374    pub weight_drift: f64,
375    pub weight_slew: f64,
376    pub weight_boundary: f64,
377    pub weight_correlation: f64,
378    pub weight_duration: f64,
379
380    /// Provenance ladder — which named upstream slice motivated this
381    /// entry. `"FrameworkDesign"` for hand-coded entries with no
382    /// dataset evidence yet; `"<dataset_key>"` (e.g.
383    /// `"tadbench_F04"`, `"aiops_challenge_packet_loss"`) for entries
384    /// observed in a vendored real-data fixture.
385    pub evidence_dataset: &'static str,
386    /// DOI of the upstream archive cited in `evidence_dataset`. Empty
387    /// string when `evidence_dataset == "FrameworkDesign"`.
388    pub evidence_dataset_doi: &'static str,
389
390    /// One-line hint for a production debug engineer reading the
391    /// matched motif on a dashboard. E.g. `"Inspect jvm.memory.heap.used
392    /// + gc.duration over the past hour"`.
393    pub dashboard_hint: &'static str,
394
395    /// Taxonomy anchor — IEEE 24765 term and Avizienis–Laprie–Randell
396    /// node. E.g. `"IEEE 24765: 'fault propagation'; A-L-R: error →
397    /// service-failure"`.
398    pub taxonomy_ref: &'static str,
399
400    /// Phase 2.5 — hand-curated tier-affinity bitmask. Each bit
401    /// corresponds to one detector tier (see `TIER_BIT_*` constants in
402    /// `heuristics_bank.rs`). The bank's
403    /// `match_episode_with_tier_affinity` AND-s this mask against the
404    /// per-cell + per-window tier-fired bitmasks to compute a
405    /// motif-conditional consensus boost. A mask of `0` falls back to
406    /// the reason-code-derived default in `affinity_tiers_for(...)`,
407    /// preserving Phase-2 behaviour for entries without a curated mask.
408    pub affinity_tiers: u32,
409
410    /// Phase 5.6 — confuser-pair adjudication. Names the primary motif
411    /// expected to compete with this one for the same residual signature
412    /// (e.g., DeploymentRegressionSlew's confuser is CircuitBreakerOpenShift —
413    /// both are step-shaped single-service motifs). The bank's match
414    /// function explicitly tracks the confuser's score during scoring,
415    /// reports `margin_vs_confuser`, and fusion gates typing on whether
416    /// the candidate beats its declared confuser by `margin_vs_confuser_threshold`.
417    /// `None` means no confuser is declared — episode types-confirmed
418    /// based on runner-up margin alone (legacy semantics).
419    pub confuser_motif: Option<MotifClass>,
420
421    /// Phase 5.6 — minimum margin against the declared confuser for typed
422    /// confirmation. Episodes that beat the runner-up but not the
423    /// confuser are reported as `confuser_ambiguous` rather than typed.
424    /// Default 0.10 (10% of top score). Set to 0.0 to disable.
425    pub margin_vs_confuser_threshold: f64,
426
427    /// Phase 7 — primary witness tier gate (strict semantic
428    /// anti-hallucination). Subset of `affinity_tiers` that MUST fire
429    /// for the motif to be a valid typing candidate. Distinct from the
430    /// Phase 5.6 zero-tier filter (any affinity tier suffices); this
431    /// requires SPECIFIC tiers to fire. E.g., `DeploymentRegressionSlew`
432    /// witnesses = {A, B, N, X} — without scalar/Page-Hinkley/offline-
433    /// CPD/Pettitt step detection actually firing, the bank refuses to
434    /// type "deployment regression" even if other affinity tiers
435    /// (correlation, dispersion) fired. A mask of `0` disables the gate
436    /// (default — match behaves identically to Phase 5.6).
437    pub primary_witness_tiers: u32,
438
439    /// Phase 8 — per-detector primary witnesses (strict ensemble-methods
440    /// SOTA gate). Named-detector subset that MUST fire for the motif to
441    /// type-confirm. Distinct from `primary_witness_tiers` (any detector
442    /// in the named tiers suffices); this requires SPECIFIC detectors
443    /// (e.g., `[poisson_burst, burst_after_silence]` for
444    /// AuthenticationFailureSpike). Names match the `detector_name`
445    /// field returned by detector functions in `incumbent_baselines.rs`.
446    /// Empty slice `&[]` disables the gate (default — Phase 7 behaviour).
447    /// Applied at fusion.rs typed-confirmation, not at bank match time;
448    /// failing motifs are demoted to ambiguous rather than skipped, so
449    /// the runner-up motif can still be reported in the operator packet.
450    pub primary_witness_detectors: &'static [&'static str],
451}
452
453/// Per-signal, per-window DSFB evaluation result.
454/// The atomic unit of the traceability chain.
455#[derive(Copy, Clone, Debug, PartialEq)]
456pub struct SignalEvaluation {
457    pub window_index: u64,
458    pub signal_index: u16,
459    pub residual_value: f64,
460    pub sign_tuple: SignTuple,
461    pub raw_grammar_state: GrammarState,
462    /// After hysteresis confirmation (n_confirm=2)
463    pub confirmed_grammar_state: GrammarState,
464    pub reason_code: ReasonCode,
465    pub motif: Option<MotifClass>,
466    pub semantic_disposition: SemanticDisposition,
467    pub dsa_score: f64,
468    pub policy_state: PolicyState,
469    /// Missingness-aware flag: if true, drift=0, slew=0, grammar=Admissible
470    pub was_imputed: bool,
471    /// Drift persistence at this evaluation (fraction of last `drift_window`
472    /// windows with positive drift). Persisted so that episode-level
473    /// `match_episode` can average across an episode's window range
474    /// without recomputing norm histories.
475    pub drift_persistence: f64,
476}
477
478/// Drift direction in an episode's structural signature
479#[derive(Copy, Clone, Debug, PartialEq, Eq)]
480pub enum DriftDirection {
481    Positive,
482    Negative,
483    Oscillatory,
484    None,
485}
486
487/// Structural signature of an episode
488#[derive(Copy, Clone, Debug, PartialEq)]
489pub struct StructuralSignature {
490    pub dominant_drift_direction: DriftDirection,
491    pub peak_slew_magnitude: f64,
492    pub duration_windows: u64,
493    pub signal_correlation: f64,
494}
495
496/// A debugging episode — the Trace Event Collapse output.
497/// Paper §7: "the primary developer-facing delta"
498#[derive(Copy, Clone, Debug, PartialEq)]
499pub struct DebugEpisode {
500    pub episode_id: u32,
501    pub start_window: u64,
502    pub end_window: u64,
503    pub peak_grammar_state: GrammarState,
504    pub primary_reason_code: ReasonCode,
505    pub matched_motif: SemanticDisposition,
506    pub policy_state: PolicyState,
507    pub contributing_signal_count: u16,
508    pub structural_signature: StructuralSignature,
509    /// Most-upstream contributing signal in the service-call graph,
510    /// if a graph was supplied to `run_evaluation_with_graph` and a
511    /// unique upstream root could be determined; else `None`.
512    /// Populated by `causality::attribute_root_cause`.
513    pub root_cause_signal_index: Option<u16>,
514}
515
516/// NIST 800-53 AU-3 compliant audit record
517/// Fields: what (event_type), when (window_index), where (signal_index),
518///         source (source), outcome (outcome)
519#[derive(Copy, Clone, Debug, PartialEq, Eq)]
520pub struct AuditRecord {
521    pub event_type: AuditEventType,
522    pub window_index: u64,
523    pub signal_index: u16,
524    pub source: AuditSource,
525    pub outcome: PolicyState,
526}
527
528#[derive(Copy, Clone, Debug, PartialEq, Eq)]
529pub enum AuditEventType {
530    GrammarStateTransition,
531    EpisodeOpened,
532    EpisodeClosed,
533    PolicyEscalation,
534    MotifMatched,
535    EndoductiveUnknown,
536}
537
538#[derive(Copy, Clone, Debug, PartialEq, Eq)]
539pub enum AuditSource {
540    GrammarEvaluator,
541    EpisodeAggregator,
542    PolicyEngine,
543    HeuristicsBank,
544}
545
546/// Benchmark metrics — the paper's headline numbers
547#[derive(Copy, Clone, Debug, PartialEq)]
548pub struct BenchmarkMetrics {
549    pub dataset_name: &'static str,
550    pub total_windows: u64,
551    pub total_signals: u16,
552    pub raw_anomaly_count: u64,
553    pub dsfb_episode_count: u64,
554    /// Review Surface Compression Ratio = raw / episodes
555    pub rscr: f64,
556    /// Fraction of episodes preceding labeled faults within W_pred
557    pub episode_precision: f64,
558    /// Fraction of labeled faults captured by at least one episode
559    pub fault_recall: f64,
560    pub investigation_load_raw: u64,
561    pub investigation_load_dsfb: u64,
562    pub investigation_load_reduction_pct: f64,
563    /// Negative control: false episode rate in clean windows
564    pub clean_window_false_episode_rate: f64,
565}