Keep a scientific Python project boring enough to reproduce.
The repository has one canonical gate: make verify. Both a developer and GitHub Actions build .devcontainer/Dockerfile and run the same locked checks. The host checkout is mounted read-only; verification copies it to /tmp/work before installing or generating artifacts.
Git checkout (read-only)
│
▼
.devcontainer/Dockerfile
│
├── Dev Container
│
└── make verify ── GitHub Actions
│
▼
/tmp/work
│
uv sync --frozen (after initialization)
│
ruff → pytest → Sphinx → smoke
Importable code lives under src/<module>/. Tests import the installed package, not files by relative path.
uv.lock is generated by scripts/init_project.py after selecting a profile. Commit the lockfile. When it exists, make verify uses uv sync --frozen. The profile-neutral template can run without a lock and prints an explicit warning while resolving from pyproject.toml; generated research projects should keep the lockfile.
.devcontainer/Dockerfile is used for interactive development and canonical verification. CI does not maintain a second package-install recipe.
make verify mounts the checkout at /work:ro, copies it into /tmp/work, and performs all installs/builds there. This catches tools that unexpectedly write into source while avoiding root-owned host artifacts.
The initializer offers generic, eda, ml, and api. A profile only chooses the first dependency/harness needed to prove the project works end-to-end. It is not an application framework and can be replaced immediately.
Container verification is the reproducibility proof but is slower. make local-verify runs equivalent checks in the current uv environment for fast iteration. CI intentionally uses only make verify.