-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathMakefile
More file actions
260 lines (221 loc) · 11.9 KB
/
Copy pathMakefile
File metadata and controls
260 lines (221 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
.PHONY: install dev db-up db-down db-reset lint typecheck test test-unit test-int test-contract \
test-noio test-db test-coverage store-durations diff-coverage fmt clean help \
migrate-status migrate-apply migrate-new migrate-hash precommit precommit-run \
arch-check arch-show docs-stage docs-build docs-serve openapi-snapshot \
record-dispositions mutmut-audit mutmut-browse restore-drill
API_DIR := apps/api
COMPOSE := docker compose -f infra/docker-compose.yml
ATLAS_DIR := infra/atlas
LOCAL_DB_URL ?= postgres://cora:cora@localhost:5432/cora?sslmode=disable
help:
@echo "Common targets:"
@echo " install Install Python deps via uv (in apps/api)"
@echo " dev Run FastAPI dev server (reload, :8000)"
@echo " db-up Start Postgres + pgvector via Docker Compose"
@echo " db-down Stop Postgres"
@echo " db-reset Stop Postgres and wipe its volume"
@echo " migrate-status Show pending migrations against local DB"
@echo " migrate-apply Apply pending migrations to local DB"
@echo " migrate-new Generate a new migration skeleton (name=<short_name>)"
@echo " migrate-hash Recompute atlas.sum after editing migrations by hand"
@echo " restore-drill Destroy + restore a THROWAWAY cluster (:5433), verify the event store"
@echo " lint Run ruff check + format check"
@echo " fmt Run ruff format and auto-fix"
@echo " typecheck Run pyright (strict)"
@echo " test Run all tests"
@echo " test-unit Run only unit tests"
@echo " test-int Run only integration tests"
@echo " test-contract Run only contract tests"
@echo " test-noio Run the no-DB CI lane (unit + architecture + contract, in-memory)"
@echo " test-db Run the DB CI lane (integration + e2e; needs db-up)"
@echo " test-coverage Run all tests with coverage report (term + html + xml)"
@echo " store-durations Record per-test timings into .test_durations to balance CI shards"
@echo " diff-coverage Run diff-cover against origin/main (fails if patch <90%)"
@echo " arch-check Tach dependency contract + architecture fitness-function tests"
@echo " arch-show Open the dependency graph (tach show)"
@echo " openapi-snapshot Regenerate apps/api/openapi.json from create_app()"
@echo " record-dispositions Regenerate the record-export redaction table"
@echo " mutmut-audit Run mutmut against Access BC deciders/evolver (audit cadence, ~5-15 min)"
@echo " mutmut-browse Open mutmut's interactive TUI to triage surviving mutants"
@echo " precommit Install pre-commit hooks (one-time per clone)"
@echo " precommit-run Run all pre-commit hooks against all files"
@echo " docs-stage Stage README + CONTRIBUTING into docs/ (link rewrites for site)"
@echo " docs-build Stage + build the static site into ./site"
@echo " docs-serve Stage + serve the docs site locally on :8001"
@echo " clean Remove caches and build artefacts"
install:
cd $(API_DIR) && uv sync --all-extras
dev: db-up
cd $(API_DIR) && uv run uvicorn cora.api.main:app --reload --host 0.0.0.0 --port 8000
db-up:
$(COMPOSE) up -d postgres
db-down:
$(COMPOSE) down
db-reset:
$(COMPOSE) down -v
$(COMPOSE) up -d postgres
lint:
cd $(API_DIR) && uv run ruff check src tests tools
cd $(API_DIR) && uv run ruff format --check src tests tools
fmt:
cd $(API_DIR) && uv run ruff check --fix src tests tools
cd $(API_DIR) && uv run ruff format src tests tools
typecheck:
cd $(API_DIR) && uv run pyright src tests tools
# pytest-xdist with `--dist=worksteal -n 4`: worksteal is the
# scheduler-of-choice for mixed-duration suites (50ms unit alongside
# 200ms+ integration). Worker count is empirically tuned: on the 8-core
# dev Mac, `-n 4` finished coverage in 9:08 while `-n 8` took 13:10
# *and* tripped a flaky asyncio concurrency test — the suite is
# I/O-bound on per-worker Postgres, so 8 workers oversubscribe Docker
# and asyncpg. `-n 4` also matches ubuntu-latest CI's 4-core runner
# exactly. Each worker brings up its own Postgres container (see
# tests/conftest.py); the per-test template-DB clone parallelizes
# cleanly. Raise the cap empirically if a future I/O speedup (tmpfs
# Docker volume, faster runner class) unblocks worker scaling.
#
# Kept out of `[tool.pytest.ini_options].addopts` so ad-hoc single-file
# runs (`uv run pytest tests/unit/foo.py`) stay sequential and avoid
# worker-spawn overhead. Make targets opt in.
PYTEST_PARALLEL := -n 4 --dist=worksteal
test:
cd $(API_DIR) && uv run pytest $(PYTEST_PARALLEL)
test-unit:
cd $(API_DIR) && uv run pytest $(PYTEST_PARALLEL) -m unit
test-int:
cd $(API_DIR) && uv run pytest $(PYTEST_PARALLEL) -m integration
test-contract:
cd $(API_DIR) && uv run pytest $(PYTEST_PARALLEL) -m contract
# Local mirrors of the two CI test lanes (see .github/workflows/ci.yml).
# Path-based selection matches CI: it is the robust selector since some
# helper/__init__ files carry no marker. test-noio starts no Postgres
# container (APP_ENV=test gives in-memory adapters); test-db needs `db-up`.
test-noio:
cd $(API_DIR) && uv run pytest $(PYTEST_PARALLEL) tests/unit tests/architecture tests/contract
test-db:
cd $(API_DIR) && uv run pytest $(PYTEST_PARALLEL) tests/integration tests/e2e
test-coverage:
cd $(API_DIR) && uv run pytest $(PYTEST_PARALLEL) --cov --cov-report=term-missing --cov-report=html --cov-report=xml
# Record per-test execution times so pytest-split balances CI shards by
# time instead of by count. Runs the FULL suite (needs db-up) and writes
# apps/api/.test_durations; commit the result. Re-run after large test
# additions; staleness costs shard balance, never correctness.
store-durations:
cd $(API_DIR) && uv run pytest $(PYTEST_PARALLEL) --store-durations
diff-coverage:
cd $(API_DIR) && uv run diff-cover coverage.xml --compare-branch=origin/main --fail-under=90
arch-check:
cd $(API_DIR) && uv run tach check
cd $(API_DIR) && uv run pytest tests/architecture
arch-show:
cd $(API_DIR) && uv run tach show
# Regenerate the committed OpenAPI snapshot after intentional API surface
# changes. The drift test (tests/architecture/test_openapi_drift.py) fails
# until this is run and the diff is reviewed in the PR.
openapi-snapshot:
cd $(API_DIR) && APP_ENV=test uv run python -c "import json; from cora.api.main import create_app; \
f = open('openapi.json', 'w'); json.dump(create_app().openapi(), f, indent=2, sort_keys=True); f.write('\n'); f.close()"
# Regenerate the record-export redaction disposition table after adding
# an event type, adding a field, or changing a field's declared type.
# The drift test (tests/architecture/test_record_dispositions_drift.py)
# fails until this is run and the diff is reviewed in the PR, and that
# diff is the list of what a published record would disclose.
#
# The generator ABORTS on an annotation it cannot classify. That is the
# design, not a bug: an unrecognised type is a question about the model.
# Pass --survey to list every such annotation in one pass instead of
# hitting them one exception at a time.
record-dispositions:
cd $(API_DIR) && uv run python tools/gen_record_dispositions.py
# Mutation testing audit. Pure-logic scope via the CLI wildcard pattern
# (Access BC deciders + evolver only). [tool.mutmut] in apps/api/pyproject.toml
# carries the test-selection + runner config. Audit-only — not per-PR.
# Expect ~5-15 min for the first run on Linux; resumable across invocations
# (state lives in apps/api/mutants/, gitignored). Override the source scope
# via `MUTMUT_SCOPE=cora.recipe.* make mutmut-audit`. NOTE: MUTMUT_SCOPE only
# changes which code is mutated; the pytest test selection is pinned to
# tests/unit/access in [tool.mutmut] (mutmut 3.5 has no CLI override for it).
# To audit a DIFFERENT BC end-to-end you must also point that test selection
# at tests/unit/<bc>. The nightly per-BC rotation workflow
# (.github/workflows/mutmut.yml) does both automatically on a Linux runner;
# that is the supported multi-BC path.
#
# macOS caveat (observed 2026-05-20): mutmut's per-mutant subprocess wrapper
# reports every result as `segfault` on darwin even when the same mutant
# is killable by the test suite when invoked manually. The forks are not
# clean on macOS (consistent with the mutmut README's macOS / setproctitle
# warning). Run audits on a Linux runner (CI, devcontainer, or remote box)
# to get meaningful survivor counts. The Makefile target works on both
# OSes; only the reliability of the reported results differs.
#
# Pre-step: mutmut copies src/ + tests/ + pyproject.toml into mutants/ but
# does NOT copy README.md, which hatchling requires for package metadata
# validation. Seeding README.md keeps the subprocess pytest from crashing
# on `OSError: Readme file does not exist` before tests start. The
# `also_copy = ["README.md"]` setting in pyproject did NOT work in mutmut
# 3.5; copy explicitly here.
MUTMUT_SCOPE ?= cora.access.aggregates.actor.evolver.* cora.access.features.register_actor.decider.* cora.access.features.deactivate_actor.decider.*
mutmut-audit:
@mkdir -p $(API_DIR)/mutants
@cp -n $(API_DIR)/README.md $(API_DIR)/mutants/README.md 2>/dev/null || true
cd $(API_DIR) && uv run mutmut run $(MUTMUT_SCOPE)
# Interactive triage: review surviving mutants, mark equivalents, retest
# after fixing the suite. See https://mutmut.readthedocs.io for keys.
mutmut-browse:
cd $(API_DIR) && uv run mutmut browse
precommit:
cd $(API_DIR) && uv run pre-commit install
precommit-run:
cd $(API_DIR) && uv run pre-commit run --all-files
migrate-status:
cd $(ATLAS_DIR) && DATABASE_URL=$(LOCAL_DB_URL) atlas migrate status --env local
migrate-apply:
cd $(ATLAS_DIR) && DATABASE_URL=$(LOCAL_DB_URL) atlas migrate apply --env local
migrate-new:
@if [ -z "$(name)" ]; then echo "Usage: make migrate-new name=add_foo"; exit 1; fi
cd $(ATLAS_DIR) && DATABASE_URL=$(LOCAL_DB_URL) atlas migrate new $(name)
migrate-hash:
cd $(ATLAS_DIR) && atlas migrate hash
# Backup and point-in-time recovery drill. Stands up an ISOLATED cluster
# (infra/backup/docker-compose.backup.yml, port 5433) with WAL archiving
# on, destroys it, restores it to a chosen moment via pgBackRest, and
# verifies the event store at the domain level: stream fold digests,
# commit order, and the Atlas revision. The cluster it destroys is one it
# created: the destructive step can reach only the drill's own volumes,
# never `cora-pg-data` behind the dev database. Needs docker, atlas and
# psql on PATH; takes a few minutes.
#
# Override the published port if 5433 is taken:
# CORA_DRILL_PORT=5439 make restore-drill
#
# See the Recovery section of docs/stack/deployment.md.
restore-drill:
python3 scripts/restore_drill.py
# `atlas migrate lint` was moved behind atlas-cloud login in v0.38; the
# project deliberately skips that path. CI runs a narrow grep-based
# safety scan on new migrations (see .github/workflows/ci.yml). Locally,
# read your migration carefully and `make migrate-apply` against a
# scratch DB before merging — that catches the same class of issues
# `lint` would flag (data loss, locking-prone DDL).
# Docs site (mkdocs-material) — published to xmap.github.io/cora/ via
# .github/workflows/docs.yml on every push to main. mkdocs runs through an
# ephemeral `uv run` env so the build deps are always present and pinned to
# match CI: mkdocs-material plus pyyaml + pydantic, which back
# scripts/beamline_descriptor.py (the on_files hook in scripts/mkdocs_hooks.py
# imports it to render the deployment beam-path page). No global install needed;
# uv caches the env after the first run.
MKDOCS = uv run --no-project \
--with "mkdocs-material==9.7.6" \
--with "pyyaml>=6,<7" \
--with "pydantic>=2.13.4,<3" \
mkdocs
docs-stage:
python3 scripts/stage_docs.py
docs-build: docs-stage
$(MKDOCS) build --strict
docs-serve: docs-stage
$(MKDOCS) serve -a localhost:8001
clean:
cd $(API_DIR) && rm -rf .pytest_cache .ruff_cache .pyright_cache build dist *.egg-info
find . -type d -name __pycache__ -exec rm -rf {} +
rm -rf site docs/reference/contributing.md