mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
feat(cycle): symptom-by-phase patterns on the insights tab
Add the cycle-native counterpart to the phase×vitals crosstab: for each logged symptom, where in the cycle it clusters. A pure aggregator tallies each symptom's occurrences across the phase-labelled days (the same phase map the calendar uses), surfaces only symptoms logged on at least three phased days, and reports per-phase counts + the dominant phase + its share. The insights tab renders it as a proportional phase-hued bar with the dominant phase named in words (never colour-only), observational and never causal. Also correct the phase-crosstab OpenAPI row enum, which had drifted behind the runtime (mood + glucose channels were missing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d2da8d2657
commit
eec3557137
14 files changed
+447
-7
No files matched your search
@@ -2426,8 +2426,19 @@ const cyclePhaseCrosstabRow = z
|
||||
"basalBodyTemp",
|
||||
"wristTemperature",
|
||||
"skinTemperature",
|
||||
"bloodGlucose",
|
||||
"mood",
|
||||
]),
|
||||
display: z.enum([
|
||||
"hours",
|
||||
"steps",
|
||||
"bpm",
|
||||
"ms",
|
||||
"kg",
|
||||
"celsius",
|
||||
"glucose",
|
||||
"mood",
|
||||
]),
|
||||
display: z.enum(["hours", "steps", "bpm", "ms", "kg", "celsius"]),
|
||||
lutealDays: z.number().int(),
|
||||
follicularDays: z.number().int(),
|
||||
lutealAvg: z.number(),
|
||||
@@ -2460,6 +2471,32 @@ const cyclePhaseLaggedPair = z
|
||||
"One FDR-surviving lagged-Pearson pair from the continuous CYCLE_PHASE ordinal × outcome matrix (mechanism B). Descriptive, never causal.",
|
||||
});
|
||||
|
||||
const cyclePhaseEnumForCount = z.enum([
|
||||
"MENSTRUAL",
|
||||
"FOLLICULAR",
|
||||
"OVULATORY",
|
||||
"LUTEAL",
|
||||
]);
|
||||
|
||||
const cycleSymptomPhaseRow = z
|
||||
.object({
|
||||
symptomKey: z.string(),
|
||||
counts: z.object({
|
||||
MENSTRUAL: z.number().int(),
|
||||
FOLLICULAR: z.number().int(),
|
||||
OVULATORY: z.number().int(),
|
||||
LUTEAL: z.number().int(),
|
||||
}),
|
||||
total: z.number().int(),
|
||||
topPhase: cyclePhaseEnumForCount,
|
||||
topShare: z.number(),
|
||||
})
|
||||
.meta({
|
||||
id: "CycleSymptomPhaseRow",
|
||||
description:
|
||||
"Where a logged symptom clusters across the cycle phases. Surfaced only once logged on ≥3 phase-labelled days. Observational, never causal.",
|
||||
});
|
||||
|
||||
const cycleInsightsResponse = z.object({
|
||||
rows: z.array(cyclePhaseCrosstabRow),
|
||||
headline: cyclePhaseCrosstabRow.nullable(),
|
||||
@@ -2469,6 +2506,7 @@ const cycleInsightsResponse = z.object({
|
||||
fdrQ: z.number(),
|
||||
minPairs: z.number().int(),
|
||||
}),
|
||||
symptomPatterns: z.array(cycleSymptomPhaseRow),
|
||||
contrast: z.object({
|
||||
high: z.literal("LUTEAL"),
|
||||
low: z.literal("FOLLICULAR"),
|
||||
|
||||
Reference in new issue
Block a user