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
|
||||
|
||||
- [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.
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user