Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
07b6f1f
feat(15v2): squash-port Phase 15 v1 scaffolding to backtest-overlay b…
shiniguchi Apr 30, 2026
53d325f
docs(15v2): Context + 9 plan docs for Phase 15 v2 — Forecast Backtest…
shiniguchi Apr 30, 2026
c32f4de
feat(15-09): add granularity column to forecast_daily (D-14)
shiniguchi Apr 30, 2026
30bf298
feat(15-09): switch forecast-refresh to weekly Monday cron (D-16)
shiniguchi Apr 30, 2026
afecc91
test(15-09): integration tests for forecast_daily granularity column
shiniguchi Apr 30, 2026
e5f7325
test(15-09): add backfill assertion for forecast_daily granularity
shiniguchi Apr 30, 2026
d2b2ca9
docs(15-09): note RLS load-bearing JOIN predicate in forecast_with_ac…
shiniguchi Apr 30, 2026
ab49a12
feat(15-10): bucket-to-weekly + bucket-to-monthly helpers (D-14)
shiniguchi Apr 30, 2026
2aa7841
feat(15-10): add date_col kwarg to bucket helpers (default 'business_…
shiniguchi Apr 30, 2026
a04fc23
feat(15-10): run_all triple-loop + freshness gate (D-16)
shiniguchi Apr 30, 2026
947fc9f
feat(15-10): thread granularity env to all 5 model fit scripts
shiniguchi Apr 30, 2026
764c445
test(15-10): integration tests for run_all triple-grain loop
shiniguchi Apr 30, 2026
d5f9c60
docs(15-10): note model-order/eval/gate semantics from spec review
shiniguchi May 1, 2026
6172060
refactor(15-10): extract grain helpers + document closed-day assumpti…
shiniguchi May 1, 2026
65ba1a1
feat(15-11): drop forecastResampling.ts; slim forecastValidation (D-14)
shiniguchi May 1, 2026
53e4422
feat(15-11): /api/forecast native-grain query + ?kpi= param + backtes…
shiniguchi May 1, 2026
4e27219
feat(15-12): CalendarRevenueCard forecast overlay (D-15/D-17/D-18)
shiniguchi May 1, 2026
73e4984
feat(15-13): CalendarCountsCard forecast overlay (D-15/D-17/D-18)
shiniguchi May 1, 2026
02040f0
feat(15-14): delete HorizonToggle (D-14 makes it redundant — global G…
shiniguchi May 1, 2026
df8d5d6
feat(15-14): RevenueForecastCard rewrite — drop HorizonToggle, full r…
shiniguchi May 1, 2026
64ed4ae
feat(15-15): add 2 i18n keys for InvoiceCountForecastCard
shiniguchi May 1, 2026
eab80f2
feat(15-15): add InvoiceCountForecastCard sibling (D-18)
shiniguchi May 1, 2026
9f546b8
feat(15-15): mount InvoiceCountForecastCard on dashboard (D-18)
shiniguchi May 1, 2026
8f03912
docs(15v2): update STATE.md + ROADMAP.md for Phase 15 v2 progress
shiniguchi May 1, 2026
5a59b52
ci(migrations): add workflow_dispatch trigger
shiniguchi May 1, 2026
e684aa6
fix(15v2): grain-aware forecast empty state + auto-scroll calendar
shiniguchi May 1, 2026
d736da9
fix(15v2): bind scrollerRef on CalendarRevenueCard scroll container
shiniguchi May 1, 2026
4bf0874
fix(15v2): use date-math for forecast auto-scroll, not chartCtx.xScale
shiniguchi May 1, 2026
6950d99
fix(15v2): RAF-defer auto-scroll so scrollWidth includes forecast zone
shiniguchi May 1, 2026
ad4cfdd
fix(15v2): make auto-scroll effect depend on chartW, not just forecas…
shiniguchi May 1, 2026
8bc87a3
fix(15v2): track our own auto-scroll writes so chartW updates can refine
shiniguchi May 1, 2026
db96814
fix(15v2): poll RAF until SVG canvas catches up to chartW
shiniguchi May 1, 2026
6c9899d
fix(15v2): use bucket-count proportion for auto-scroll target
shiniguchi May 1, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/forecast-refresh.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Forecast Refresh
on:
schedule:
- cron: '0 1 * * *' # 01:00 UTC — C-02, Guard 8 cascade
- cron: '0 7 * * 1' # Mondays 07:00 UTC (09:00 Berlin) — Phase 15-09 D-16 weekly cadence; preserves Guard 8 cascade
workflow_dispatch:
inputs:
models:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/migrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ name: DB Migrations (DEV)
on:
push:
branches: [main]
workflow_dispatch:
jobs:
push:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion .planning/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ A restaurant owner opens the site on their phone and makes a real business decis
- [x] **Phase 12: Foundation — Decisions & Guards** — ITS validity audit script + CI grep guard (`tenant_id` → `restaurant_id`) + UTC-anchored cron schedule contract
- [x] **Phase 13: External Data Ingestion** — 5 ingest tables (weather/holidays/school/transit/events) + pipeline_runs + shop_calendar + GHA workflow + backfill from 2025-06-11
- [x] **Phase 14: Forecasting Engine — BAU Track** — SARIMAX/Prophet/ETS/Theta/Naive nightly fits + sample-path resampling + last_7_eval + forecast_daily_mv
- [ ] **Phase 15: Forecast Chart UI** — RevenueForecastCard + horizon/legend toggles + hover popup + event markers + 3 deferred `/api/*` endpoints + 375px QA
- [~] **Phase 15: Forecast Chart UI** — v2 (Forecast Backtest Overlay) impl 15-09..15-15 complete on branch `feature/phase-15-forecast-backtest-overlay`; 15-16 (DEV deploy + PR) pending user authorization; 15-17 (retire dedicated cards) deferred
- [ ] **Phase 16: ITS Uplift Attribution** — campaign_calendar + Track-B counterfactual fit + campaign_uplift_v + CampaignUpliftCard with honest "CI overlaps zero" labeling
- [ ] **Phase 17: Backtest Gate & Quality Monitoring** — rolling-origin CV at 4 horizons + ConformalIntervals + ≥10% RMSE promotion gate + freshness-SLO badges + ACCURACY-LOG

Expand Down
10 changes: 5 additions & 5 deletions .planning/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
gsd_state_version: 1.0
milestone: v1.3
milestone_name: External Data & Forecasting Foundation
status: "Phase 14 shipped"
stopped_at: Phase 14 (Forecasting Engine — BAU Track) shipped 2026-04-30. PR #22 merged, 17 migrations applied to DEV, weather backfill complete (1622 rows + 365 climatology norms), GHA pipeline passing (5/5 models x 2 KPIs x 365 days). forecast_quality populates after 2nd nightly run. Ready for Phase 15 (Forecast Chart UI).
last_updated: "2026-04-30T00:00:00Z"
status: "Phase 15 v2 implementation complete (DEV deploy + PR pending user authorization)"
stopped_at: Phase 15 v2 (Forecast Backtest Overlay) plans 15-09 through 15-15 implemented locally on branch feature/phase-15-forecast-backtest-overlay. 19 commits, all unit tests pass, svelte-check clean at 6-error baseline. Migration 0057 (granularity column) applied locally. Plan 15-16 partial — STATE/ROADMAP closure done; localhost gate deferred (no Phase 14 forecast seed data on local DB causes /api/forecast 500 on cold start); DEV deploy + PR creation paused for user authorization. Plan 15-17 (retire dedicated forecast cards) remains deferred per CONTEXT.md.
last_updated: "2026-05-01T00:00:00Z"
progress:
total_phases: 17
completed_phases: 14
total_plans: 64
total_plans: 73
completed_plans: 61
percent: 82
percent: 84
---

# STATE: Ramen Bones Analytics
Expand Down
164 changes: 164 additions & 0 deletions .planning/phases/15-forecast-backtest-overlay/15-09-PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Phase 15-09: Schema Migration — `granularity` column on `forecast_daily`

> REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use checkbox `- [ ]` syntax.

**Goal:** Add `granularity text NOT NULL CHECK (granularity IN ('day','week','month'))` to `forecast_daily`, update PK + MV + wrapper view to include it, switch the forecast-refresh cron from nightly to weekly Monday morning. Replay-safe migration; backfills existing rows to `'day'`.

**Architecture:** One new SQL migration file. Three downstream artifacts adjust: `forecast_daily_mv` (rebuild with new PK), `forecast_with_actual_v` (re-create with granularity passthrough), `.github/workflows/forecast-refresh.yml` (cron schedule edit).

**Tech Stack:** PostgreSQL 15, Supabase migrations, GitHub Actions YAML.

---

## File Structure

| File | Action | Responsibility |
|---|---|---|
| `supabase/migrations/0057_forecast_daily_granularity.sql` | **Create** | Adds column with default `'day'`, backfills existing rows, drops default, bumps NOT NULL, redrops + recreates PK to include granularity. Rebuilds `forecast_daily_mv` and `forecast_with_actual_v`. |
| `.github/workflows/forecast-refresh.yml` | **Modify** | Cron `0 1 * * *` (nightly) → `0 7 * * 1` (Monday 07:00 UTC). |
| `tests/integration/forecast_daily_granularity.test.ts` | **Create** | Asserts: column exists with CHECK constraint; PK includes granularity; existing rows backfill to `'day'`; MV + view expose `granularity`. |

---

### Task 1: Migration file

- [ ] **Step 1: Create `supabase/migrations/0057_forecast_daily_granularity.sql`**

```sql
-- 0057_forecast_daily_granularity.sql
-- Phase 15 v2 D-14: per-grain forecasts. Each refresh writes 3 rows per
-- (model, target_date) — one for each (day, week, month) granularity —
-- so the chart can show a daily forecast trained at last_actual−7d AND
-- a weekly forecast trained at last_actual−5w AND a monthly forecast
-- trained at end-of-month−5mo, all from the same forecast_daily table.
--
-- Backfill safety: ALTER ADD COLUMN with DEFAULT is O(rows) on PG12+.
-- forecast_daily currently holds Phase 14's nightly runs (all daily
-- grain), so DEFAULT 'day' produces correct historical labelling.
-- Then DROP DEFAULT so future inserts must specify granularity.

ALTER TABLE public.forecast_daily
ADD COLUMN IF NOT EXISTS granularity text NOT NULL DEFAULT 'day'
CHECK (granularity IN ('day', 'week', 'month'));

ALTER TABLE public.forecast_daily ALTER COLUMN granularity DROP DEFAULT;

-- Drop + recreate PK to include granularity in the natural key.
-- Existing key was (restaurant_id, kpi_name, target_date, model_name, run_date, forecast_track).
ALTER TABLE public.forecast_daily DROP CONSTRAINT forecast_daily_pkey;
ALTER TABLE public.forecast_daily ADD PRIMARY KEY
(restaurant_id, kpi_name, target_date, model_name, granularity, run_date, forecast_track);

-- Rebuild forecast_daily_mv with granularity in select + unique index.
DROP MATERIALIZED VIEW IF EXISTS public.forecast_daily_mv CASCADE;

CREATE MATERIALIZED VIEW public.forecast_daily_mv AS
SELECT DISTINCT ON (restaurant_id, kpi_name, target_date, model_name, granularity, forecast_track)
restaurant_id, kpi_name, target_date, model_name, granularity, forecast_track,
run_date, yhat, yhat_lower, yhat_upper, horizon_days, exog_signature
FROM public.forecast_daily
ORDER BY restaurant_id, kpi_name, target_date, model_name, granularity, forecast_track, run_date DESC;

CREATE UNIQUE INDEX forecast_daily_mv_uq
ON public.forecast_daily_mv
(restaurant_id, kpi_name, target_date, model_name, granularity, forecast_track);

REVOKE ALL ON public.forecast_daily_mv FROM authenticated, anon;

-- Rebuild forecast_with_actual_v to include granularity passthrough.
-- Actual is keyed by business_date (daily-grain only); for weekly/monthly
-- forecasts the consumer (Phase 15 v2 endpoint) joins to k via target_date
-- which is already the bucket-start date for those grains, so the LEFT JOIN
-- works for daily but produces NULL actual_value for weekly/monthly rows
-- whose target_date doesn't land on a daily kpi_daily_mv row. The Phase
-- 15-11 endpoint handles that by building actuals from kpi_daily_mv directly
-- for the back-test window.
CREATE OR REPLACE VIEW public.forecast_with_actual_v AS
SELECT
f.restaurant_id, f.kpi_name, f.target_date, f.model_name, f.granularity, f.forecast_track,
f.run_date, f.yhat, f.yhat_lower, f.yhat_upper, f.horizon_days, f.exog_signature,
CASE f.kpi_name
WHEN 'revenue_eur' THEN k.revenue_cents / 100.0
WHEN 'invoice_count' THEN k.tx_count::double precision
END AS actual_value
FROM public.forecast_daily_mv f
LEFT JOIN public.kpi_daily_mv k
ON k.restaurant_id = f.restaurant_id
AND k.business_date = f.target_date
WHERE f.restaurant_id = (auth.jwt()->>'restaurant_id')::uuid;

GRANT SELECT ON public.forecast_with_actual_v TO authenticated;
```

- [ ] **Step 2: Apply migration locally + run Phase 14 integration tests**

```bash
supabase db reset # local DB; replays all migrations cleanly
npm run test:integration -- forecast_daily 2>&1 | tail -10
```

Expected: clean replay, Phase 14's existing integration tests still pass (they don't reference granularity yet so the new column is silently `'day'` for all rows the tests insert).

- [ ] **Step 3: Commit**

```bash
git add supabase/migrations/0057_forecast_daily_granularity.sql
git commit -m "feat(15-09): add granularity column to forecast_daily (D-14)"
```

---

### Task 2: Cron schedule change

- [ ] **Step 1: Edit `.github/workflows/forecast-refresh.yml`**

Replace `cron: '0 1 * * *'` with `cron: '0 7 * * 1'`. Update the comment header to note the cadence change is per Phase 15-09 D-16.

- [ ] **Step 2: Commit**

```bash
git add .github/workflows/forecast-refresh.yml
git commit -m "feat(15-09): switch forecast-refresh to weekly Monday cron (D-16)"
```

---

### Task 3: Integration test for granularity

- [ ] **Step 1: Write `tests/integration/forecast_daily_granularity.test.ts`** asserting:
- `granularity` column exists with CHECK constraint
- PK includes granularity (query `pg_constraint` for the 7-column key)
- Inserting without granularity fails (NOT NULL violation)
- Inserting with `granularity='hourly'` fails (CHECK violation)
- Existing rows from Phase 14 fixtures are visible at `granularity='day'`
- `forecast_with_actual_v` exposes granularity column

- [ ] **Step 2: Run test**

```bash
npm run test:integration -- forecast_daily_granularity 2>&1 | tail -10
```

- [ ] **Step 3: Commit**

```bash
git add tests/integration/forecast_daily_granularity.test.ts
git commit -m "test(15-09): integration tests for forecast_daily granularity column"
```

---

## Verification (end of plan)

- [ ] `supabase db reset` replays cleanly
- [ ] Phase 14 integration tests still pass (no regressions on the existing forecast_daily contract)
- [ ] CI guards pass (`npm run test:guards` — no raw `_mv` references introduced; the new view is the wrapper surface)

## Spec Coverage

| Decision | Where covered |
|---|---|
| D-14 (grain-specific TRAIN_ENDs schema) | Task 1 |
| D-16 (weekly cron) | Task 2 |

This plan is **prerequisite for** 15-10 (run_all.py writes rows with granularity values), 15-11 (endpoint queries by granularity column instead of resampling).
Loading
Loading