Skip to content

Commit b535eae

Browse files
authored
Implement illness suspicion evidence surface
1 parent 9104d08 commit b535eae

17 files changed

Lines changed: 2072 additions & 14 deletions

cmd/illness_probe/main.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"flag"
7+
"fmt"
8+
"log"
9+
"os"
10+
11+
"health-receiver/internal/health"
12+
"health-receiver/internal/storage"
13+
)
14+
15+
func main() {
16+
schema := flag.String("schema", "health", "tenant schema")
17+
lang := flag.String("lang", "en", "briefing language")
18+
date := flag.String("date", "", "specific local date YYYY-MM-DD; defaults to latest briefing")
19+
days := flag.Int("days", 0, "retrospective sweep over recent daily_scores dates")
20+
noCheckin := flag.Bool("no-checkin", false, "ignore subjective check-in evidence")
21+
flag.Parse()
22+
23+
db, err := storage.NewWithSchema(context.Background(), os.Getenv("DATABASE_URL"), *schema)
24+
if err != nil {
25+
log.Fatal(err)
26+
}
27+
defer db.Close()
28+
29+
if *days > 0 {
30+
runSweep(db, *schema, *days, !*noCheckin)
31+
return
32+
}
33+
34+
if *date != "" {
35+
printDate(db, *schema, *date, !*noCheckin)
36+
return
37+
}
38+
39+
briefing, err := db.GetHealthBriefing(*lang)
40+
if err != nil {
41+
log.Fatal(err)
42+
}
43+
out := map[string]any{
44+
"schema": *schema,
45+
"date": briefing.Date,
46+
"readiness_band": briefing.ReadinessBand,
47+
"energy_verdict": nil,
48+
"illness_suspicion": briefing.IllnessSuspicion,
49+
"subjective_checkin": briefing.SubjectiveCheckin,
50+
}
51+
if briefing.EnergyBank != nil {
52+
out["energy_verdict"] = briefing.EnergyBank.ActionVerdict
53+
}
54+
printJSON(out)
55+
}
56+
57+
func printDate(db *storage.DB, schema, date string, includeCheckin bool) {
58+
var checkin *health.SubjectiveCheckinSummary
59+
if includeCheckin {
60+
checkin = checkinForDate(db, date)
61+
}
62+
ill := db.BuildIllnessEvidenceInput(date, checkin)
63+
out := map[string]any{
64+
"schema": schema,
65+
"date": date,
66+
"illness_suspicion": health.ComputeIllnessSuspicion(ill),
67+
"subjective_checkin": checkin,
68+
}
69+
printJSON(out)
70+
}
71+
72+
func runSweep(db *storage.DB, schema string, days int, includeCheckin bool) {
73+
if days < 1 {
74+
days = 1
75+
}
76+
if days > 3650 {
77+
days = 3650
78+
}
79+
rows, err := db.QueryReadOnly(fmt.Sprintf("SELECT date FROM daily_scores ORDER BY date DESC LIMIT %d", days))
80+
if err != nil {
81+
log.Fatal(err)
82+
}
83+
counts := map[string]int{"none": 0, "low": 0, "moderate": 0, "high": 0}
84+
type hit struct {
85+
Date string `json:"date"`
86+
Confidence string `json:"confidence"`
87+
Signals []string `json:"signals"`
88+
}
89+
var hits []hit
90+
for _, row := range rows {
91+
date, _ := row["date"].(string)
92+
if date == "" {
93+
continue
94+
}
95+
var checkin *health.SubjectiveCheckinSummary
96+
if includeCheckin {
97+
checkin = checkinForDate(db, date)
98+
}
99+
ill := health.ComputeIllnessSuspicion(db.BuildIllnessEvidenceInput(date, checkin))
100+
counts[ill.Confidence]++
101+
if ill.Confidence == "moderate" || ill.Confidence == "high" {
102+
h := hit{Date: date, Confidence: ill.Confidence}
103+
for _, sig := range ill.Signals {
104+
if sig.Status == "ok" && sig.Strength != "weak" {
105+
h.Signals = append(h.Signals, sig.Metric+":"+sig.Strength)
106+
}
107+
}
108+
hits = append(hits, h)
109+
}
110+
}
111+
printJSON(map[string]any{"schema": schema, "days": days, "counts": counts, "top_hits": hits})
112+
}
113+
114+
func checkinForDate(db *storage.DB, date string) *health.SubjectiveCheckinSummary {
115+
row, err := db.GetTodayCheckin(date, storage.CheckinSourceTelegram)
116+
if err != nil || row == nil {
117+
return nil
118+
}
119+
return &health.SubjectiveCheckinSummary{Status: row.Status, Answer: row.Answer}
120+
}
121+
122+
func printJSON(v any) {
123+
data, err := json.MarshalIndent(v, "", " ")
124+
if err != nil {
125+
log.Fatal(err)
126+
}
127+
fmt.Println(string(data))
128+
}

docs/ai-plans/2026-06-01-illness-suspicion-mcp-evidence.html

Lines changed: 548 additions & 0 deletions
Large diffs are not rendered by default.

internal/health/i18n_en.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ var en = LangStrings{
191191
"energy_reason_illness_signature": "Temperature, breathing rate and HRV all point to your body fighting something — rest is the recovery move today.",
192192
"energy_reason_recovery_debt": "Yesterday's load showed up overnight (HRV down, RHR up) — keep today light to clear the debt before pushing again.",
193193
"energy_reason_rebound_addon": "Note: HR was elevated but HRV is also above baseline — that's a recovery-phase pattern, not acute stress.",
194+
"energy_reason_illness_suspicion_moderate": "Illness-like respiratory/autonomic signals are present, so today's recommendation is capped at active recovery.",
195+
"energy_reason_illness_suspicion_high": "Multiple illness-like signals are present, so today's recommendation is capped at rest.",
194196

195197
// v2.2 hero-row stress-flag chips.
196198
"stress_flags_aria": "Stress signal flags",

internal/health/i18n_ru.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ var ru = LangStrings{
165165
"energy_reason_illness_signature": "Температура, частота дыхания и HRV — все три указывают, что тело борется с инфекцией. Сегодня покой лучше тренировки.",
166166
"energy_reason_recovery_debt": "Вчерашняя нагрузка догнала ночью (HRV ↓, RHR ↑) — держите день лёгким, чтобы вернуть долг.",
167167
"energy_reason_rebound_addon": "Примечание: пульс был повышен, но HRV выше нормы — это паттерн фазы восстановления, не острый стресс.",
168+
"energy_reason_illness_suspicion_moderate": "Есть респираторные/вегетативные сигналы, похожие на болезнь, поэтому рекомендация ограничена активным восстановлением.",
169+
"energy_reason_illness_suspicion_high": "Несколько сигналов похожи на болезнь, поэтому рекомендация ограничена отдыхом.",
168170

169171
// v2.2 hero-row stress-flag chips.
170172
"stress_flags_aria": "Флаги стресс-сигналов",

internal/health/i18n_sr.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ var sr = LangStrings{
165165
"energy_reason_illness_signature": "Temperatura, frekvencija disanja i HRV — sve tri govore da telo bori infekciju. Danas je odmor pravi izbor.",
166166
"energy_reason_recovery_debt": "Jučerašnje opterećenje stiglo je noću (HRV ↓, RHR ↑) — držite danas lakim da se vrati dug pre nego što opet pritisnete.",
167167
"energy_reason_rebound_addon": "Napomena: HR je bio povišen, ali je HRV iznad norme — to je obrazac faze oporavka, ne akutni stres.",
168+
"energy_reason_illness_suspicion_moderate": "Respiratorni/autonomni signali liče na bolest, zato je današnja preporuka ograničena na aktivni oporavak.",
169+
"energy_reason_illness_suspicion_high": "Više signala liči na bolest, zato je današnja preporuka ograničena na odmor.",
168170

169171
// v2.2 hero-row stress-flag chips.
170172
"stress_flags_aria": "Oznake stres signala",

0 commit comments

Comments
 (0)