Dev Log¶
A running, dated log of what was built and what was learned — newest first.
2026-07-02 — Real Kafka stream source ✅¶
- Wearable stream now reads
format("kafka")from a local Docker KRaft broker (single-node, no Zookeeper). Akafka-pythonproducer publishes 15169 events to thewearablestopic; the Spark consumer runs the sameclean_wearablestransform → same parquet sink as the file path. make stream-parityconfirms cleaned output is identical (15169 events, file == kafka) — concrete proof the source swap changes nothing downstream. Closes the "compatible but not exercised" gap. (ADR 0010)
2026-07-01 — Great Expectations silver DQ gate ✅¶
- Great Expectations (GX Core 1.x) is now the gating DQ contract for silver. A code-defined suite (
src/vitals/dq.py) validates coded-vocabulary value-sets (everyicd10_code∈ the ICD-10 set,observation.metric∈ the standard set, glucoseunit_std==mg/dL), the PHI boundary column set, and ranges + key uniqueness — exits non-zero on any violation. - CI runs it after
make build: the gate can't be skipped. Complements (not replaces) the descriptivedq_report.json; dbt tests still gate gold. (ADR 0009)
2026-06-30 — Feast feature store made real ✅¶
- Feast was scaffolded but never applied. Now materialized offline→online (sqlite):
get_online_features(low-latency inference path) + point-in-time historical retrieval (get_historical_featuresover an entity dataframe — the leakage-safe training join). Both paths parity-checked against the offline parquet (NULL-aware, float-tolerant).make feast-demo. - Production would point the offline store at Databricks/Delta — noted, not exercised (local is the deliverable). (ADR 0008)
2026-06-30 — Full-medallion job on Databricks ✅¶
- Bronze + silver are now in the Asset Bundle job as a
python_wheel_task(medallion_ingest). One scheduled serverless run does generate → bronze Delta → silver Delta → gold (dbt + 29 tests) → drift monitor, no laptop. Verified TERMINATED SUCCESS:medallion_ingest(bronze=28816, silver=27402) →gold_dbt→drift_monitor. (ADR 0005 Update) - Three Free-Edition lessons learned live: ship a lean wheel (core deps →
localextra); pin the compute Python version (env version 3 / Python 3.12); branch dbt dialect ontarget.type(metricflow_time_spineneededrange()on DuckDB vssequence()+explode()on Spark).
2026-06-29 — dbt semantic layer + real pgvector RAG ✅¶
- MetricFlow semantic layer: 7 composable metrics (
surgery_rate,avg_conservative_spend, …) declared in YAML over a newfct_patient_metricsper-patient base.mf queryresults parity-proven against the marts;make metrics-query. (ADR 0007) - pgvector replaces the TF-IDF placeholder: Docker (
pgvector/pgvector:pg16), fastembedbge-small-en-v1.5(384-d ONNX/CPU, no API keys), HNSW cosine index, idempotent upsert. TF-IDF path remains the fallback when the store orvectorextra is absent.make rag-up / rag-index / rag-query. (ADR 0006)
2026-06-26 — Databricks deploy path + ops hardening ✅¶
- Asset Bundle job (
databricks.yml) ships gold as a scheduled serverless job (make bundle-deploy/bundle-run, verifiedTERMINATED SUCCESS). Dev path stays databricks-connect for fast iteration; the bundle path is the "how this ships in a real shop" answer — two modes, one codebase behind a target switch. - Failure alerts (
on_failureemail, address injected at deploy time — no address committed to this public repo) and drift monitoring as a job task (drift_monitorspark_python_taskruns downstream ofgold_dbt, scores PSI feature-drift on every run, appends tovitals_gold.monitoring.drift_report). - Hermetic CI gate (
.github/workflows/ci.yml): ruff + unit tests + full local pipeline, on every push. (ADR 0005)
2026-06-23 — Phase 4: governance & polish ✅¶
- Drift monitoring (
monitoring.py): PSI per feature, reference vs current. Stable on a natural split; correctly flags an injected population shift (pain/ODI/activity → significant). - Auto-generated data dictionary + lineage (
catalog.py) from dbt's manifest/catalog — a Mermaid lineage graph (10 models, 17 edges) + per-column dictionary that can't drift from the code. - Governance page: PHI classification, the silver de-id boundary, and the Unity Catalog production mapping.
- ADRs (
docs/adr/) for the four non-obvious decisions (DuckDB-vs-Databricks, de-id, OMOP, three-store gold); summarized in the vault.
2026-06-23 — Phase 3: streaming + Spark at scale ✅¶
- Wearables now also flow through a Spark Structured Streaming job: file source → cleaned
Parquet sink with checkpointing,
trigger(availableNow). 15,169 events streamed, 448 outliers nulled on the fly. Production swaps the source to Kafka — one line, identical downstream. - Added a PySpark-at-scale batch transform with a window function (7-obs rolling pain per patient) — the Databricks scale path for the silver logic (1,631 rows).
- Infra note: Spark 4 needs JDK 17/21 (not 24); the modules auto-select an installed 17/21 JDK.
2026-06-23 — Phase 2: multi-source ingestion ✅¶
- Added three source types through bronze→silver→dbt gold: claims (837/835-style, 1,510), PRO surveys (Oswestry Disability Index, 1,718), wearables (daily batch, 15,169).
- Each with its own injected mess + silver fix: billed-as-string → numeric (96% recovered), out-of-range ODI clamped (0 remaining), outlier step counts nulled (0 remaining).
- New dbt models:
fct_claim,fct_pro,fct_wearable_daily, andmart_cost_outcomes(a value-based-care view: conservative spend, imaging rate, surgery rate per condition). - Leakage guard: claims contain only conservative-care CPTs (office, MRI, PT, injection) — no surgery codes — so they predict the future outcome without leaking it.
- Feature store grew to 20 features across 4 sources; demo model uses a curated 10 (feature selection). dbt now 1 seed + 10 models + 26 tests, all passing.
2026-06-23 — Phase 2 begins: OMOP CDM ✅¶
- Conformed silver into the OMOP Common Data Model in dbt:
omop_person,omop_condition_occurrence,omop_measurement(600 / 600 / 5,303 rows). - Source codes mapped to standard concepts via a dbt seed (
concept_map.csv): ICD-10 → condition concepts, LOINC → measurement concepts, gender → 8507/8532. Referential integrity tested. - dbt now: 1 seed + 6 models + 18 tests, all passing.
- Next: widen sources (claims 837/835, PRO surveys, wearable batch) and expand features.
2026-06-23 — Phase 1 MVP slice: working end-to-end ✅¶
- Built the full vertical slice: generate → bronze → silver → dbt gold → serve, runnable with
one command (
make run). See Results for the real numbers. - Bronze: a seeded generator emits FHIR-shaped NDJSON with deliberate mess (dupe patients, mixed glucose units, free-text conditions, schema drift, missing values) — so the cleaning layer has real work to show.
- Silver: de-identify (PHI dropped at the boundary, with a build-failing assertion), flatten
FHIR, standardize glucose mmol/L→mg/dL, recover ICD-10 from free text (123 conditions), dedupe.
Data-quality report written to
data/dq_report.json. - Gold (dbt):
dim_patient,fct_observation,mart_condition_outcomes— 3 models + 8 tests, all passing. - Serve: a 600×8 feature store (offline table + Parquet + Feast repo), a TF-IDF vector index with a working RAG query, and a surgery-risk model (ROC-AUC 0.825) tracked in MLflow.
- Engineering decision: the MVP runs on DuckDB for one-command reproducibility; Databricks /Delta is the documented deployment target and PySpark the Phase-3 scale path. Runnable > impressive-but-dead.
- Next (Phase 2): widen sources (claims, PRO, wearable batch) and land the OMOP CDM.
2026-06-23 — Project kickoff¶
- Defined the project, locked the architecture (medallion + healthcare layer), scaffolded the repo, tooling (mise + uv), and this documentation site.