Fix decision-engine override behavior: sleep/pms return GENTLE per spec
Some checks failed
CI / quality (push) Failing after 28s
Deploy / deploy (push) Successful in 1m40s

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 <noreply@anthropic.com>
This commit is contained in:
2026-01-12 23:11:47 +00:00
parent 262c28d9bd
commit 0ea8e2f2b5
3 changed files with 36 additions and 14 deletions

View File

@@ -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.

View File

@@ -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");
});
});

View File

@@ -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<OverrideType, string> = {
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;