# Composing Primitives
How to combine nexus-stats primitives to build complex monitoring systems
from simple parts.
## Principle: Separate Concerns
Each primitive answers one question. Complex monitoring answers multiple
questions by composing primitives, not by finding one primitive that does
everything.
```
┌──────────────┐
│ Raw Signal │
└──────┬───────┘
│
▼
┌──────────────┐ ┌──────────────┐
│ MultiGate │────▶│ Rejected │ (filter bad data)
│ (quality) │ │ log/drop │
└──────┬───────┘ └──────────────┘
│ Accept/Unusual
├───────────────────────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ EMA │ │ Welford │ (statistics)
│ (smoothed) │ │ (mean/var) │
└──────┬───────┘ └──────────────┘
│
▼
┌──────────────┐ ┌──────────────┐
│ CUSUM │────▶│ Alert │ (change detection)
│ (shifts) │ │ handler │
└──────────────┘ └──────────────┘
```
## Common Patterns
### Filter → Track → Detect
The most common composition. Filter bad data first, track statistics on
clean data, detect anomalies in the statistics.
```rust
// 1. Filter
let verdict = gate.update(sample);
ema.update(sample);
stats.update(sample);
// 3. Detect
cusum.update(sample);
}
```
### Dual-EMA Crossover
Two EMAs at different speeds detect trend changes when the fast one
crosses the slow one.
```rust
let mut fast = EmaF64::builder().span(5).build().unwrap();
let mut slow = EmaF64::builder().span(50).build().unwrap();
if let (Some(f), Some(s)) = (fast.update(x), slow.update(x)) {
if f > s { /* uptrend */ }
else { /* downtrend */ }
}
```
### CUSUM + WindowedMedian for Auto-Baseline
When CUSUM detects a shift, use WindowedMedian to compute the new
baseline, then reset CUSUM:
```rust
if let Some(Direction::Rising) = cusum.update(sample) {
if let Some(new_base) = median.median() {
cusum.reset_with_target(new_base);
}
}
```
### Multi-Signal Health Score
Combine several bool-returning monitors into a composite:
```rust
let alive = liveness.check(now);
let queue_ok = !matches!(qd.update(now, sojourn), Some(Condition::Degraded));
let errors_ok = !matches!(error_rate.record(ok), Some(Condition::Degraded));
let health_score = [alive, queue_ok, errors_ok]
.iter().filter(|&&b| b).count();
match health_score {
3 => Status::Healthy,
2 => Status::Warning,
_ => Status::Critical,
}
```
## Anti-Patterns
### Don't smooth before detecting
Feeding CUSUM an EMA-smoothed signal delays detection. CUSUM works best
on raw (filtered but unsmoothed) data.
### Don't use all-time stats for live alerting
`Welford::mean()` after 1 million samples barely moves when the last
100 samples shift. Use windowed or exponentially-weighted variants.
### Don't cascade detectors redundantly
CUSUM and AdaptiveThreshold both detect mean shifts. Using both on the
same signal doesn't add information — pick the one that fits your model.