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 <noreply@anthropic.com>
This commit is contained in:
@@ -1104,6 +1104,7 @@ These items were identified during gap analysis and have been completed.
|
|||||||
|
|
||||||
### Previously Fixed Issues
|
### 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] ~~**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 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.
|
- [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.
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ describe("getTrainingDecision (algorithmic rules)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("getDecisionWithOverrides", () => {
|
describe("getDecisionWithOverrides", () => {
|
||||||
describe("override types force REST", () => {
|
describe("override types force appropriate decisions", () => {
|
||||||
it("flare override forces REST", () => {
|
it("flare override forces REST", () => {
|
||||||
const data = createHealthyData();
|
const data = createHealthyData();
|
||||||
const overrides: OverrideType[] = ["flare"];
|
const overrides: OverrideType[] = ["flare"];
|
||||||
@@ -145,20 +145,20 @@ describe("getDecisionWithOverrides", () => {
|
|||||||
expect(result.reason).toContain("stress");
|
expect(result.reason).toContain("stress");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sleep override forces REST", () => {
|
it("sleep override forces GENTLE (per spec)", () => {
|
||||||
const data = createHealthyData();
|
const data = createHealthyData();
|
||||||
const overrides: OverrideType[] = ["sleep"];
|
const overrides: OverrideType[] = ["sleep"];
|
||||||
const result = getDecisionWithOverrides(data, overrides);
|
const result = getDecisionWithOverrides(data, overrides);
|
||||||
expect(result.status).toBe("REST");
|
expect(result.status).toBe("GENTLE");
|
||||||
expect(result.reason).toContain("sleep");
|
expect(result.reason).toContain("sleep");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("pms override forces REST", () => {
|
it("pms override forces GENTLE (per spec)", () => {
|
||||||
const data = createHealthyData();
|
const data = createHealthyData();
|
||||||
const overrides: OverrideType[] = ["pms"];
|
const overrides: OverrideType[] = ["pms"];
|
||||||
const result = getDecisionWithOverrides(data, overrides);
|
const result = getDecisionWithOverrides(data, overrides);
|
||||||
expect(result.status).toBe("REST");
|
expect(result.status).toBe("GENTLE");
|
||||||
expect(result.reason).toContain("pms");
|
expect(result.reason).toContain("PMS");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,31 @@ import type { DailyData, Decision, OverrideType } from "@/types";
|
|||||||
// Override priority order - checked before algorithmic rules
|
// Override priority order - checked before algorithmic rules
|
||||||
const OVERRIDE_PRIORITY: OverrideType[] = ["flare", "stress", "sleep", "pms"];
|
const OVERRIDE_PRIORITY: OverrideType[] = ["flare", "stress", "sleep", "pms"];
|
||||||
|
|
||||||
const OVERRIDE_REASONS: Record<OverrideType, string> = {
|
// Override decisions per spec: flare/stress -> REST, sleep/pms -> GENTLE
|
||||||
flare: "Hashimoto's flare - rest required",
|
const OVERRIDE_DECISIONS: Record<
|
||||||
stress: "High stress override - rest required",
|
OverrideType,
|
||||||
sleep: "Poor sleep override - rest required",
|
{ status: "REST" | "GENTLE"; reason: string; icon: string }
|
||||||
pms: "pms override - rest required",
|
> = {
|
||||||
|
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 {
|
export function getTrainingDecision(data: DailyData): Decision {
|
||||||
@@ -81,10 +101,11 @@ export function getDecisionWithOverrides(
|
|||||||
// Check overrides first, in priority order: flare > stress > sleep > pms
|
// Check overrides first, in priority order: flare > stress > sleep > pms
|
||||||
for (const override of OVERRIDE_PRIORITY) {
|
for (const override of OVERRIDE_PRIORITY) {
|
||||||
if (overrides.includes(override)) {
|
if (overrides.includes(override)) {
|
||||||
|
const overrideDecision = OVERRIDE_DECISIONS[override];
|
||||||
const decision: Decision = {
|
const decision: Decision = {
|
||||||
status: "REST",
|
status: overrideDecision.status,
|
||||||
reason: OVERRIDE_REASONS[override],
|
reason: overrideDecision.reason,
|
||||||
icon: "🛑",
|
icon: overrideDecision.icon,
|
||||||
};
|
};
|
||||||
decisionEngineCallsTotal.inc({ decision: decision.status });
|
decisionEngineCallsTotal.inc({ decision: decision.status });
|
||||||
return decision;
|
return decision;
|
||||||
|
|||||||
Reference in New Issue
Block a user