Skip to content

Commit 0ce735e

Browse files
committed
Clarify EnergyBank v2 cleanup state
1 parent 4a30654 commit 0ce735e

14 files changed

Lines changed: 237 additions & 76 deletions

ENERGY_BANK.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# EnergyBank v2 — Design
1+
# EnergyBank v2 — Current Implementation
22

3-
Status: design, not implemented. v1 (current `internal/health/energy.go`) is a stateless daily snapshot whose strain scale saturates on typical days; v2 replaces it with a Bevel-style continuous battery integrating drain and restore over time, with cross-day carryover.
3+
Status: shipped. EnergyBank v2 stores current bank state in `energy_snapshots`, drives dashboard/report rendering when a same-day snapshot exists, and serves the hourly history chart from those rows. The old stateless daily formula in `internal/health/energy.go` remains only as a legacy fallback for fresh tenants or days before the first v2 snapshot lands.
44

55
## Model
66

@@ -10,8 +10,8 @@ bank[t] = bank[t−1] + restore(t−1 → t) − drain(t−1 → t)
1010

1111
State machine. Each event recomputes the bank for the affected interval and writes a snapshot. No daily reset — yesterday's residual carries through sleep restoration into today's morning capacity.
1212

13-
- **Live current** (dashboard hero, Telegram report): computed on read = latest stored snapshot + delta from `metric_points` since that snapshot. Always reflects "now", independent of write cadence.
14-
- **History** (sparkline, hourly graph in `<details>`): reads `energy_snapshots` directly.
13+
- **Live current** (dashboard hero, Telegram report): read from the latest same-day stored snapshot, then projected into the shared `health.EnergyBank` response shape.
14+
- **History** (hourly graph in `<details>`): reads `energy_snapshots` directly.
1515

1616
## Storage
1717

@@ -59,7 +59,7 @@ bootstrap_tail — within first 3 days after formula_version bump (transient
5959

6060
v2.5+ extends with `calibration_alpha_tuned`, `calibration_alert`, `data_quality_warn` — no schema change required, just new string constants in Go.
6161

62-
`daily_scores.energy_*` columns become a roll-up (last bucket of the day) — no longer the source of truth.
62+
`daily_scores.energy_*` columns are legacy compatibility columns. They are no longer the source of truth and must not be dropped without an explicit reversible migration.
6363

6464
## Cadence — event-driven, not timer
6565

@@ -211,7 +211,7 @@ Distribution of `bank_eod` under asymptotic restore: min=3, p10=17, p25=34, medi
211211

212212
## Phased delivery
213213

214-
**v2.0 — skeleton.** Schema + event-driven recompute + sleep-only restore + UI hourly chart in hero `<details>` + live read with compute-on-read. Constants set to plausible starting values, calibration deferred. Old `EnergyBank` formula and verdict thresholds stay live until v2.0 is validated; AI orchestrator continues reading the daily snapshot field.
214+
**v2.0 — shipped skeleton.** Schema + event-driven recompute + sleep-only restore + UI hourly chart in hero `<details>` + latest-snapshot read. Constants set to plausible starting values, calibration deferred. The old `EnergyBank` formula is retained only as a cold-start fallback.
215215

216216
**v2.1 — calibration.** Verdict thresholds are re-derived from the user's own day-level `energy_snapshots` distribution: one latest eligible snapshot per local date, never raw intraday rows. Mature personal calibration requires 30 distinct eligible dates for the current formula (`personal_latest_formula`) or for explicitly compatible formula versions during warmup (`personal_mixed_formula_warmup`). A guarded provisional mode exists for 20-29 compatible dates (`provisional_compatible_formula_warmup`): it uses the same 180-day window, clamps Rest / Recovery / PushHard so they are not more permissive than defaults, and exposes `UsedDays`, `LatestFormulaDays`, and `CompatibleFormulaDays` so clients do not mistake it for mature calibration. β stays at 0 (the autonomic-load term is a v2.2 piece, not v2.1) — tuning it here against pre-rubric data would fit it to whatever the bank already does, defeating the validation in STRESS_MEASUREMENT.md §4.5.
217217

@@ -267,7 +267,7 @@ With one parameter and 30 observations against an external physiological signal,
267267
- **Per-user calibration tuning** at launch (everyone gets same starting α/β/quality_weight/z_threshold) — but constants live in `settings` table from day one, not as Go consts, so v2.1 can tune per-user without schema migration. β reserves the setting slot but stays at 0 effective until v2.2 ships and the §4.5 validation rubric passes; same for `z_threshold` and `stress_drain_enabled`. "Athlete profile" preset becomes a v2.1+ feature.
268268
- Workout-type detection (drain is HR/kcal driven, not exercise-class aware)
269269
- Real-time push notifications when bank drops below threshold
270-
- Migration of `daily_scores.energy_*` rows — left in place as-is; v2 starts writing alongside; `daily_scores` becomes a derived view of v2 snapshots in v2.1
270+
- Migration or removal of `daily_scores.energy_*` rows — left in place as-is as compatibility columns. Any destructive cleanup needs a separate reversible migration.
271271

272272
## Open implementation questions
273273

@@ -277,7 +277,7 @@ These don't block v2.0 design but need resolution during build:
277277

278278
| State | Trigger | Baseline source | UI |
279279
|---|---|---|---|
280-
| `cold` | `n_nights < 3` overnight RHR samples in last 30d | None — `computeEnergyBank` returns `nil` | "Collecting baseline · 3 nights minimum" placeholder, no bank rendered |
280+
| `cold` | `n_nights < 3` overnight RHR samples in last 30d | None — no v2 bank is rendered; briefing may still use the legacy fallback if its own data gate passes | "Collecting baseline · 3 nights minimum" placeholder, no bank rendered |
281281
| `warmup` | `3 ≤ n_nights < 7` OR newest sample > 14 days old | Simple mean of available samples (EMA α not calibrated for <7 points) | Bank rendered with `calibration_state: "warmup"` flag → frontend badges "calibrating · need N more nights" |
282282
| `steady` | `n_nights ≥ 7` AND newest sample ≤ 14 days old | EMA, α ≈ 0.25 (~7-day effective window) | Bank rendered normally |
283283

SCORING.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -416,13 +416,14 @@ the headline with section verdicts so the briefing is **internally consistent**:
416416
417417
---
418418

419-
## Energy Bank v1 (shipping — to be replaced by v2)
419+
## Energy Bank legacy fallback
420420

421-
> **Status:** v1 is what currently runs in `internal/health/energy.go`. Cross-user empirical validation revealed structural problems (saturation on typical days, no multi-day carryover, formula squashing); a redesign — Energy Bank v2 — is documented in `ENERGY_BANK.md` and summarised in the next section. v1 stays here as the operational reference until v2 ships.
421+
> **Status:** this is the compatibility fallback in `internal/health/energy.go::computeLegacyEnergyBank`. The normal dashboard/report path uses the latest v2 row from `energy_snapshots` when one exists for the requested local date. The legacy formula remains for fresh tenants, cold-start days, and older compatibility history; it is not the source of truth for current Energy Bank methodology.
422422
423-
Computed in `internal/health/energy.go::computeEnergyBank`, returned in
424-
`response.energy_bank`. Where the Headline answers *"what's notable today?"*,
425-
the Energy Bank answers *"what should you do?"* — a prescriptive verdict that
423+
The fallback is returned in `response.energy_bank` only until the storage
424+
briefing path replaces it with the latest v2 snapshot. Where the Headline
425+
answers *"what's notable today?"*, Energy Bank answers *"what should you do?"*
426+
— a prescriptive verdict that
426427
rolls capacity, observed activity load, and autonomic stress into a single
427428
plain-language action ("push_hard", "moderate", "active_recovery", "rest").
428429

@@ -470,8 +471,8 @@ plain-language action ("push_hard", "moderate", "active_recovery", "rest").
470471
The same active day costs more when autonomics are already taxed — this
471472
is the McEwen 1998 / 2024 PNAS allostatic-load shape [[38]](#ref-38),
472473
conservatively parameterised. Without intraday HR we cannot model
473-
parasympathetic *refill*; the system therefore monotonically drains
474-
through the day. Documented as `// TODO(v2)` in `energy.go`.
474+
parasympathetic *refill*; the fallback therefore monotonically drains
475+
through the day. v2 fixes this with cross-day state and sleep restore.
475476

476477
### Verdict thresholds (Plews / Vesterinen smallest-worthwhile-change band)
477478

@@ -514,13 +515,14 @@ is stronger than any single-marker green light.
514515

515516
---
516517

517-
## Energy Bank v2 (design — pending implementation)
518+
## Energy Bank v2 (current shipped path)
518519

519-
> Full specification: [`ENERGY_BANK.md`](./ENERGY_BANK.md). This section captures
520-
> the methodology and the empirically-derived design principles; the spec doc
521-
> carries the implementation detail (schema, concurrency, rollout phases).
520+
> Full specification and operational details: [`ENERGY_BANK.md`](./ENERGY_BANK.md).
521+
> v2 writes `energy_snapshots`, reads the latest local-date snapshot into the
522+
> dashboard/report `energy_bank` field, and serves hourly history from the same
523+
> table. Legacy `daily_scores.energy_*` columns remain compatibility-only.
522524
523-
Three structural problems with v1 were discovered through an empirical
525+
Three structural problems with the legacy fallback were discovered through an empirical
524526
prototype run on 90 days of real data, on **two distinct users** (one
525527
sedentary with clean data, one mixed-source with frequent sync gaps):
526528

cmd/server/main.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,9 @@ func main() {
183183

184184
mux := http.NewServeMux()
185185

186-
// EnergyBank v2 orchestrator: runs in parallel with the v1
187-
// dashboard rendering. Snapshots accumulate silently for
188-
// observation; PR8 will flip the UI over once v2 is validated.
186+
// EnergyBank v2 orchestrator: writes snapshots consumed by the
187+
// dashboard, reports, and history chart. Briefing rendering still
188+
// has a legacy fallback for days before the first v2 snapshot lands.
189189
energyV2 := storage.NewEnergyV2Orchestrator()
190190

191191
onNewData := func(db *storage.DB, dates []string) {
@@ -286,8 +286,7 @@ func runSingleTenant(ctx context.Context, addr, baseURL string, trustFwdAuth boo
286286
var morningSendMu sync.Mutex
287287
maybeFireMorningReport := makeMorningTrigger(db, &morningSendMu, mgr, reg, schema, notifyDefaults)
288288
backfillDatesFn := makeBackfillDatesFn(db, schema, notifyDefaults)
289-
// EnergyBank v2 orchestrator: same role as in multi-tenant mode —
290-
// passive snapshot accumulation alongside the live v1 dashboard.
289+
// EnergyBank v2 orchestrator: same role as in multi-tenant mode.
291290
energyV2 := storage.NewEnergyV2Orchestrator()
292291
onNewData := func(_ *storage.DB, dates []string) {
293292
backfillDatesFn(dates)
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Implementation Plan: Issue #148 EnergyBank v1 Cleanup and Docs</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<style>
8+
body { margin: 0; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f6f7f9; color: #1f2937; line-height: 1.5; }
9+
main { max-width: 1100px; margin: 0 auto; padding: 32px; }
10+
section { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; margin: 16px 0; }
11+
h1, h2 { line-height: 1.2; margin-top: 0; }
12+
code { background: #f3f4f6; padding: 2px 5px; border-radius: 4px; }
13+
.badge { display: inline-block; padding: 4px 10px; border-radius: 999px; background: #dcfce7; color: #166534; font-size: 12px; font-weight: 700; }
14+
.risk { border-left: 4px solid #dc2626; }
15+
.approval { border-left: 4px solid #d97706; }
16+
</style>
17+
</head>
18+
<body>
19+
<main>
20+
<h1>Implementation Plan: Issue #148 EnergyBank v1 Cleanup and Docs</h1>
21+
<p class="badge">Approved and implemented</p>
22+
23+
<section>
24+
<h2>Task Summary</h2>
25+
<p>Audit remaining EnergyBank v1 code and documentation, then either remove obsolete v1 paths or explicitly document them as compatibility fallback. Update scoring docs so they describe the shipped v2 behavior rather than pending design language.</p>
26+
</section>
27+
28+
<section>
29+
<h2>Current Behavior</h2>
30+
<ul>
31+
<li><code>internal/health/energy.go::computeEnergyBank</code> is still called by <code>internal/health/scoring.go</code>.</li>
32+
<li><code>internal/storage/briefing.go</code> computes the health response through the v1 path first, then overrides display bank/capacity/drain/verdict from <code>energy_snapshots</code> when a v2 snapshot exists.</li>
33+
<li><code>internal/storage/energy_history.go::SaveEnergyBankSnapshot</code> still writes legacy <code>daily_scores.energy_*</code> columns; <code>GetEnergyHistory</code> serves the older EOD history path.</li>
34+
<li><code>internal/storage/energy_history_v2.go</code> serves the newer hourly <code>energy_snapshots</code> path.</li>
35+
<li><code>SCORING.md</code> still says EnergyBank is computed in <code>internal/health/energy.go::computeEnergyBank</code>, while <code>ENERGY_BANK.md</code> includes v2 design/cutover history and says old formula stays live in older roadmap text.</li>
36+
</ul>
37+
</section>
38+
39+
<section>
40+
<h2>Desired Behavior</h2>
41+
<ul>
42+
<li>Every remaining v1 reference is either removed or labeled as compatibility/fallback with a current reason.</li>
43+
<li>Docs describe the shipped v2 state machine, snapshot table, personal verdict bands, and legacy daily columns accurately.</li>
44+
<li>No destructive DB cleanup is bundled unless separately approved and reversible.</li>
45+
<li>Dashboard, Telegram, AI prompt, and MCP/debug paths continue to render EnergyBank correctly.</li>
46+
</ul>
47+
</section>
48+
49+
<section>
50+
<h2>Assumptions And Unknowns</h2>
51+
<ul>
52+
<li>Assumption: do not drop <code>daily_scores.energy_*</code> in this PR; keep or deprecate them unless a separate migration is approved.</li>
53+
<li>Assumption: <code>computeEnergyBank</code> may still be needed as a bootstrap/fallback when no v2 snapshot exists.</li>
54+
<li>Unknown: whether the legacy sparkline path is still used by any client after the v2 hourly chart.</li>
55+
<li>Unknown: whether production has enough v2 stability to remove fallback behavior entirely; issue says at least 7 production days without material issues.</li>
56+
</ul>
57+
</section>
58+
59+
<section>
60+
<h2>Files Likely To Change</h2>
61+
<ul>
62+
<li><code>internal/health/energy.go</code> and <code>internal/health/scoring.go</code> - only if v1 fallback can be safely removed or renamed as legacy.</li>
63+
<li><code>internal/storage/briefing.go</code> - clarify or simplify v1-to-v2 override comments and persistence behavior.</li>
64+
<li><code>internal/storage/energy_history.go</code>, <code>internal/storage/energy_history_v2.go</code>, and related UI handlers - audit whether legacy EOD history is still needed.</li>
65+
<li><code>SCORING.md</code>, <code>ENERGY_BANK.md</code>, <code>README.md</code>, <code>AGENTS.md</code>, <code>CLAUDE.md</code> - update current methodology and compatibility notes.</li>
66+
<li><code>internal/health/*energy*_test.go</code> and storage energy tests - update tests only where they pin obsolete v1 behavior.</li>
67+
</ul>
68+
</section>
69+
70+
<section>
71+
<h2>Implementation Steps</h2>
72+
<ol>
73+
<li>Run <code>git grep computeEnergyBank</code>, <code>git grep SaveEnergyBankSnapshot</code>, and <code>git grep energy_</code> to build a final removal/deprecation list.</li>
74+
<li>Trace dashboard, Telegram report, AI prompt, MCP, and chart reads to identify which paths still depend on legacy daily columns.</li>
75+
<li>Decide between two safe paths: docs-only deprecation of v1 fallback, or code removal where no live callsite remains.</li>
76+
<li>Update methodology docs first so reviewers can check intended current behavior.</li>
77+
<li>Apply the smallest code cleanup that matches the audit; avoid schema drops.</li>
78+
<li>Run EnergyBank-focused tests and full Go verification.</li>
79+
</ol>
80+
</section>
81+
82+
<section>
83+
<h2>Impact</h2>
84+
<ul>
85+
<li><strong>Data:</strong> no destructive migration planned; legacy columns remain unless separately approved.</li>
86+
<li><strong>API/UX:</strong> EnergyBank JSON shape should remain compatible.</li>
87+
<li><strong>Docs:</strong> methodology docs become the main output if code still needs v1 fallback.</li>
88+
<li><strong>Deployment:</strong> normal app deploy only if production code changes; docs-only changes need no deploy for runtime behavior.</li>
89+
</ul>
90+
</section>
91+
92+
<section>
93+
<h2>Test Plan</h2>
94+
<ul>
95+
<li><code>go test ./internal/health -run "Test.*Energy|Test.*Verdict" -count=1</code></li>
96+
<li><code>go test ./internal/storage -run "Test.*Energy" -count=1</code></li>
97+
<li><code>go test ./internal/ui -run "Test.*Energy|Test.*Dashboard" -count=1</code> if UI/chart handlers change.</li>
98+
<li><code>go test ./...</code></li>
99+
<li><code>go vet ./...</code></li>
100+
<li>Manual QA if deployed: confirm dashboard hero EnergyBank, hourly chart, and Telegram morning report still render.</li>
101+
</ul>
102+
</section>
103+
104+
<section class="risk">
105+
<h2>Risks And Edge Cases</h2>
106+
<ul>
107+
<li>Removing v1 too early can break cold-start tenants or days with no v2 snapshot.</li>
108+
<li>Docs can become inconsistent if <code>AGENTS.md</code> and <code>CLAUDE.md</code> are not kept in lockstep.</li>
109+
<li>Legacy daily columns may still support old chart or AI history paths; audit before removal.</li>
110+
<li>Changing EnergyBank behavior under a cleanup issue would be out of scope.</li>
111+
</ul>
112+
</section>
113+
114+
<section>
115+
<h2>Rollback Plan</h2>
116+
<p>For docs/test cleanup, revert the PR. For any code removal, restore the legacy fallback callsite and tests. No database rollback is expected because schema drops are excluded from this plan.</p>
117+
</section>
118+
119+
<section>
120+
<h2>Open Questions</h2>
121+
<ul>
122+
<li>Should <code>computeEnergyBank</code> remain as a named legacy fallback until all tenants have v2 snapshots?</li>
123+
<li>Is the old EOD <code>GetEnergyHistory</code> path still needed by any frontend or mobile client?</li>
124+
<li>Do we want a separate future issue for dropping <code>daily_scores.energy_*</code> columns after compatibility expires?</li>
125+
</ul>
126+
</section>
127+
128+
<section class="approval">
129+
<h2>Approval Gate</h2>
130+
<p>Approved by the user on 2026-06-06 before implementation.</p>
131+
</section>
132+
133+
<section>
134+
<h2>Implementation Result</h2>
135+
<ul>
136+
<li>Renamed <code>computeEnergyBank</code> to <code>computeLegacyEnergyBank</code> to make the remaining v1 behavior explicitly compatibility-only.</li>
137+
<li>Kept the legacy fallback behavior unchanged for fresh tenants and days before a v2 snapshot exists.</li>
138+
<li>Updated storage, server, UI, and chart comments that still described v2 as passive, pending, or waiting for a future UI flip.</li>
139+
<li>Updated <code>SCORING.md</code> and <code>ENERGY_BANK.md</code> so v2 is documented as the current shipped path and <code>daily_scores.energy_*</code> columns are documented as legacy compatibility columns.</li>
140+
<li>No schema drops or destructive database cleanup were included.</li>
141+
</ul>
142+
</section>
143+
144+
<section>
145+
<h2>Verification Run</h2>
146+
<ul>
147+
<li><code>gofmt -w</code> on changed Go files.</li>
148+
<li><code>go test ./internal/health -run "Test.*Energy|Test.*Verdict" -count=1</code></li>
149+
<li><code>go test ./internal/storage -run "Test.*Energy" -count=1</code></li>
150+
<li><code>go test ./internal/ui -run "Test.*Energy|Test.*Dashboard" -count=1</code></li>
151+
<li><code>go test ./...</code></li>
152+
<li><code>go vet ./...</code></li>
153+
<li><code>git diff --check</code></li>
154+
</ul>
155+
</section>
156+
157+
<section>
158+
<h2>Known Limitations And Follow-Up</h2>
159+
<ul>
160+
<li>The legacy fallback is intentionally still present; removing it would need a separate compatibility decision.</li>
161+
<li><code>daily_scores.energy_*</code> columns remain in place and should only be removed by a future explicit migration.</li>
162+
<li>The day-level history endpoint remains available for compatibility and AI verdict-history context.</li>
163+
</ul>
164+
</section>
165+
</main>
166+
</body>
167+
</html>

0 commit comments

Comments
 (0)