Root cause of the unreadable code editor: infra/marimo/marimo.toml had
`theme = "dark"` under [display], which causes marimo to apply the
.dark class to its app root. That triggers the rule
.dark, .marimo:is(.dark *) { color-scheme: dark }
and flips every token through light-dark() to its dark value, so our
editorial cream palette was being replaced at the root with dark navy
at render time. All the panels, outline sidebar, data table headers,
and code cells were unreadable because of this single setting.
Fix: flip to theme = "light" in infra/marimo/marimo.toml. That removes
the .dark class from the app root, CodeMirror uses its oneLight theme
instead of oneDark, and all our existing :root / .marimo token overrides
now actually reach descendant elements.
CSS changes:
- Remove the .dark / html.dark / body.dark selector hacks — unneeded
now that the root cause is fixed.
- Add a blanket fallback: `.cm-editor .cm-line [class^="ͼ"]` forces
any unmapped CodeMirror token class to var(--foreground) at
specificity (0,2,0). Specific token rules (.cm-editor .ͼq..ͼ15)
override by equal specificity + later source position. This means
token hashes can drift across marimo versions without regressing to
invisible text.
- Explicit font-family on every cm-editor descendant — JetBrains Mono
with a proper monospace fallback stack and font-feature-settings
disabled so the editor doesn't fall back to a proportional font
on any device.
Stack
Healthcare analytics platform on self-hosted infrastructure. Replaces dbt SQL models with narwhals DataFrame-agnostic expression functions, backed by DuckDB locally and Iceberg/Trino in the lakehouse. 28 services, 14 data pipelines, 12,634 tests at 99% coverage.
Quick start
# 1. Generate root key and seed .env
ROOT_KEY=$(openssl rand -hex 32)
cat > .env <<EOF
DOMAIN=fhirworx.io
HOST_IP=192.168.1.192
EOF
# 2. Start core services and bootstrap credentials
docker compose up -d postgres rustfs traefik
ROOT_KEY=$ROOT_KEY uv run python -m api.auth bootstrap $(git rev-parse HEAD)
# 3. Start everything
docker compose up -d
# 4. Verify
curl -s http://localhost:8000/health | python3 -m json.tool
Installation
The package supports skinny installs — install only the modules you need:
pip install stack[aco] # ACO analytics only
pip install stack[bib] # bibliography only
pip install stack[cli] # CLI (pulls aco + api + bib)
pip install stack[all] # everything
pip install stack[aco,aws] # ACO analytics with AWS storage
14 modules available as optional extras: aco, api, bcda, bib, bls, ccw, cli, cms, conf, opps, perf, pfs, rex, sem. Cloud providers: aws, azure, gcp. Aggregates: all, lake.
Services
All services route through Traefik at *.fhirworx.io.
| Service | URL | Purpose |
|---|---|---|
| API | api (internal) |
FastAPI — health, pipelines, bib endpoints |
| Notebooks | notebooks.fhirworx.io |
Marimo notebooks (GPU-accelerated) |
| Docs | docs.fhirworx.io |
Docusaurus — API reference + CMS bibliography |
| Gitea | git.fhirworx.io |
Git server, container registry, LFS |
| Zotero | zotero.fhirworx.io |
Reference manager (KasmVNC desktop) |
| Dashboard | fhirworx.io |
Service launcher |
| RustFS | s3.fhirworx.io |
S3-compatible object storage |
| Nessie | nessie.fhirworx.io |
Git-like Iceberg catalog |
| Polaris | polaris.fhirworx.io |
Iceberg catalog with governance |
| Trino | trino.fhirworx.io |
Distributed SQL engine |
| Grafana | grafana.fhirworx.io |
Dashboards |
| Prometheus | prometheus.fhirworx.io |
Metrics |
| Jaeger | jaeger.fhirworx.io |
Distributed tracing |
| Loki | loki.fhirworx.io |
Log aggregation |
Analytics platform
Architecture
CCLF/BCDA/CMS data
|
v
src/aco/express/ narwhals expressions (DataFrame-agnostic)
|
v
src/aco/pipe/ pipeline runner with schema validation
|
v
DuckDB (local) --or-- Iceberg + Trino (lakehouse) --or-- Databricks
|
v
src/api/ FastAPI (health, runs, bib)
|
v
notebooks/ Marimo interactive analysis
Pipelines
14 registered pipelines with 230 steps, validated by run_pipeline(steps, load) which enforces output schemas via pandera-style column checks.
| Pipeline | Steps | Domain |
|---|---|---|
ahrq_measures |
39 | AHRQ quality indicators |
cclf |
19 | CCLF connector pipeline -- raw CCLF files -> input_layer tables |
claims_preprocessing |
63 | Claim matching, encounter IDs |
cms_quality_measures |
20 | CMS ACO quality metrics |
core |
12 | Encounters, patients, providers |
data_quality |
1 | Validation and completeness |
hcc_suspecting |
6 | HCC risk adjustment |
input_layer |
13 | Raw CCLF/BCDA ingestion |
main |
14 | Full pipeline orchestration |
opps |
2 | Outpatient payment system |
pharmacy |
4 | Part D drug utilization |
provider_attribution |
10 | Provider-patient assignment |
quality_measures |
15 | Composite quality scores |
readmissions |
12 | 30-day readmission rates |
Expression layer
src/aco/express/ contains narwhals functions that work across polars, pandas, and cuDF. Each module mirrors a pipeline and exports pure transformation functions.
Configuration
stack.toml is the single config file. src/conf/ provides:
cfg— attribute access (cfg.db.aco)path("db.aco")— absolute path resolution relative to repo rootcontext()— environment switching (local/lake/databricks/trino/aws/gcp/azure)secret("api.secret", "STACK_API_SECRET")— env var with config fallbackstorage.get_filesystem()— multi-cloud storage abstraction (S3/GCS/ABFS/local)
Override context at runtime: STACK_CONTEXT=lake (or aws, gcp, azure).
Bibliography
src/bib/ manages a Zotero-backed citation store in data/bib.sqlite. Column-level provenance via Tag.col(ref, desc). The docs site exports library.json for a searchable bibliography browser at /library.
CI/CD
Backend switching
CI/CD workflows are auto-generated from stack.toml by gen_config.py. Three backends are supported:
| Backend | Workflows directory | When to use |
|---|---|---|
gitea |
.gitea/workflows/ |
Self-hosted Gitea instance (default) |
github |
.github/workflows/ |
GitHub repos / GitHub Actions |
woodpecker |
.woodpecker/ |
Woodpecker CI server |
Switch backends by editing stack.toml:
[ci]
backend = "gitea" # change to "github" or "woodpecker"
Then regenerate:
uv run python dev/scripts/gen_config.py # generate new workflows
uv run python dev/scripts/gen_config.py --check # verify (used in CI)
uv run python dev/scripts/gen_config.py --backend github # one-off override
The generator reads image definitions from stack.toml [images], dispatches to the active backend emitter (dev/scripts/backends/{backend}.py), writes workflow YAML, and auto-removes stale workflows from inactive backend directories.
Workflows
7 workflows generated per backend:
| Workflow | Trigger | What it does |
|---|---|---|
ci.yml |
Push/PR | Ruff lint + format, pytest (99% coverage), gen_config --check, skinny-install matrix |
deploy.yml |
Push to main | Build 5 images, Trivy scan, vuln reporting |
harden.yml |
Weekly cron / manual | Rebuild --no-cache, scan, auto-close/file vuln issues |
rebuild-all.yml |
Manual | Full rebuild + scan + deploy |
infra-ci.yml |
Path-filtered push | Hadolint Dockerfiles, dry-run builds |
release.yml |
Tag (v*) |
uv build + release |
pkg-supply-chain.yml |
Daily / push | Package inventory, mirror sync, drift detection, vuln scan |
Secret mapping
| Secret | Gitea | GitHub | Woodpecker |
|---|---|---|---|
| Registry auth | REGISTRY_USER + REGISTRY_TOKEN |
GITHUB_TOKEN (built-in) |
registry_username + registry_password |
| Gitea API | GITEA_TOKEN |
N/A | gitea_token |
| Vuln reporting | GITEA_TOKEN |
GITHUB_TOKEN |
gitea_token |
Image tagging
Images are tagged with the short commit SHA (8 chars). Compose resolves via ${COMMIT_SHA:-latest} from .env. Custom images: api, notebooks, zotero, docs, mc.
Vulnerability management
The harden.yml pipeline rebuilds all images with --no-cache to pick up OS patches, runs Trivy scans, and auto-files or auto-closes Gitea issues via api.diag.vuln.
Package supply chain
Daily automated pipeline (pkg-supply-chain.yml) that:
- Scans Dockerfiles, pyproject.toml, and CI workflows for all package dependencies
- Syncs packages to the Gitea package registry (local mirror)
- Detects drift between manifest and mirror
- Runs Trivy vulnerability scans on dependencies
- Auto-creates Gitea issues for missing packages and CVEs
Credential management
All 20 service credentials derive from a single 256-bit root key via HKDF-SHA256. No passwords stored in .env — they regenerate deterministically from root key + commit SHA on each deploy.
ROOT_KEY + commit_sha -> HKDF-SHA256 -> all credentials -> .env + backends
| Tier | Salt | Rotates |
|---|---|---|
| Bootstrap | b"bootstrap" |
Only when root key changes |
| Service | commit_sha |
Every deploy |
# Bootstrap (first time)
ROOT_KEY=$KEY uv run python -m api.auth bootstrap $(git rev-parse HEAD)
# Rotate (every deploy, automatic in CI)
ROOT_KEY=$KEY uv run python -m api.auth provision $COMMIT_SHA
# Dry-run
ROOT_KEY=$KEY uv run python -m api.auth derive $COMMIT_SHA --redact
Network segmentation
| Network | Type | Services |
|---|---|---|
gateway |
external | Traefik, all public-facing |
storage |
internal | PostgreSQL, RustFS |
data |
internal | Nessie, Trino, Polaris |
observability |
internal | Grafana, Prometheus, Jaeger, Loki |
ci |
external | Gitea, Act Runner, RustFS |
Design system
Theme: fhirworx (federal editorial). Traefik's inject-fhirworx middleware rewrites HTML to inject fhirworx.css and the favicon. Per-service CSS in assets/css/. Altair chart theme in assets/fhirworx.py (federal palette, Source Serif 4 / Playfair Display / JetBrains Mono).
Data lakehouse
| Component | Purpose |
|---|---|
| Nessie | Git-like branching and time travel for Iceberg tables |
| Polaris | Iceberg catalog with RBAC and multi-tenant governance |
| Trino | Distributed SQL (iceberg catalog) |
| RustFS | S3 storage backend (s3://lakehouse/) |
Query via Trino CLI, JDBC, Web UI, PyIceberg, or DuckDB+PyArrow.
Cloud deployment
The platform runs on self-hosted Docker Compose by default. Cloud provider guides in cloud/:
| Provider | Storage | Catalog | Compute | Context |
|---|---|---|---|---|
| Self-hosted | RustFS (S3) | Nessie / Polaris | Docker Compose | local / lake |
| AWS | S3 | Glue | ECS / EKS | aws |
| GCP | GCS | BigQuery | Cloud Run / GKE | gcp |
| Azure | ABFS | Unity Catalog | Container Apps / AKS | azure |
| Databricks | DBFS | Unity Catalog | Databricks Jobs | databricks |
Switch with STACK_CONTEXT=aws or edit stack.toml [context] active.
Databricks
Databricks Asset Bundle (databricks.yml) is auto-generated from the pipeline registry. 14 pipeline tasks with dependency ordering, 3 targets (dev/staging/prod), daily schedule.
uv run python dev/scripts/gen_config.py --dab-sql # generate SQL + DDL (~4 min)
databricks bundle deploy -t dev # deploy to Databricks
SDK integration
The platform uses 18 Databricks SDK services via UnityClient (wrapping WorkspaceClient):
| Category | Services | Module | Purpose |
|---|---|---|---|
| Catalog CRUD | catalogs, schemas, tables, volumes |
lake/unity.py |
Unity Catalog object management |
| Data loading | files, statement_execution |
lake/sync.py |
Parquet upload + COPY INTO |
| Governance | grants |
lake/governance.py |
Declarative schema permissions with drift audit |
| Secrets | secrets |
lake/unity.py |
Scope management, env var sync |
| Constraints | table_constraints |
lake/unity.py |
PK/FK on claims tables for lineage |
| Jobs | jobs |
lake/jobs.py |
Create/run/monitor pipeline jobs from Python |
| Compute | warehouses |
lake/unity.py |
Find/create/start/stop warehouses by name |
| Quality | quality_monitors |
lake/quality.py |
Lakehouse Monitoring profiles |
| Audit | system_schemas |
lake/unity.py |
Table lineage and audit log queries |
Governance
src/aco/lake/governance.py provides declarative grant management:
from aco.lake.governance import GovernancePolicy, GrantRule, apply_governance
policy = GovernancePolicy(
catalog="aco",
rules=[
GrantRule(group="analysts", schemas=["readmissions"], privileges=["SELECT"]),
GrantRule(group="pipeline_svc", schemas=["*"], privileges=["ALL_PRIVILEGES"]),
],
phi_schemas=["core", "claims_preprocessing", "cclf"],
)
# Preview changes
apply_governance(client, policy, dry_run=True)
# Detect drift between declared and actual grants
from aco.lake.governance import audit_governance
drifts = audit_governance(client, policy)
PHI schemas, group privilege mappings, and quality monitor config are declared in stack.toml:
[databricks.governance]
phi_schemas = ["core", "claims_preprocessing", "cclf", "input_layer"]
[databricks.governance.groups.analysts]
schemas = ["readmissions", "quality_measures"]
privileges = ["SELECT"]
Job orchestration
src/aco/lake/jobs.py translates Pipeline objects into Databricks Jobs:
from aco.lake.jobs import JobManager
mgr = JobManager(client, catalog="aco")
job_id = mgr.create_pipeline_job("readmissions", schedule="0 0 6 * * ?")
run_id = mgr.run_now(job_id)
status = mgr.get_run_status(run_id)
Secret sync
Push environment variables to Databricks secret scopes per stack.toml [databricks.secrets]:
uv run python dev/scripts/sync_secrets.py --dry-run # preview
uv run python dev/scripts/sync_secrets.py # push to Databricks
uv run python dev/scripts/sync_secrets.py --list # show current secrets
Quality monitoring
from aco.lake.quality import setup_monitors
setup_monitors(client, catalog="aco", schemas=["core", "claims_preprocessing"])
Testing
Architecture
Testing follows a semantic coverage model (src/sem/) rather than tracking raw line numbers. The system parses every .py file into an AST, builds stable semantic nodes (functions, branches, exception handlers, loops), and attaches three signal layers:
source → AST → semantic nodes → attach (ruff, ty, coverage) → planner
| Layer | Module | What it does |
|---|---|---|
| Parse | sem.parse |
Walk AST, emit SemanticNode per function, branch, except, loop, return, raise |
| Enrich | sem.enrich |
Run ruff check and ty check, map diagnostics to tightest-span node |
| Runtime | sem.runtime |
Map coverage.py JSON report onto nodes (hit/miss, per-test contexts) |
| Plan | sem.plan |
Score uncovered nodes by priority, suggest next test targets |
| State | sem.state |
Persist node status across runs, reset on source hash change |
Each node has a stable identity derived from its module path, qualified symbol name, kind, and ordinal position — never from line numbers:
app.config::load_settings::branch_if[1]
aco.express.pharmacy::pharmacy_claims::except_handler[0]
Priority heuristic
priority =
uncovered_branch × 5
+ ty_diagnostic × 4
+ inside_partially_tested_function × 3
+ uncovered_exception_path × 3
+ ruff_warning × 2
− prior_failures × 2
− already_covered × 5
Structural invariants
tests/test_ast_coverage.py runs at collection time (pure AST, no imports) and enforces:
- Every
@nw.narwhalifyfunction has a docstring - Every
Expr(...)call suppliesname,fn,output, andafter - Every public Pydantic model is importable
- Every
pipe/*.pymodule exports aPipelinewith non-empty.exprs - Implementation ratio stays above baseline (ratchet)
express/andpipe/modules are symmetric
Pre-commit hooks
Git hooks live in dev/hooks/ (tracked) and are activated via:
git config core.hooksPath dev/hooks
The shell hook is a 3-line wrapper. All logic lives in src/sem/hooks.py:
#!/usr/bin/env bash
exec uv run python -m sem.hooks
Smart test selection
The hook classifies staged files and runs only what is relevant:
| What changed | Tests run | Why |
|---|---|---|
src/sem/*.py |
tests/sem/ + test_ast_coverage.py |
Module tests + structural invariants |
src/aco/*.py + src/sem/*.py |
tests/aco/ + tests/sem/ + structural |
Both module test dirs |
tests/bib/test_sync.py |
tests/bib/ |
Changed test dir |
pyproject.toml or conftest.py |
Full suite (12,634+ tests) | Infrastructure change |
notebooks/pfs_calcs.py |
marimo check + notebook execution |
Notebook validation only |
README.md only |
Nothing | No testable changes |
The mapping rule is: src/<module>/ changes → tests/<module>/ runs. Any src/ change also triggers test_ast_coverage.py to verify structural invariants haven't regressed.
Forcing full suite
GIT_PRE_COMMIT_FULL=1 git commit -m "message"
Hook steps (in order)
- Venv recovery — if
uv run python -c 'import sys'fails, runuv sync --dev - Config regeneration — if
stack.tomlorgen_config.pychanged, regenerate CI workflows - Ruff lint + format — only staged
.pyfiles - AST parse check — verify staged source files have valid syntax
- Pytest — targeted or full suite based on what changed
- Marimo check — only if notebooks are staged
- Notebook execution — only staged notebooks in the safe-to-run list
Post-commit
dev/hooks/post-commit rebuilds the docs Docker image in the background after every commit so the documentation site stays current with docstring changes.
Running tests
uv run python -m pytest tests/ # full suite
uv run python -m pytest tests/sem/ # one module
uv run python -m pytest tests/ -m "not stub" # skip stub inventory
uv run python -m pytest tests/ -m stub # only stub status
uv run python -m pytest tests/test_ast_coverage.py # structural invariants only
README generation
README.md is generated from README.md.j2 using live codebase data. Never edit README.md directly.
uv run python dev/scripts/gen_readme.py # regenerate from template
uv run python dev/scripts/gen_readme.py --check # verify freshness (CI gate)
uv run python dev/scripts/gen_readme.py --dry-run # preview to stdout
Dynamic values pulled from: compose.yml (services), pyproject.toml (modules), pipeline registry (pipelines + steps), pytest --collect-only (test count), api.auth.manifest (credentials), .gitea/workflows/ (workflow count).
Project layout
stack/
├── compose.yml Docker Compose (28 services)
├── stack.toml Centralised configuration
├── pyproject.toml Python project (uv, optional deps per module)
├── src/
│ ├── aco/ ACO analytics (express, pipe, table, lake, load)
│ ├── api/ FastAPI server + auth + diag
│ ├── bcda/ BCDA FHIR R4 client
│ ├── bib/ Zotero bibliography store, tags, metadata
│ ├── bls/ BLS data
│ ├── ccw/ CCW data dictionary
│ ├── cli/ CLI entry point (typer)
│ ├── cms/ CMS public data tables
│ ├── conf/ Config loader, storage abstraction, table base
│ ├── opps/ Outpatient Prospective Payment System
│ ├── perf/ Pipeline telemetry (OpenTelemetry)
│ ├── pfs/ Physician Fee Schedule
│ ├── rex/ REX fixed-width file processing
│ ├── sem/ Semantic coverage orchestration
│ └── zot/
├── infra/ Service configs and Dockerfiles
│ ├── images/ All Dockerfiles (api, notebooks, zotero, docs, mc)
│ ├── traefik/ Reverse proxy + fhirworx CSS injection
│ ├── grafana/ Dashboards and datasource provisioning
│ ├── prometheus/ Metrics collection
│ ├── loki/ Log aggregation
│ └── ... coredns, nginx, trino, polaris, rustfs, act-runner, gitea
├── assets/ Branding, styles, generated artifacts
│ ├── css/ Per-service CSS (dashboard, gitea, marimo, woodpecker)
│ ├── icons/ Favicons, logos, coverage badge
│ └── fhirworx.py fhirworx chart palette and Altair theme
├── cloud/ Cloud provider deployment guides
│ ├── self-hosted/ Docker Compose (default)
│ ├── aws/ S3, RDS, Glue, ECS
│ ├── gcp/ GCS, Cloud SQL, BigQuery, Cloud Run
│ └── azure/ ABFS, Azure SQL, Unity Catalog, Container Apps
├── dev/ Dev tooling
│ ├── scripts/ Code generators, bootstrap, supply chain tools
│ ├── hooks/ Git hooks (tracked, core.hooksPath = dev/hooks)
│ ├── seeds/ Reference data (BCDA samples, CMS docs)
│ └── pipelines/ CI-agnostic pipeline specs
├── tests/ 12,634 tests at 99% coverage
├── notebooks/ Marimo notebooks
├── docs/ Docusaurus site
├── data/ DuckDB, bib.sqlite, BCDA/CMS data, zotero (gitignored)
└── .env Derived credentials (not in git)
Prerequisites
- Docker (rootless mode)
- NVIDIA GPU with container toolkit
- Domain or
/etc/hostsentries for*.fhirworx.io
# Fix for rootless NVIDIA containers
sudo sed -i 's/#no-cgroups = false/no-cgroups = true/' /etc/nvidia-container-runtime/config.toml
SSH access
# ~/.ssh/config
Host gitea
HostName fhirworx.io
Port 2222
User git
IdentityFile ~/.ssh/gitea_ed25519
IdentitiesOnly yes