# MESH_SILOS Makefile
#
# Canonical release check: ``make verify`` runs six targets:
#   1. ``make test``     - the full test suite (hands off to pytest when
#                          present; see scripts/run_tests.py)
#   2. ``make air-gap``  - the orchestrator refuses outbound sockets
#   3. ``make demo``     - ``mesh_silos verify`` end to end, which pulls in
#                          ``make fixtures`` first
#   4. ``make budget``   - the written performance budgets, as a gate
#   5. ``make coverage`` - the suite under coverage, gated on COVERAGE_MIN
#   6. ``tools/_audit_stubs.py`` - no dead-end or phantom-export stubs
#
# This list used to be four items and named a "SHA-256 chain-of-custody
# over the artefacts" as the last one. There is no such step in `verify`
# and there never was: the custody tooling is `tools/sign_release.py`, and
# it belongs to `make sign`, which needs a GPG key. A header that
# describes a check the target does not run is the same defect as a
# document claiming a capability the code lacks, and the fix here was to
# describe what the target does rather than to imply a step nobody
# implemented.
#
# This check does not build or inspect the wheel. That is
# `python tools/ci.py frontend` plus ``out/_build_and_inspect.py`` plus
# ``tools/_verify_wheel_install.py``, and it is described as unrun in
# docs/LOOSE_ENDS.md.
#
# Targets are deliberately portable. ``make`` (GNU Make) is the only
# external dependency on POSIX; on Windows, ``make`` resolves to the
# one shipped with Git for Windows or with Strawberry Perl.

SHELL := /usr/bin/env bash
PY   ?= python
PYTEST ?= $(PY)

OUT  ?= out
VENV ?= .venv

#: Coverage floor, in percent. Measured when the gate was added, re-measured
#: after `.coveragerc` was fixed, and raised in v0.8.67 when the training half of
#: the package stopped being untested: **84 %** of the whole shipped package
#: (9 897 statements, 1 624 missed). The floor is set one point below the
#: measurement so a platform-specific branch does not fail an unrelated change.
#:
#: It is a floor, not the aspiration: it moves up as coverage rises and never
#: down.
#:
#: Why not 85, in one line: `slm/train/` still needs a GPU to finish, so
#: `qlora.py` (55 %), `merge.py` (17 %) and `hf2gguf.py` (25 %) are as far as
#: CPU-only tests can reach. `.coveragerc` used to exclude three of them, which
#: read 84.5 % — that exclusion is gone, because all four import cleanly here and
#: are therefore untested rather than untestable. The fix was tests, not an
#: exemption, and the gap is now attributable: see docs/FINE_TUNE.md.
COVERAGE_MIN ?= 83

.DEFAULT_GOAL := help

.PHONY: help
help:
	@echo "mesh_silos Makefile"
	@echo ""
	@echo "Targets:"
	@echo "  verify   - run the canonical release check (test + air-gap + demo + budget + coverage)"
	@echo "  test     - run the stdlib test suite (with coverage if available)"
	@echo "  demo     - run 'mesh_silos verify' end-to-end into \$$OUT"
	@echo "  fixtures - build / rebuild the synthetic bank fixture under \$$OUT/fixtures"
	@echo "  frontend - install + build the Next.js frontend"
	@echo "  sbom     - regenerate the CycloneDX SBOM (CycloneDX-lite)"
	@echo "  sign     - GPG-sign the release manifest (requires gpg)"
	@echo "  clean    - remove \$$OUT"
	@echo "  air-gap  - verify the orchestrator refuses outbound sockets"
	@echo "  budget   - run the written performance budgets (rows, tables, statements, API bytes)"
	@echo "  coverage - run the suite under coverage, gated on COVERAGE_MIN"
  @echo "  docs     - citations, quantities and reachability of every living document"
	@echo "  e2e-live - the product against live database engines (needs servers; SKIPs without)"
  @echo "  e2e-browser - the five browser journeys (needs a browser; SKIPs without)"
	@echo "  venv     - create a local virtualenv"

.PHONY: venv
venv:
	@if command -v uv >/dev/null 2>&1; then \
		uv venv $(VENV) 2>/dev/null; \
		uv pip install --python $(VENV)/bin/python ".[all]"; \
	else \
		$(PY) -m venv $(VENV); \
		$(PY) -m pip install ".[all]"; \
	fi

.PHONY: fixtures
fixtures:
	@$(PY) tests/fixtures/synthetic_bank/build_fixture.py --out-dir $(OUT)/fixtures

.PHONY: demo
demo: fixtures
	@$(PY) -m mesh_silos verify --out-dir $(OUT)

.PHONY: test
test:
	@$(PY) scripts/run_tests.py -v

# Coverage measures the suite CI runs, not a subset of it.
#
# This used to be `coverage run ... -m unittest discover -s tests`. But
# `unittest` collects TestCase classes only: a module-level `def test_*`
# or anything under `@pytest.mark.parametrize` is invisible to it, which is
# why scripts/run_tests.py exists and warns about it. So `make coverage`
# reported a tidy percentage over roughly a third of the tests and
# `--fail-under=85` could go green on a measurement of a subset -- the same
# defect this project has found in its own verification harness three
# times. `coverage run -m pytest` needs no pytest-cov, which is not a
# declared dependency.
.PHONY: coverage
coverage:
	@$(PY) -c "import coverage" >/dev/null 2>&1 || { \
		$(PY) -m pip install coverage >/dev/null 2>&1 || \
		uv pip install --python $(PY) coverage || { \
			echo "[coverage] coverage is not installed and pip is unavailable."; \
			echo "           uv-managed venvs have no pip: uv pip install --python $(PY) coverage"; \
			exit 1; \
		}; \
	}
	@$(PY) -m coverage run --source=mesh_silos -m pytest tests -q
	@$(PY) -m coverage report --fail-under=$(COVERAGE_MIN)

# The written performance budgets, as a gate. Each tool exits non-zero when its
# ceiling is exceeded. ``--statements`` and the API byte budget are the two
# added when docs/IMPROVEMENT_PLAN.md Phase 0 made the work measurable: a SQL
# statement *count* per column and a *byte* size per route, both asserted as
# numbers because a duration is a benchmark for one laptop and a count is a
# property of the code.
.PHONY: budget
budget:
	@$(PY) tools/_measure_weave_budget.py --rows 10000
	@$(PY) tools/_measure_weave_budget.py --tables 300
	@$(PY) tools/_measure_weave_budget.py --statements 300
	@$(PY) tools/_measure_api_budget.py --tables 300
	@$(PY) tools/_measure_ui_budget.py

.PHONY: frontend
frontend:
	@cd frontend && npm ci --no-audit --no-fund
	@cd frontend && npm run build

.PHONY: verify
verify: test air-gap demo budget deps dockerfile docs coverage
	@$(PY) tools/_audit_stubs.py
	@echo "[make verify] all green."

#: Static consistency of the container files against the tree. Not a build:
#: there is no Docker host here, and the target says so when it runs.
.PHONY: dockerfile
dockerfile:
	@$(PY) tools/_verify_dockerfile.py

#: Every `file:line` citation in a living document must resolve, every quantity
#: must match a source, and every document under docs/ must be reachable. The
#: claim register and the compliance package had their citations checked; docs/
#: did not, so RUNBOOK.md accumulated ~13 anchors pointing at lines that had
#: moved. Audits of a named past revision are exempt with a reason; a new
#: document with citations that is in neither set fails, forcing the decision.
#:
#: The last line is the version, across the four places it lives — and it is
#: here because before RELEASE-2 it was nowhere: the tool existed, checked
#: `CITATION.cff` as well as `pyproject.toml` and `__version__`, and no gate
#: ran it. The same four gates ``tools/ci.py docs`` runs — one entry point must
#: not check less than the other.
#:
#: RELEASE-5 added ``_verify_doc_claims.py``, which reads what a document says
#: about the *repository* — the one kind of claim the other three cannot see. Its
#: leading `-` follows the `deps` convention for the same reason: it exits 3 when
#: there is no ``origin/main`` to compare against, and a checkout with no remote
#: cannot lie about one. ``tools/ci.py docs`` (what CI runs) distinguishes 0 from
#: 3; make's `-` cannot, and the tool prints which of its answers it gave.
.PHONY: docs
docs:
	@$(PY) tools/_verify_doc_citations.py
	@$(PY) tools/_verify_doc_numbers.py
	@$(PY) tools/_verify_doc_index.py
	-@$(PY) tools/_verify_doc_claims.py
	@$(PY) tools/_verify_publication.py

#: Known vulnerabilities in the dependencies, with a reviewed allowlist that
#: expires. Needs network on the first run (the advisory database); when it is
#: unreachable the tool exits 3 and prints SKIP — not a pass, and deliberately
#: not a failure either, because this product is meant to be verified offline.
#: The leading `-` stops make aborting on that code; the tool's own output says
#: which of the three answers it gave. Use `--require-network` for a release run.
.PHONY: deps
deps:
	-@$(PY) tools/_audit_deps.py

.PHONY: air-gap
air-gap:
	@$(PY) -m mesh_silos check-air-gap

#: The whole product against **live** database engines (opt-in, needs Docker).
#: Runs the sweep in tools/_live_engines_e2e.py: register -> weave -> decide ->
#: export -> verify artefact set -> uninstall -> measure residue, against SQLite /
#: CSV / Parquet / DuckDB (always) and PostgreSQL / MySQL / MongoDB (when up).
#: Deliberately NOT in `make verify`: a suite that needs servers must not turn a
#: machine without them red. Exits 3 (SKIP) when no live server is reachable —
#: the leading `-` keeps make from aborting on that code, and the tool's own
#: output prints SKIP, which is never a pass.
.PHONY: e2e-live
e2e-live:
	-@$(PY) tools/ci.py live

#: The four browser journeys (opt-in, needs a browser binary).
#: Runs tools/_browser_journeys.py: sign in, navigate /map by keyboard, type YAML
#: into /intake and read it back off disk byte for byte, decide a pair on /review.
#: Deliberately NOT in `make verify`: a machine without a browser must not turn
#: red. Exits 3 (SKIP) when there is no browser or no `websockets`; the leading
#: `-` keeps make from aborting on that code, and the tool prints SKIP, which is
#: never a pass.
.PHONY: e2e-browser
e2e-browser:
	-@$(PY) tools/ci.py browser

.PHONY: sbom
sbom:
	@$(PY) tools/gen_sbom.py > sbom.json

.PHONY: sign
sign:
	@$(PY) tools/sign_release.py --version $(VERSION) --key-id $(GPG_KEY_ID)

.PHONY: clean
clean:
	@rm -rf $(OUT)
	@echo "[make clean] removed $(OUT)/"