This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
T1D Engine is a Type 1 diabetes data intelligence system. It ingests CGM + insulin pump data, detects events (missed meals, anomalies), clusters daily BG patterns, and surfaces insights via a live Telegram alert loop, a Next.js personal dashboard, and a local Streamlit dashboard. Read TECHNICAL_SPEC.md before writing any code.
# Dependencies (uses uv, not pip)
uv sync
# Run
uv run python main.py
# Tests
uv run pytest
uv run pytest tests/test_specific.py
uv run pytest tests/test_specific.py::test_function_name
# Jupyter notebook
uv run jupyter notebook research.ipynbThe ingestion + enrichment + detection layers are in place, and three surfaces are shipped: a live Telegram meal-rise alert loop, a Next.js personal dashboard (Vercel + Supabase), and a local Streamlit dashboard. Current layout:
core/— storage-agnostic library shared by every deployment shell. Houses theStorageProtocol (core/storage/protocol.py), the schema registry (core/schema.py), typed metadata records (core/storage/records.py), and three implementations (core/storage/parquet.py,core/storage/memory.py,core/storage/supabase.py). The pandas → Postgres converters thatSupabaseStorageand the bootstrap script share live incore/storage/_postgres_converters.py.core/detection/holds the shared windowing helper and the meal-rise detector.ingestion/— tconnectsync client, per-event-type builders, enrichment (bolus_category,forced_by_alarm,site_issues,cgm_gaps), parquet storage shim (ingestion/storage.pydelegates toParquetStorage), shared view-mode helper (view_data.py).detection/— typedAppConfig+daily_featurespatterns-layer foundation. Source-agnostic: pure DataFrame-in / DataFrame-out, no ingestion imports. v1 reference implementation (anomaly / meal / clustering) is quarantined underdetection/legacy/— see its README.detection/calibration/holds the M2 meal-rise scoring module.apps/local/— local Streamlit OSS dashboard: day view, heatmap, TIR panel, insulin, AGP, compare — Plotly charts. Runs against parquet files with no cloud accounts required. AGP percentiles come fromcore/metrics/agp.py.apps/web/— Next.js personal dashboard deployed on Vercel + Supabase. Routes: day view, heatmap, TIR trends, insulin panel, search, compare, AGP, alerts history, status. All/api/*data routes require a signed-in session (lib/api/auth.ts).apps/personal/cron/— live meal-rise alert loop: polls Dexcom every 5 min, runs the detector, sends a Telegram alert on a missed-meal signal. Invoked by the Vercel Python worker atapi/index.py(a separate Vercel project), triggered by external cron-job.org.apps/personal/telegram/— deterministic Telegram command surface (/today,/yesterday,/trends,/status,/help). Webhook entrypointapi/telegram.py(same cron-worker Vercel project). No LLM — pure aggregates over the Storage layer; secret-token + chat-allowlist auth.core/metrics/— shared metric definitions (AGP hourly percentile profile); pure pandas/numpy, consumed by local charts and mirrored by web SQL.db/migrations/— Supabase schema migrations and RLS policies..github/workflows/— nightly Tandem sync (Telegram alert on failure), manual meal-rise fallback, smoke test, and pytest CI.scripts/— CLI entry points:sanity_check(check),daily_viz(viz),doctor;score_meal_rise.py(M2 calibration report — advisory only).tests/— 742 passed, 42 skipped, 48 deselected in the default suite across builders, storage, enrichment, detection features/config, thecore/metrics/clinical-analytics suite (golden + hypothesis property tests), CLI, the storage Protocol contract suite undertests/core/, and Telegram command/digest/handler tests undertests/personal/(supabase-parameterized tests skip unlessSUPABASE_TEST_URLis set; integration-marked tests are deselected by default). 47 additionallegacy-marked tests coverdetection/legacy/*and run opt-in viauv run pytest -m legacy. The web shell has its own vitest suite (93 tests) +tsc --noEmit+next build, gated in CI alongside pytest.
The core/ package is the storage-agnostic library that both the personal deployment shell (Next.js + Vercel + Supabase) and the OSS local shell (Streamlit + parquet/SQLite) consume. Import rules are binding:
core/MAY import from: stdlib, pandas, numpy, pydantic, typing/Protocol.core/MAY NOT import from:ingestion/,scripts/,apps/,psycopg2,supabase-py, parquet-specific code outsidecore/storage/parquet.py, Vercel SDK, Streamlit, FastAPI, Telegram libs, LLM clients.- Backend-specific concrete code (psycopg2 calls, parquet I/O) lives in
core/storage/parquet.py,core/storage/supabase.py, andcore/storage/_postgres_converters.py. Those are the ONLY files allowed to import their respective backend SDKs. - Code in
core/never decides which backend to use; the shell instantiates aStorageimplementation at startup and passes it down via constructor injection. - New downstream code (detection v2, the live alert loop, the Tandem→Supabase sync, the dashboard backend) takes a
Storagevia DI from the start.
The Storage Protocol in core/storage/protocol.py is the backend-agnostic data layer every caller talks to. Phase 1 (this PR family) landed the Protocol, the schema registry, and three implementations: ParquetStorage (local files), InMemoryStorage (tests), and SupabaseStorage (Postgres via psycopg2) — all three validated by parameterized contract tests under tests/core/test_storage_contract.py. The supabase parameterization skips unless SUPABASE_TEST_URL is set, and refuses to run against any host that matches a production-host denylist. The existing ingestion/storage.py is still a thin shim over ParquetStorage so every pre-Protocol caller (fetch, view, detection, bootstrap_supabase) keeps working unchanged — migrating existing callers to take a Storage via DI is deferred to follow-up PRs (live alert loop, Tandem nightly sync, dashboard backend, Telegram handlers).
SupabaseStorage callers MUST use the transaction-mode pooler URL (*.pooler.supabase.com:6543) and an open-do-close lifecycle (context manager for short-lived via SupabaseStorage.from_pooler_url(url), caller-managed conn via SupabaseStorage(conn=...) for long-lived). Direct connections (db.*.supabase.co:5432) are reserved for the nightly GitHub Action and the one-shot scripts/bootstrap_supabase.py. The Postgres-side idle_in_transaction_session_timeout = '5min' set by migration 0002 is the belt-and-suspenders backstop.
Migration 0003_enable_rls.sql enables Row-Level Security on every public table under a four-role threat model. postgres (psycopg2 with the DB password — used by bootstrap_supabase.py, the GitHub Action nightly sync, and SupabaseStorage) and service_role (Supabase JWT for server-side admin calls — used by future Vercel API routes) both have the BYPASSRLS attribute and are unaffected by RLS. authenticated (Supabase JWT for signed-in users) and anon (Supabase JWT for unauthenticated requests, the key embedded in client bundles) are subject to RLS.
Each of the 13 public tables carries one permissive policy: auth_required_all FOR ALL TO authenticated USING (true) WITH CHECK (true). anon has no policy on any table, so the default-deny behaviour applies — anon-key requests see zero rows everywhere. This is the minimum-viable lockdown for the current single-user shape; per-row ownership (USING (user_id = auth.uid())) is deferred until a multi-user story exists.
Implication for new code: server-side handlers that need admin access should connect via service_role (or the postgres role through SupabaseStorage) and rely on application-level authorization. Client-side handlers (Next.js bundles, future Streamlit pages with @supabase/supabase-js) MUST go through the anon key + Supabase Auth and rely on RLS for tenant isolation — they cannot read or write any public table without a signed-in authenticated session.
When you finish a substantive change, write a dated docs/updates/YYYY-MM-DD-*.md entry rather than mutating prior updates. The dated trail is the audit log.
Two ingestion modes:
- Historical: Tandem CSV exports (in
data/) and tconnectsync, synced nightly to Supabase via a GitHub Actions workflow. - Live: pydexcom polls the Dexcom Share API every 5 minutes. The live loop (
apps/personal/cron/) runs the meal-rise detector on each new reading and fires a Telegram alert when a missed-meal signal is detected. The loop is invoked by a Vercel Python worker (api/index.py) triggered by cron-job.org.
The detection engine must be source-agnostic — it operates on normalized data regardless of ingestion source.
check and viz both accept --view {original,enriched} (default original):
uv run python main.py check --date YYYY-MM-DD [--view enriched]
uv run python main.py viz --date YYYY-MM-DD [--view enriched]original— hides enrichment columns/overlays; preserves pre-enrichment output for regression comparisons.enriched— addsbolus_category/override_delta/forced_by_alarmsections tocheckand forced-site / site-issue /cgm_gaps-based OOR shading overlays toviz. Backfilled in memory if the parquets on disk predate enrichment; on-disk files are never mutated.
Shared backfill lives in ingestion/view_data.ensure_enriched; scripts/run_detection and both CLI commands all delegate to it.
Each CSV contains three sections separated by blank lines, each with its own header row:
- EGV (CGM readings):
DeviceType,SerialNumber,Description,EventDateTime,Readings (mg/dL)— lines 7–7413 - Manual BG:
DeviceType,SerialNumber,Description,EventDateTime,BG (mg/dL),Note— should be ignored per spec - Bolus data:
Type,BolusType,BolusDeliveryMethod,BG (mg/dL),SerialNumber,CompletionDateTime,InsulinDelivered,FoodDelivered,CorrectionDelivered,...— lines 7542+
The first 6 lines are a metadata header (device info, software version, report date). Ingestion must parse these sections separately.
data/— real patient CSV exports anddata/processed/*.parquet(do not commit new data files without asking)test_data/— anonymized copies for testingcore/— storage-agnostic Protocol library (core/schema.py,core/storage/)ingestion/— tconnectsync client, builders, enrichment, storage shim, view-mode helperdetection/— typed config +daily_featurespatterns-layer foundation.detection/legacy/holds the v1 reference implementation (not maintained, not imported from production code).detection/calibration/holds the M2 meal-rise scoring module.core/detection/— shared windowing helper and meal-rise detector (used by the live loop).apps/local/— local Streamlit OSS dashboard (day/heatmap/TIR, Plotly).apps/web/— Next.js personal dashboard (Vercel + Supabase, Phase A routes).apps/personal/cron/— live Dexcom poll → detect → Telegram alert loop;api/index.pyis the Vercel Python worker that invokes it.db/migrations/— Supabase schema and RLS migrations..github/workflows/— nightly Tandem sync, manual meal-rise fallback, smoke test, pytest CI.scripts/— CLI entry points (sanity_check,daily_viz,doctor)tests/— pytest suite (742 passed / 42 skipped / 48 deselected by default, plus 47 legacy-marked tests opt-in via-m legacy); Storage Protocol contract suite andcore/metrics/analytics tests undertests/core/docs/operating_docs/—TECHNICAL_SPEC.md,DATA_CATALOG.md,DATA_NOTES.md,DATA_NOTES_2.md,DATA_ISSUES.md,api_levels.md,tconnectsync_api_map.mddocs/updates/— dated session write-ups (YYYY-MM-DD-*.md); append-only audit logresearch.ipynb— exploratory analysis notebook
- Do not extend
detection/legacy/; do not import from it in production code. v2 modules land at the top level ofdetection/(ordetection/v2/if symmetry with legacy is preferred when v2 ships). - Never hardcode thresholds or personal parameters. All config lives in
config/user_config.yaml(seeTECHNICAL_SPEC.mdfor schema). Detection logic reads from config at runtime. - Real-time detection uses trailing window only — no future BG context available.
- Bump
ingestion.pipeline_version.PIPELINE_VERSION(and add a changelog entry) whenever a builder or enricher changes output schema or timestamp semantics in a way that invalidates existingdata/processed/*.parquet. Runuv run python main.py doctorto confirm on-disk data matches the code. - Python 3.12+ required. Dependencies managed with
uv(seepyproject.toml). - ML stack: scikit-learn, xgboost, lightgbm, statsmodels, scipy.