Architecture · Part 07
Decision Engine
How one daily record becomes a recommendation: every weight and threshold on this page is read from shipped code. Three properties define the design — additive legible scoring, strain as the spine, and floors (never sums) for advice.
The pipeline
Additive, legible scoring — every contribution is a named integer, so drivers[] is literally the score decomposition; that is why every card can explain itself. Strain is the spine — computed once, propagated into capacity (−0.25×) and crash risk (+0.4×). Floors for advice — fueling uses Math.max everywhere: ten reasons to raise protein still yield “3”, never “30”.
Interpretation layer — derived signals
| Signal | Definition (shipped) |
|---|---|
isHardSession | activity ∈ hard set (HIIT, Gym, Climbing, Hyrox…) OR any non-rest activity felt Hard / Harder than expected / ⚡ Peak |
| Effective Sleep Quality | sleepq ± tag deltas, capped [+1.5, −2]: 2-4am wake / Fragmented / Night sweats / Woke up tired −1 · Overheating −0.5 · refreshed/energized +1 |
symptomBurden | 0–3, Σ tag weights (Crash day 3 · Afternoon crash 2.5 · Fatigue 2 · Sore body / Low energy / Hot flash 1.5 …) |
periodBurden | 0–5: bleed load (Spotting .25 → Heavy 2) + period-symptom loads |
isCrashDay | 'Crash day' logged, OR energy = 1 AND ≥2 of six corroborating indicators |
overnightTrend | wearable HR/HRV · ≥3 nights · MAD outlier filtering · baseline-half vs recent-half · HRV weighted 2× · no single-night conclusions · raw numbers never leave the module |
patternConfidence | cycle-length consistency (≥4 valid lengths = high), downgraded by long-running cycle, <5 entries → low, <15 caps at medium, missing sleep caps at medium |
The three scores
| Score | Composition (headline weights) |
|---|---|
| Recovery Strain (0–100) | hard session 1/2/3 days ago +35/+25/+15 · ≥2 sessions in 48h +15 (incl. wearable-detected workout days when no manual log) · 'Had to stop' +20 · logged sleep <6h +20 · sleep-tag disruptors ≤+24 · period symptoms ≤+30 · heat +10 · symptom tags ≤+25 (boosters negative) · wearable +6…+8 only below strain 60 |
| Recovery Capacity (base 70) | sleep ±15 · ESQ ±12 · strain × −0.25 · logged energy +10/−20 (highest single weight) · fog ±15 · fatigue −15 · Crash day −30 · period syms capped −15 · symptom tags capped −35/+15 · 7-day energy trend ±15 · wearable ±5–8 with manual override |
| Crash Risk (0–100) | strain × 0.4 · sleep <6h +25 · energy ≤2 +20 · crash logged +25 · cycle capped +10 (secondary only) · personal history: same-phase crashes +20, same training+sleep pattern before a crash +35 — the largest weight in the engine |
What missing data does — and doesn't do
Recommendation mapping
A logged crash bypasses both scores — symptoms always win. Below 5 entries or 2 period starts the engine returns the learning state instead of scores. The basedOnText paragraph assembles Observation → Interpretation → Recommendation, with cycle phrasing hedged by confidence.
Fueling — floors by priority
| Priority | Signal | Floors (C/P/E, 1–3) |
|---|---|---|
| 1 | Training load high / veryHigh (correct 0–2-day windows) | C3 P2 / C3 P3 · E2–3 with sweat or night-sweats |
| 2 | Logged sleep <6h (<5h) | C2 P2 E2 (C3) |
| 3 | Capacity <30 / <50 | C3 P3 E2 / C3 P2 E2 |
| 4 | Crash risk ≥70 / ≥45 | C3 P3 E2–3 / C2–3 P2 |
| 5 | Fatigue · heavy bleeding | C2–3 · P2 E2 |
| 6 | Symptom flags | protein: Sore body, Heavy legs, flu-like · electrolytes: Hot flash, Night sweats, cramps family, Diarrhea→E3 · carbs: Low energy, fatigue family |
| 7 | Appetite signals | cravings → C2–3 · low appetite → P2 (protein-first when eating is hard) |
| 8 (lowest) | Cycle phase | skipped entirely when patternConfidence = low |
Comparative engines
| Engine | Mechanics |
|---|---|
| What Changed | recomputes capacity for today and yesterday · headline 'Capacity ↓N' or 'No significant changes' when |Δ|<5 and no threshold crossed · changes capped at 3 · one primary driver |
| What Next | similarity score vs every past day: capacity state 35 · strain 20 · sleep 15 · training 10 · energy 10 · risk 8 · cycle booster 7 — threshold 45 · day-after outcomes aggregate into chips · population fallback with hedged copy at low confidence |
Override hierarchy
| Enforcement point | Mechanism in code |
|---|---|
| Crash dominance | Crash day → capacity −30, recommendation bypass, other reducer tags dominated to 20% of their strain |
| Manual beats wearable | energy ≥4 or 'Woke up refreshed/energized' blocks negative wearable adjustments; manual fatigue blocks positive ones; wearable never a primary driver, never adds above strain 60 |
| Cycle as context | ≤ +10 risk, secondary tier only · hedged language below high confidence · removed entirely in irregular / late_peri modes |
| Dedup guards | Brain-fog tag ≈ 0 when fog scale set · Low energy halves when Fatigue present |
Worked example — the brief's case
5.5h logged sleep · Sore body · CD 25 (late luteal) · hard gym session yesterday.
| Stage | Computation | Result |
|---|---|---|
| Strain | hard recent +35 · sleep <6h +20 · Sore body +8 | 63 |
| Capacity | 70 −15 (sleep) −16 (strain×.25) +5 (no fatigue) −6 (Sore body) | 38 → Limited |
| Crash risk | strain×.4 = 25 (primary: Training load) · sleep +25 (primary) · CD 25 +10 (secondary) · Sore body +5 | 65 |
| Recommendation | risk ≥45 ∧ capacity <50 | “Recover first” |
| Fueling | load high → C3 · sleep + capacity floors → P2 E2 | C●●● P●● E●● |
Personal history flips tiers
Engine observation register
#1 · Units mismatch in dailyFocus recency windows — resolved 2026-06-12
daysBetween() returns days but was compared against 24/48/72 under “within last 24h/48h/72h” labels — so a hard session within ~24 days added +35 strain and the +8 rest bonus required 72 hard-session-free days. Fixed to day units (dAgo ≤ 1/2/3) in the strain, capacity, and crash-risk windows; signal labels unchanged; dailyFocus and fuelingEngine now agree on recency. One playbook ordering test re-pinned (a tie-order artefact of the inflated windows); suite green at 281/281. Practical effect: “Train normally” is reachable again for regularly-training users, and the drawer no longer cites weeks-old sessions as “within last 24h”.| # | Observation | Status |
|---|---|---|
| 2 | HARD_TRAINING set includes personal activity names (Volley class, Acroyoga, Hyrox) — personal-vocabulary-aware by listing, not learning | as designed; scaling note |
| 3 | Primary/secondary driver split is the UI contract for driver ordering — new contributions must declare a tier | convention |