Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
79db58f
fix(forecast): validate model_path type in feature-metadata loader (#…
w7-mgfcode May 20, 2026
1ea196f
docs(docs): fix PRP-31 endpoint count in scope statement (#267)
w7-mgfcode May 20, 2026
655007a
docs(docs): align PRP-31 missing-bundle status code to 422 (#267)
w7-mgfcode May 20, 2026
5bc681a
Merge pull request #272 from w7-mgfcode/fix/coderabbit-pr266-quickwins
w7-mgfcode May 20, 2026
6fcb404
Merge pull request #271 from w7-mgfcode/chore/back-merge-main-v0-2-17
w7-mgfcode May 20, 2026
a06c32b
docs(docs): add tech-stack badges to README (shields.io for-the-badge…
w7-mgfcode May 20, 2026
c994842
Merge pull request #274 from w7-mgfcode/docs/readme-tech-stack-badges
w7-mgfcode May 20, 2026
1946dfc
feat(repo): add backend + frontend Dockerfiles and .dockerignore (#275)
w7-mgfcode May 20, 2026
797e899
feat(repo): extend docker-compose for full stack + GPU profile (#275)
w7-mgfcode May 20, 2026
3ac976b
docs(repo): document compose-mode stack and failure runbook (#275)
w7-mgfcode May 20, 2026
f2f1375
chore(repo): refresh uv.lock to v0.2.17 (#275)
w7-mgfcode May 20, 2026
917a54c
Merge pull request #276 from w7-mgfcode/feat/repo-dockerize-full-stack
w7-mgfcode May 20, 2026
5925fc4
docs(repo): add 5 batch-runner INITIAL docs (#277)
w7-mgfcode May 20, 2026
1397261
Merge pull request #278 from w7-mgfcode/docs/batch-initial-docs
w7-mgfcode May 20, 2026
cb5c96e
docs(repo): add PRP-33 batch-runner-mvp (#277)
w7-mgfcode May 20, 2026
df90d22
Merge pull request #279 from w7-mgfcode/docs/prp-33-batch-runner-mvp
w7-mgfcode May 20, 2026
73584c8
feat(batch): implement batch-runner MVP per PRP-33 (#280)
w7-mgfcode May 20, 2026
37dd038
fix(batch): clean leaked data-platform rows in test db_session (#280)
w7-mgfcode May 25, 2026
7403743
fix(db): register batch models with alembic env for autogenerate (#280)
w7-mgfcode May 25, 2026
d8e6098
fix(batch): declare partial picker index on model so autogenerate see…
w7-mgfcode May 25, 2026
c495357
Merge pull request #281 from w7-mgfcode/feat/prp-33-batch-runner-mvp
w7-mgfcode May 25, 2026
458bb9f
docs(docs): update MLZOO index — mark D as PRP-31 (#280)
w7-mgfcode May 25, 2026
d2c674a
Merge pull request #282 from w7-mgfcode/docs/mlzoo-index-prp31-status
w7-mgfcode May 25, 2026
631c7de
docs(docs): refresh batch-parallel-execution INITIAL post-PRP-33 (#280)
w7-mgfcode May 25, 2026
c9428db
Merge pull request #283 from w7-mgfcode/docs/batch-parallel-execution…
w7-mgfcode May 25, 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
67 changes: 67 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Build-context exclusions — keep `docker build` fast and keep secrets out.
# Mirrors the host `.gitignore` plus a few build-only caches.

# Git metadata (never needs to be in the image)
.git/
.gitignore
.gitattributes

# Local secrets and machine-specific overrides
.env
.env.cloud
.env.local
CLAUDE.local.md

# Python virtualenvs and caches
.venv/
venv/
venv_linux/
venv_windows/
__pycache__/
*.pyc
*.pyo
*.pyd
.pytest_cache/
.ruff_cache/
.mypy_cache/
.pyright/

# Frontend build outputs and node modules — rebuilt inside the image
frontend/node_modules/
frontend/dist/
frontend/.vite/

# Generated artifacts (models, backtest results)
artifacts/

# Local session artifacts (plans, handoffs, current session notes)
.agents/
.handoffs/
HANDOFF.md

# Local CI / dogfood logs and screenshots
.ci-logs/
playwright-report/
test-results/
.playwright-mcp/

# Editor / OS noise
.vscode/
.idea/
.DS_Store
*.log

# Claude Code local-only directory (gitignored upstream, redundant here for safety)
.claude/

# Documentation, examples and PRPs aren't needed at runtime; the backend image
# bakes only `app/`, `alembic/`, `alembic.ini`, and `scripts/`. Excluding these
# from the build context keeps the layer cache stable when docs change.
# README.md is intentionally NOT excluded — `pyproject.toml`'s `readme =
# "README.md"` makes uv read it during `uv sync`, so it must be present in the
# build context.
docs/
examples/
PRPs/
HANDOFF.md
CHANGELOG.md
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
# Copy this file to .env and adjust values as needed

# Database connection (PostgreSQL + pgvector via Docker Compose)
# Host-mode (`uv run uvicorn`): use the line below as-is.
# Compose-mode (`make docker-up`): the backend container's `environment:` block
# in docker-compose.yml overrides this with
# DATABASE_URL=postgresql+asyncpg://forecastlab:forecastlab@postgres:5432/forecastlab
# so this `.env` value stays the host-mode default.
DATABASE_URL=postgresql+asyncpg://forecastlab:forecastlab@localhost:5433/forecastlab

# Application settings
Expand Down Expand Up @@ -31,7 +36,10 @@ OPENAI_API_KEY=sk-your-openai-api-key-here
RAG_EMBEDDING_MODEL=text-embedding-3-small

# Ollama Configuration (when RAG_EMBEDDING_PROVIDER=ollama)
# Host-mode default — uncomment and adjust as needed:
# OLLAMA_BASE_URL=http://localhost:11434
# Compose-mode (`make docker-up-gpu`): the backend container injects
# OLLAMA_BASE_URL=http://ollama:11434 (in-cluster DNS, NOT localhost).
# OLLAMA_EMBEDDING_MODEL=nomic-embed-text

# Embedding dimension (must match your model: OpenAI=1536, nomic-embed-text=768, etc.)
Expand Down Expand Up @@ -103,5 +111,8 @@ AGENT_APPROVAL_TIMEOUT_MINUTES=60
# Streaming
AGENT_ENABLE_STREAMING=true

# Batch runner (PRP-33) — cap on scope expansion (pairs × model_configs).
BATCH_MAX_SCOPE_EXPANSION=1000

# Frontend (Vite)
VITE_API_BASE_URL=http://localhost:8123
94 changes: 94 additions & 0 deletions Dockerfile.backend
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# syntax=docker/dockerfile:1.7
#
# ForecastLabAI backend image.
#
# Multi-stage:
# base — python:3.12-slim-bookworm + curl (for healthcheck) + uv installer
# deps — `uv sync --frozen --extra dev --no-install-project` against pyproject.toml/uv.lock
# ONLY (no `app/` yet — keeps the dep layer cacheable across source edits)
# dev — copies the project, installs it, runs alembic+uvicorn with --reload
# prod — re-syncs without dev extras, no --reload (smaller, immutable)
#
# Build:
# docker build -t forecastlab-backend:dev --target dev -f Dockerfile.backend .
# docker build -t forecastlab-backend:prod --target prod -f Dockerfile.backend .
#
# Gotchas (verified on this host):
# * asyncpg talks the Postgres wire protocol directly — NO libpq needed.
# * uv 0.11.8 supports `--no-install-project` (verified via `uv sync --help`).
# * Inside the container the Postgres DNS name is `postgres:5432`, NOT
# `localhost:5433` (that's the host-side published port).

ARG PYTHON_VERSION=3.12-slim-bookworm

# ---------- base ----------
FROM python:${PYTHON_VERSION} AS base

# curl for the healthcheck; ca-certificates for the uv install over HTTPS.
# build-essential is intentionally NOT installed — every Python dep in uv.lock
# ships a manylinux wheel, asyncpg includes a binary distribution, and pure-Python
# packages need no compiler.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# Install uv (pinned by the official installer's stable URL; the binary lands
# in /root/.local/bin which we add to PATH).
ENV UV_INSTALL_DIR=/root/.local/bin
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:${PATH}"

# Don't write .pyc files or buffer stdout — keeps logs clean and the image small.
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
UV_LINK_MODE=copy \
UV_PROJECT_ENVIRONMENT=/opt/venv

WORKDIR /app

# ---------- deps ----------
FROM base AS deps

# Bring in JUST the dependency manifests. `--no-install-project` keeps uv from
# trying to install the workspace itself when `app/` isn't present yet.
COPY pyproject.toml uv.lock README.md ./

RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --extra dev --no-install-project

# ---------- dev (default target) ----------
FROM deps AS dev

# Project source. Keep these in their own layer so an `app/` edit doesn't
# invalidate the (much heavier) deps layer above.
COPY app/ ./app/
COPY alembic/ ./alembic/
COPY alembic.ini ./
COPY scripts/ ./scripts/

# Now register the project itself against the existing venv. Cheap — deps are
# cached above, only the project's editable install runs.
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --extra dev

EXPOSE 8123

# Inline entrypoint:
# 1. Wait for Postgres? — handled by `depends_on: condition: service_healthy`
# in docker-compose; the container only starts after pg_isready is green.
# 2. Run migrations (forward-only — idempotent if already applied).
# 3. Hand off to uvicorn with --reload for the dev hot-loop.
ENTRYPOINT ["sh", "-c", "uv run alembic upgrade head && exec uv run uvicorn app.main:app --host 0.0.0.0 --port 8123 --reload"]

# ---------- prod ----------
FROM dev AS prod

# Re-sync WITHOUT the dev extra to drop pytest/ruff/mypy/pyright from the image.
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen

# No --reload in prod. The migration step stays — first container of a new
# release applies migrations idempotently; subsequent containers see "no-op".
ENTRYPOINT ["sh", "-c", "uv run alembic upgrade head && exec uv run uvicorn app.main:app --host 0.0.0.0 --port 8123"]
90 changes: 90 additions & 0 deletions Dockerfile.frontend
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# syntax=docker/dockerfile:1.7
#
# ForecastLabAI frontend image.
#
# Multi-stage:
# builder — Node 22 + pnpm via corepack, runs `pnpm install --frozen-lockfile`
# and `pnpm build` for a static SPA bundle
# prod — nginx:1.27-alpine serving the built bundle with SPA try_files
# dev — Node 22 + pnpm, mounts source for hot-reload via `vite --host 0.0.0.0`
#
# Build:
# docker build -t forecastlab-frontend:dev --target dev -f Dockerfile.frontend .
# docker build -t forecastlab-frontend:prod --target prod -f Dockerfile.frontend .
#
# Gotcha:
# `frontend/vite.config.ts` already sets `server.host: true` (== 0.0.0.0).
# The dev CMD's `--host 0.0.0.0` is belt-and-suspenders — harmless even if
# someone removes the config-file binding in a future edit.
#
# pnpm.onlyBuiltDependencies: ["esbuild"] is already in frontend/package.json —
# the pnpm-11 depsStatusCheck stall documented in RUNBOOKS.md is mitigated
# at the source. We still call Vite directly in the dev target because
# explicit > implicit in a hot-reload container path.

ARG NODE_VERSION=22-bookworm-slim

# ---------- builder ----------
FROM node:${NODE_VERSION} AS builder

WORKDIR /app

# pnpm 11 added a strict "ignored build scripts" check that exits 1 even when
# `pnpm.onlyBuiltDependencies` in package.json approves the only script that
# needs running (esbuild). The non-interactive override is the env var below —
# safe because the only entry in `onlyBuiltDependencies` is `esbuild`, which
# is a first-party Vite dep anyway. Verified on pnpm 11.0.9.
ENV PNPM_CONFIG_DANGEROUSLY_ALLOW_ALL_BUILDS=true

# corepack ships with Node 22; activate pnpm without a global install.
RUN corepack enable pnpm

# Lock-bound install first — invalidates only when the lockfile or manifest
# changes, so source edits don't blow away the node_modules cache layer.
COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

# Project source.
COPY frontend/ ./

# Produces /app/dist for the prod target.
RUN pnpm build

# ---------- prod ----------
FROM nginx:1.27-alpine AS prod

# Static bundle.
COPY --from=builder /app/dist/ /usr/share/nginx/html/

# Minimal SPA conf: client-side routes fall back to /index.html so a deep link
# like /showcase doesn't 404 on a hard refresh.
RUN printf 'server {\n listen 80;\n server_name _;\n root /usr/share/nginx/html;\n index index.html;\n location / {\n try_files $uri $uri/ /index.html;\n }\n}\n' > /etc/nginx/conf.d/default.conf

EXPOSE 80

# ---------- dev (default target) ----------
FROM node:${NODE_VERSION} AS dev

WORKDIR /app

# Same pnpm-11 strict-build override as the builder stage — see comment above.
ENV PNPM_CONFIG_DANGEROUSLY_ALLOW_ALL_BUILDS=true

RUN corepack enable pnpm

# Same lock-bound install pattern as builder, but kept as the dev install so
# tools like vite + tsc + eslint are available in the image.
COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

# Bake the source into the image so a standalone `docker run` works without
# any bind-mount. The compose file ADDS bind-mounts on top of this for the
# hot-reload dev loop — but the image must be self-sufficient.
COPY frontend/ ./

EXPOSE 5173

# `--host 0.0.0.0` is required so the host browser can reach the container.
# Vite's default bind is 127.0.0.1 inside the container, which yields
# ECONNREFUSED on http://localhost:5173 from the host.
CMD ["./node_modules/.bin/vite", "--host", "0.0.0.0"]
26 changes: 21 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,25 @@
# make help — list available targets

.DEFAULT_GOAL := help
.PHONY: help demo demo-quick demo-clean
.PHONY: help demo demo-quick demo-clean docker-up docker-up-gpu docker-down

help: ## show this help and exit
@echo "ForecastLabAI Make targets:"
@echo " make demo run the full end-to-end demo (~90-180 s)"
@echo " make demo-quick re-run the demo without re-seeding"
@echo " make demo-clean wipe the DB, then run the full demo"
@echo " make demo run the full end-to-end demo (~90-180 s)"
@echo " make demo-quick re-run the demo without re-seeding"
@echo " make demo-clean wipe the DB, then run the full demo"
@echo " make docker-up bring the full stack up in containers (no GPU)"
@echo " make docker-up-gpu bring the full stack up with Ollama on GPU"
@echo " make docker-down stop containers (keeps named volumes)"
@echo ""
@echo "Preconditions for all targets:"
@echo "Preconditions for the demo targets:"
@echo " * docker compose Postgres+pgvector must be reachable on :5433"
@echo " * uvicorn must already be serving on http://localhost:8123"
@echo " (start with: uv run uvicorn app.main:app --port 8123)"
@echo ""
@echo "Preconditions for the docker targets:"
@echo " * docker + docker compose v2 installed; `.env` populated"
@echo " (`cp .env.example .env`)."

demo: ## full e2e — seed -> features -> train x3 -> backtest -> register -> agent
docker compose up -d
Expand All @@ -43,3 +50,12 @@ demo-clean: ## destructive — wipe DB then run the full demo
docker compose up -d
uv run alembic upgrade head
uv run python scripts/run_demo.py --seed 42 --reset

docker-up: ## bring the full stack up in containers (no GPU)
docker compose up -d --wait --wait-timeout 90

docker-up-gpu: ## bring the full stack up with Ollama on GPU
docker compose -f docker-compose.yml -f docker-compose.gpu.yml --profile gpu up -d --wait --wait-timeout 120

docker-down: ## stop and remove containers (keeps named volumes)
docker compose down
2 changes: 1 addition & 1 deletion PRPs/INITIAL/INITIAL-MLZOO-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Recommended PRP sequence:
| 2.5 | `INITIAL-MLZOO-B.2-feature-aware-backtesting.md` | PRP-MLZOO-B.2 | Wire feature-aware models into the backtesting fold loop (per-fold leakage-safe `X_train` / `X_future`) |
| 3a | `INITIAL-MLZOO-C-xgboost-prophet-extensions.md` (XGBoost half) | PRP-MLZOO-C1 | XGBoost feature-aware model — a low-risk follow-up mirroring the merged LightGBM design (optional `ml-xgboost` extra) |
| 3b | `INITIAL-MLZOO-C-xgboost-prophet-extensions.md` (Prophet-like half) | PRP-MLZOO-C2 | Prophet-like additive model — a distinct model-family design (pure scikit-learn; trend / seasonality / regressor decomposition) |
| 4 | `INITIAL-MLZOO-D-frontend-registry-explainability.md` | Future PRP | UI, registry surfacing, and explanation polish |
| 4 | `INITIAL-MLZOO-D-frontend-registry-explainability.md` | PRP-31 | UI, registry surfacing, and explanation polish |

**C is two PRPs, not one.** `INITIAL-MLZOO-C` briefs both XGBoost and a Prophet-like model,
but they are deliberately split into **two separate PRPs, branches, and review units** —
Expand Down
Loading
Loading