Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions ENERGY_BANK.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# EnergyBank v2 — Design
# EnergyBank v2 — Current Implementation

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.
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.

## Model

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

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.

- **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.
- **History** (sparkline, hourly graph in `<details>`): reads `energy_snapshots` directly.
- **Live current** (dashboard hero, Telegram report): read from the latest same-day stored snapshot, then projected into the shared `health.EnergyBank` response shape.
- **History** (hourly graph in `<details>`): reads `energy_snapshots` directly.

## Storage

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

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

`daily_scores.energy_*` columns become a roll-up (last bucket of the day) — no longer the source of truth.
`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.

## Cadence — event-driven, not timer

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

## Phased delivery

**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.
**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.

**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.

Expand Down Expand Up @@ -267,7 +267,7 @@ With one parameter and 30 observations against an external physiological signal,
- **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.
- Workout-type detection (drain is HR/kcal driven, not exercise-class aware)
- Real-time push notifications when bank drops below threshold
- 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
- Migration or removal of `daily_scores.energy_*` rows — left in place as-is as compatibility columns. Any destructive cleanup needs a separate reversible migration.

## Open implementation questions

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

| State | Trigger | Baseline source | UI |
|---|---|---|---|
| `cold` | `n_nights < 3` overnight RHR samples in last 30d | None — `computeEnergyBank` returns `nil` | "Collecting baseline · 3 nights minimum" placeholder, no bank rendered |
| `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 |
| `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" |
| `steady` | `n_nights ≥ 7` AND newest sample ≤ 14 days old | EMA, α ≈ 0.25 (~7-day effective window) | Bank rendered normally |

Expand Down
26 changes: 14 additions & 12 deletions SCORING.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,13 +416,14 @@ the headline with section verdicts so the briefing is **internally consistent**:

---

## Energy Bank v1 (shipping — to be replaced by v2)
## Energy Bank legacy fallback

> **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.
> **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.

Computed in `internal/health/energy.go::computeEnergyBank`, returned in
`response.energy_bank`. Where the Headline answers *"what's notable today?"*,
the Energy Bank answers *"what should you do?"* — a prescriptive verdict that
The fallback is returned in `response.energy_bank` only until the storage
briefing path replaces it with the latest v2 snapshot. Where the Headline
answers *"what's notable today?"*, Energy Bank answers *"what should you do?"*
— a prescriptive verdict that
rolls capacity, observed activity load, and autonomic stress into a single
plain-language action ("push_hard", "moderate", "active_recovery", "rest").

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

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

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

---

## Energy Bank v2 (design — pending implementation)
## Energy Bank v2 (current shipped path)

> Full specification: [`ENERGY_BANK.md`](./ENERGY_BANK.md). This section captures
> the methodology and the empirically-derived design principles; the spec doc
> carries the implementation detail (schema, concurrency, rollout phases).
> Full specification and operational details: [`ENERGY_BANK.md`](./ENERGY_BANK.md).
> v2 writes `energy_snapshots`, reads the latest local-date snapshot into the
> dashboard/report `energy_bank` field, and serves hourly history from the same
> table. Legacy `daily_scores.energy_*` columns remain compatibility-only.

Three structural problems with v1 were discovered through an empirical
Three structural problems with the legacy fallback were discovered through an empirical
prototype run on 90 days of real data, on **two distinct users** (one
sedentary with clean data, one mixed-source with frequent sync gaps):

Expand Down
9 changes: 4 additions & 5 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,9 @@ func main() {

mux := http.NewServeMux()

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

onNewData := func(db *storage.DB, dates []string) {
Expand Down Expand Up @@ -286,8 +286,7 @@ func runSingleTenant(ctx context.Context, addr, baseURL string, trustFwdAuth boo
var morningSendMu sync.Mutex
maybeFireMorningReport := makeMorningTrigger(db, &morningSendMu, mgr, reg, schema, notifyDefaults)
backfillDatesFn := makeBackfillDatesFn(db, schema, notifyDefaults)
// EnergyBank v2 orchestrator: same role as in multi-tenant mode —
// passive snapshot accumulation alongside the live v1 dashboard.
// EnergyBank v2 orchestrator: same role as in multi-tenant mode.
energyV2 := storage.NewEnergyV2Orchestrator()
onNewData := func(_ *storage.DB, dates []string) {
backfillDatesFn(dates)
Expand Down
167 changes: 167 additions & 0 deletions docs/ai-plans/2026-06-06-issue-148-energybank-v1-cleanup-docs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Implementation Plan: Issue #148 EnergyBank v1 Cleanup and Docs</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { margin: 0; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f6f7f9; color: #1f2937; line-height: 1.5; }
main { max-width: 1100px; margin: 0 auto; padding: 32px; }
section { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; margin: 16px 0; }
h1, h2 { line-height: 1.2; margin-top: 0; }
code { background: #f3f4f6; padding: 2px 5px; border-radius: 4px; }
.badge { display: inline-block; padding: 4px 10px; border-radius: 999px; background: #dcfce7; color: #166534; font-size: 12px; font-weight: 700; }
.risk { border-left: 4px solid #dc2626; }
.approval { border-left: 4px solid #d97706; }
</style>
</head>
<body>
<main>
<h1>Implementation Plan: Issue #148 EnergyBank v1 Cleanup and Docs</h1>
<p class="badge">Approved and implemented</p>

<section>
<h2>Task Summary</h2>
<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>
</section>

<section>
<h2>Current Behavior</h2>
<ul>
<li><code>internal/health/energy.go::computeEnergyBank</code> is still called by <code>internal/health/scoring.go</code>.</li>
<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>
<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>
<li><code>internal/storage/energy_history_v2.go</code> serves the newer hourly <code>energy_snapshots</code> path.</li>
<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>
</ul>
</section>

<section>
<h2>Desired Behavior</h2>
<ul>
<li>Every remaining v1 reference is either removed or labeled as compatibility/fallback with a current reason.</li>
<li>Docs describe the shipped v2 state machine, snapshot table, personal verdict bands, and legacy daily columns accurately.</li>
<li>No destructive DB cleanup is bundled unless separately approved and reversible.</li>
<li>Dashboard, Telegram, AI prompt, and MCP/debug paths continue to render EnergyBank correctly.</li>
</ul>
</section>

<section>
<h2>Assumptions And Unknowns</h2>
<ul>
<li>Assumption: do not drop <code>daily_scores.energy_*</code> in this PR; keep or deprecate them unless a separate migration is approved.</li>
<li>Assumption: <code>computeEnergyBank</code> may still be needed as a bootstrap/fallback when no v2 snapshot exists.</li>
<li>Unknown: whether the legacy sparkline path is still used by any client after the v2 hourly chart.</li>
<li>Unknown: whether production has enough v2 stability to remove fallback behavior entirely; issue says at least 7 production days without material issues.</li>
</ul>
</section>

<section>
<h2>Files Likely To Change</h2>
<ul>
<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>
<li><code>internal/storage/briefing.go</code> - clarify or simplify v1-to-v2 override comments and persistence behavior.</li>
<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>
<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>
<li><code>internal/health/*energy*_test.go</code> and storage energy tests - update tests only where they pin obsolete v1 behavior.</li>
</ul>
</section>

<section>
<h2>Implementation Steps</h2>
<ol>
<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>
<li>Trace dashboard, Telegram report, AI prompt, MCP, and chart reads to identify which paths still depend on legacy daily columns.</li>
<li>Decide between two safe paths: docs-only deprecation of v1 fallback, or code removal where no live callsite remains.</li>
<li>Update methodology docs first so reviewers can check intended current behavior.</li>
<li>Apply the smallest code cleanup that matches the audit; avoid schema drops.</li>
<li>Run EnergyBank-focused tests and full Go verification.</li>
</ol>
</section>

<section>
<h2>Impact</h2>
<ul>
<li><strong>Data:</strong> no destructive migration planned; legacy columns remain unless separately approved.</li>
<li><strong>API/UX:</strong> EnergyBank JSON shape should remain compatible.</li>
<li><strong>Docs:</strong> methodology docs become the main output if code still needs v1 fallback.</li>
<li><strong>Deployment:</strong> normal app deploy only if production code changes; docs-only changes need no deploy for runtime behavior.</li>
</ul>
</section>

<section>
<h2>Test Plan</h2>
<ul>
<li><code>go test ./internal/health -run "Test.*Energy|Test.*Verdict" -count=1</code></li>
<li><code>go test ./internal/storage -run "Test.*Energy" -count=1</code></li>
<li><code>go test ./internal/ui -run "Test.*Energy|Test.*Dashboard" -count=1</code> if UI/chart handlers change.</li>
<li><code>go test ./...</code></li>
<li><code>go vet ./...</code></li>
<li>Manual QA if deployed: confirm dashboard hero EnergyBank, hourly chart, and Telegram morning report still render.</li>
</ul>
</section>

<section class="risk">
<h2>Risks And Edge Cases</h2>
<ul>
<li>Removing v1 too early can break cold-start tenants or days with no v2 snapshot.</li>
<li>Docs can become inconsistent if <code>AGENTS.md</code> and <code>CLAUDE.md</code> are not kept in lockstep.</li>
<li>Legacy daily columns may still support old chart or AI history paths; audit before removal.</li>
<li>Changing EnergyBank behavior under a cleanup issue would be out of scope.</li>
</ul>
</section>

<section>
<h2>Rollback Plan</h2>
<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>
</section>

<section>
<h2>Open Questions</h2>
<ul>
<li>Should <code>computeEnergyBank</code> remain as a named legacy fallback until all tenants have v2 snapshots?</li>
<li>Is the old EOD <code>GetEnergyHistory</code> path still needed by any frontend or mobile client?</li>
<li>Do we want a separate future issue for dropping <code>daily_scores.energy_*</code> columns after compatibility expires?</li>
</ul>
</section>

<section class="approval">
<h2>Approval Gate</h2>
<p>Approved by the user on 2026-06-06 before implementation.</p>
</section>

<section>
<h2>Implementation Result</h2>
<ul>
<li>Renamed <code>computeEnergyBank</code> to <code>computeLegacyEnergyBank</code> to make the remaining v1 behavior explicitly compatibility-only.</li>
<li>Kept the legacy fallback behavior unchanged for fresh tenants and days before a v2 snapshot exists.</li>
<li>Updated storage, server, UI, and chart comments that still described v2 as passive, pending, or waiting for a future UI flip.</li>
<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>
<li>No schema drops or destructive database cleanup were included.</li>
</ul>
</section>

<section>
<h2>Verification Run</h2>
<ul>
<li><code>gofmt -w</code> on changed Go files.</li>
<li><code>go test ./internal/health -run "Test.*Energy|Test.*Verdict" -count=1</code></li>
<li><code>go test ./internal/storage -run "Test.*Energy" -count=1</code></li>
<li><code>go test ./internal/ui -run "Test.*Energy|Test.*Dashboard" -count=1</code></li>
<li><code>go test ./...</code></li>
<li><code>go vet ./...</code></li>
<li><code>git diff --check</code></li>
</ul>
</section>

<section>
<h2>Known Limitations And Follow-Up</h2>
<ul>
<li>The legacy fallback is intentionally still present; removing it would need a separate compatibility decision.</li>
<li><code>daily_scores.energy_*</code> columns remain in place and should only be removed by a future explicit migration.</li>
<li>The day-level history endpoint remains available for compatibility and AI verdict-history context.</li>
</ul>
</section>
</main>
</body>
</html>
Loading