From 0ea8e2f2b5dace5b3f7147e11ffb4566ae831d27 Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Mon, 12 Jan 2026 23:11:47 +0000 Subject: [PATCH] Fix decision-engine override behavior: sleep/pms return GENTLE per spec The spec (decision-engine.md lines 93-94) clearly states: - sleep override -> GENTLE - pms override -> GENTLE But the implementation was returning REST for all overrides. This fix: - Updates decision-engine.ts to use OVERRIDE_DECISIONS with correct status/reason/icon per override type - Updates tests to expect GENTLE for sleep and pms overrides - Aligns implementation with specification Co-Authored-By: Claude Opus 4.5 --- IMPLEMENTATION_PLAN.md | 1 + src/lib/decision-engine.test.ts | 12 +++++------ src/lib/decision-engine.ts | 37 ++++++++++++++++++++++++++------- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 7ed0f04..0e11393 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -1104,6 +1104,7 @@ These items were identified during gap analysis and have been completed. ### Previously Fixed Issues +- [x] ~~**Decision Engine Override Inconsistency**~~ - FIXED. The decision engine implementation had an inconsistency with the spec for sleep and pms overrides. According to specs/decision-engine.md lines 93-94, sleep and pms overrides should return GENTLE status, but the implementation was incorrectly returning REST for all overrides (flare, stress, sleep, pms). Updated `getDecisionWithOverrides()` in `src/lib/decision-engine.ts` to return the correct status: flare β†’ REST, stress β†’ REST, sleep β†’ GENTLE, pms β†’ GENTLE. This aligns the implementation with the specification. - [x] ~~**CRITICAL: Cycle phase boundaries hardcoded for 31-day cycle**~~ - FIXED. Phase boundaries were not scaling with cycle length. The spec (cycle-tracking.md) defines formulas: MENSTRUAL 1-3, FOLLICULAR 4-(cycleLength-16), OVULATION (cycleLength-15)-(cycleLength-14), EARLY_LUTEAL (cycleLength-13)-(cycleLength-7), LATE_LUTEAL (cycleLength-6)-cycleLength. Added `getPhaseBoundaries(cycleLength)` function and updated `getPhase()` to accept cycleLength parameter. Updated all callers (API routes, components) to pass cycleLength. Added 13 new tests. - [x] ~~ICS emojis did not match calendar.md spec~~ - FIXED. Changed from colored circles (πŸ”΅πŸŸ’πŸŸ£πŸŸ‘πŸ”΄) to thematic emojis (πŸ©ΈπŸŒ±πŸŒΈπŸŒ™πŸŒ‘) per spec. - [x] ~~ICS missing CATEGORIES field for calendar app colors~~ - FIXED. Added CATEGORIES field per calendar.md spec: MENSTRUAL=Red, FOLLICULAR=Green, OVULATION=Pink, EARLY_LUTEAL=Yellow, LATE_LUTEAL=Orange. Added 5 new tests. diff --git a/src/lib/decision-engine.test.ts b/src/lib/decision-engine.test.ts index fc8ee9b..9f0ea2d 100644 --- a/src/lib/decision-engine.test.ts +++ b/src/lib/decision-engine.test.ts @@ -128,7 +128,7 @@ describe("getTrainingDecision (algorithmic rules)", () => { }); describe("getDecisionWithOverrides", () => { - describe("override types force REST", () => { + describe("override types force appropriate decisions", () => { it("flare override forces REST", () => { const data = createHealthyData(); const overrides: OverrideType[] = ["flare"]; @@ -145,20 +145,20 @@ describe("getDecisionWithOverrides", () => { expect(result.reason).toContain("stress"); }); - it("sleep override forces REST", () => { + it("sleep override forces GENTLE (per spec)", () => { const data = createHealthyData(); const overrides: OverrideType[] = ["sleep"]; const result = getDecisionWithOverrides(data, overrides); - expect(result.status).toBe("REST"); + expect(result.status).toBe("GENTLE"); expect(result.reason).toContain("sleep"); }); - it("pms override forces REST", () => { + it("pms override forces GENTLE (per spec)", () => { const data = createHealthyData(); const overrides: OverrideType[] = ["pms"]; const result = getDecisionWithOverrides(data, overrides); - expect(result.status).toBe("REST"); - expect(result.reason).toContain("pms"); + expect(result.status).toBe("GENTLE"); + expect(result.reason).toContain("PMS"); }); }); diff --git a/src/lib/decision-engine.ts b/src/lib/decision-engine.ts index cc32aca..cf04545 100644 --- a/src/lib/decision-engine.ts +++ b/src/lib/decision-engine.ts @@ -6,11 +6,31 @@ import type { DailyData, Decision, OverrideType } from "@/types"; // Override priority order - checked before algorithmic rules const OVERRIDE_PRIORITY: OverrideType[] = ["flare", "stress", "sleep", "pms"]; -const OVERRIDE_REASONS: Record = { - flare: "Hashimoto's flare - rest required", - stress: "High stress override - rest required", - sleep: "Poor sleep override - rest required", - pms: "pms override - rest required", +// Override decisions per spec: flare/stress -> REST, sleep/pms -> GENTLE +const OVERRIDE_DECISIONS: Record< + OverrideType, + { status: "REST" | "GENTLE"; reason: string; icon: string } +> = { + flare: { + status: "REST", + reason: "Hashimoto's flare - rest required", + icon: "πŸ›‘", + }, + stress: { + status: "REST", + reason: "High stress override - rest required", + icon: "πŸ›‘", + }, + sleep: { + status: "GENTLE", + reason: "Poor sleep override - gentle activity only", + icon: "🟑", + }, + pms: { + status: "GENTLE", + reason: "PMS override - gentle activity only", + icon: "🟑", + }, }; export function getTrainingDecision(data: DailyData): Decision { @@ -81,10 +101,11 @@ export function getDecisionWithOverrides( // Check overrides first, in priority order: flare > stress > sleep > pms for (const override of OVERRIDE_PRIORITY) { if (overrides.includes(override)) { + const overrideDecision = OVERRIDE_DECISIONS[override]; const decision: Decision = { - status: "REST", - reason: OVERRIDE_REASONS[override], - icon: "πŸ›‘", + status: overrideDecision.status, + reason: overrideDecision.reason, + icon: overrideDecision.icon, }; decisionEngineCallsTotal.inc({ decision: decision.status }); return decision;