# solstone Makefile
# Python-based AI-driven desktop journaling toolkit

# Route pytest tmp dirs to /var/tmp (disk) instead of default /tmp (tmpfs/RAM).
# Each top-level pytest invocation gets its own --basetemp so concurrent runs
# in different worktrees do not share /var/tmp/pytest-of-$USER/pytest-N/. The
# basetemp is created at recipe runtime (not parse time) and removed via shell
# trap on exit, so non-test make targets don't leak empty dirs and test runs
# don't leak full ones. PYTEST_BASETEMP_INIT must be on the same recipe shell
# line as PYTEST_BASETEMP_FLAG (each recipe line is its own shell). Do not
# re-add --basetemp to pyproject — it would pin all runs to one path and
# pytest wipes it on startup, destroying concurrent state.
export TMPDIR := /var/tmp
PYTEST_BASETEMP_INIT := BASETEMP=$$(mktemp -d /var/tmp/solstone-pytest-XXXXXX); trap 'rm -rf "$$BASETEMP"' EXIT INT TERM;
PYTEST_BASETEMP_FLAG := --basetemp "$$BASETEMP"

.PHONY: install hopper-install uninstall test test-cov test-integration test-performance test-app test-only format format-check install-checks ci clean clean-install coverage watch versions update update-prices preflight pre-commit skills render-packaging check-rust-fmt check-rust-msrv check-rust-clippy check-rust-test check-rust-ios check-rust-deny check-release-advisory-liveness check-rust-release-manifest audit openapi check-openapi check-openapi-observer-client-contract contract check-contract journal-resolution-vectors check-journal-resolution-vectors dev all sandbox sandbox-stop install-models parakeet-helper parakeet-helper-clean wheel-macos wheel-macos-clean verify verify-api verify-schemathesis update-api-baselines eval-schemas service-logs check-layer-hygiene check-api-conventions check-journal-io-access check-journal-io-mechanic check-journal-config-owner check-call-http-only check-no-legacy-chat check-brain-health-cutover check-tools-http-only check-access-imports-clean check-convey-bind-imports-clean check-schema-bounds check-thin-base-install check-cogitate-prompts smoke-cogitate release release-test FORCE

# Default target - install package in editable mode
all: install

# Virtual environment directory
VENV := .venv
VENV_BIN := $(VENV)/bin
VENV_PY := $(VENV_BIN)/python
PYTHON := $(VENV_PY)
RUST_MANIFEST := core/Cargo.toml
IOS_TARGET := aarch64-apple-ios
REQUIRE_CARGO := command -v cargo >/dev/null 2>&1 || { echo "cargo is required for Rust checks; install cargo and retry" >&2; exit 1; }
REQUIRE_RUSTUP := command -v rustup >/dev/null 2>&1 || { echo "rustup is required for the iOS gate; install rustup and retry" >&2; exit 1; }
# Pick the GPU (CUDA) journal runtime only on x86_64 NVIDIA hosts. The
# CUDA bundle resolves onnxruntime-gpu, which ships NO aarch64 wheel on PyPI, so
# an aarch64 NVIDIA host (e.g. DGX Spark / GB10) that auto-selected `cuda` would
# die in the `.installed` `uv sync` below — before the per-arch `install` guard
# (which correctly skips non-x86_64 Linux) ever runs. Gating on x86_64 also
# keeps this coherent with the STT arch decision (aarch64-linux uses the
# parakeet.cpp CPU/Vulkan bundle). Everything non-x86_64 falls to the CPU
# `journal-cpu` group, whose onnxruntime has aarch64 wheels.
JOURNAL_VARIANT ?= $(shell if [ "$$(uname -m)" = "x86_64" ] && nvidia-smi -L >/dev/null 2>&1; then echo cuda; else echo cpu; fi)

# Dev install groups: install exactly ONE journal leaf for this host.
# `journal-cpu` and `journal-cuda` select the same journal stack and differ only
# in the ONNX runtime package, so NEVER install both and NEVER use all optional
# dependency groups:
#   - `journal-cpu` pulls onnxruntime; `journal-cuda` pulls onnxruntime-gpu. Both
#     packages own the SAME onnxruntime/ import dir and clobber each other ->
#     `import onnxruntime` fails (ModuleNotFoundError) even though uv still
#     lists it installed. Surfaces as `journal install-models` dying with "No
#     module named 'onnxruntime'".
#   - on Darwin, resolving the CUDA group also forces cuda's nvidia-* wheels,
#     which have no arm64 builds, so `uv sync` errors out outright.
# Pick the GPU group only on NVIDIA hosts; everyone else gets the CPU group.
JOURNAL_GROUP := $(if $(filter cuda,$(JOURNAL_VARIANT)),journal-cuda,journal-cpu)

# Require uv only for goals that actually use it. `preflight` is a pure
# stdlib readiness battery and `install` runs preflight as its own fail-fast
# pre-step, so neither should abort at parse time when uv is absent — they
# report uv-absence themselves. test/ci/etc. still abort early.
UV := $(shell command -v uv 2>/dev/null)
UV_OPTIONAL_GOALS := preflight install render-packaging check-rust-fmt check-rust-msrv check-rust-clippy check-rust-test check-rust-ios check-rust-deny audit
ifndef UV
ifneq ($(filter-out $(UV_OPTIONAL_GOALS),$(MAKECMDGOALS)),)
$(error uv is not installed. Install it: curl -LsSf https://astral.sh/uv/install.sh | sh)
endif
endif

# User bin directory for symlink (standard location, usually already in PATH)
USER_BIN := $(HOME)/.local/bin

.python-version-hash: FORCE
	@tmp_file=$$(mktemp); \
	python3 -c "import sys; print(sys.version_info[:2])" > "$$tmp_file"; \
	if [ ! -f $@ ] || ! cmp -s "$$tmp_file" $@; then mv "$$tmp_file" $@; else rm -f "$$tmp_file"; fi

# Marker file to track installation
.installed: pyproject.toml packages/*/pyproject.toml uv.lock .python-version-hash
	python3 scripts/render_packaging.py
	$(MAKE) preflight
	@echo "Installing package with uv..."
	$(UV) sync --group dev --group $(JOURNAL_GROUP)
	@# Python 3.14+ needs onnxruntime from nightly (not yet on PyPI)
	@OS_NAME=$$(uname -s); \
	PY_MINOR=$$($(PYTHON) -c "import sys; print(sys.version_info.minor)"); \
	if [ "$$OS_NAME" = "Darwin" ] && [ "$$PY_MINOR" -ge 14 ]; then \
		echo "Python 3.14+ detected - installing onnxruntime from nightly feed..."; \
		$(UV) pip install --pre --no-deps --index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/ORT-Nightly/pypi/simple/ onnxruntime; \
	fi
	@# The `--group $(JOURNAL_GROUP)` sync above already pulls the right
	@# ONNX runtime package (journal-cpu = CPU, journal-cuda = GPU).
	@$(MAKE) --no-print-directory skills
	@touch .installed

# Generate lock file if missing
uv.lock: pyproject.toml packages/*/pyproject.toml
	python3 scripts/render_packaging.py
	$(UV) lock

# Install package in editable mode with isolated venv
install: .installed
	@(cd /tmp && $(CURDIR)/$(VENV_BIN)/python -c "from solstone.think.sol_cli import main") 2>/dev/null || { \
		echo ">>> re-registering editable install"; \
		$(UV) pip install -e . --no-deps; \
		if (cd /tmp && $(CURDIR)/$(VENV_BIN)/python -c "from solstone.think.sol_cli import main"); then \
			echo ">>> re-registered successfully"; \
		else \
			echo ">>> editable install still broken; run make clean-install"; \
			exit 1; \
		fi; \
	}
	@OS_NAME=$$(uname -s); \
	ARCH=$$(uname -m); \
	if [ "$$OS_NAME" = "Darwin" ] && [ "$$ARCH" = "arm64" ]; then \
		$(MAKE) parakeet-helper || { echo 'parakeet install: helper build failed' >&2; exit 1; }; \
	elif [ "$$OS_NAME" = "Linux" ]; then \
		if [ "$$ARCH" = "x86_64" ]; then \
				echo "journal install: JOURNAL_GROUP=$(JOURNAL_GROUP)"; \
				$(UV) sync --group dev --group $(JOURNAL_GROUP) || { echo "journal install: uv sync --group dev --group $(JOURNAL_GROUP) failed" >&2; exit 1; }; \
				if [ "$(JOURNAL_VARIANT)" = "cuda" ]; then \
					$(UV) sync --group dev --group $(JOURNAL_GROUP) --reinstall-package onnxruntime-gpu || { echo "journal install: failed to force-reinstall onnxruntime-gpu" >&2; exit 1; }; \
				$(VENV_PY) -c "import onnxruntime as ort; ort.preload_dlls(cuda=True, cudnn=True); assert 'CUDAExecutionProvider' in ort.get_available_providers(), 'CUDAExecutionProvider missing after install'; print('journal install: CUDA runtime ready')" || { echo "journal install: CUDA runtime validation failed" >&2; exit 1; }; \
			fi; \
		else \
			echo "journal install: skipping unsupported Linux arch $$ARCH"; \
		fi; \
	else \
		echo "parakeet install: unsupported host '$$OS_NAME/$$ARCH'; supported: darwin/arm64, linux/x86_64" >&2; \
		exit 1; \
	fi
	@touch .installed
	@$(VENV_BIN)/journal install-models || { echo "journal install-models failed" >&2; exit 1; }

# Lean lode bootstrap: build only the base dependency layer (.installed) that
# `make ci` needs — venv + dev/host journal deps + skills. Runtime/model
# provisioning (parakeet build, forced onnxruntime-gpu reinstall + CUDA
# validation, journal install-models) is intentionally deferred to `install`.
hopper-install: .installed

# Stdlib-only install-readiness battery — runs before `.venv`/`uv` exist; a
# blocker failure exits non-zero. Also wired as the first step of `.installed`.
preflight:
	python3 scripts/preflight.py

render-packaging:
	python3 scripts/render_packaging.py

check-rust-fmt:
	@$(REQUIRE_CARGO)
	cargo fmt --manifest-path $(RUST_MANIFEST) --all -- --check

check-rust-msrv:
	@$(REQUIRE_CARGO)
	@python3 scripts/check_release_preflight.py msrv --toolchain 1.95.0
	RUSTUP_TOOLCHAIN=1.95.0 cargo check --manifest-path $(RUST_MANIFEST) --workspace --locked

check-rust-clippy:
	@$(REQUIRE_CARGO)
	cargo clippy --manifest-path $(RUST_MANIFEST) --workspace --all-targets --locked -- -D warnings

check-rust-test:
	@$(REQUIRE_CARGO)
	cargo test --manifest-path $(RUST_MANIFEST) --workspace --locked

check-rust-ios:
	@$(REQUIRE_CARGO)
	@$(REQUIRE_RUSTUP)
	@rustup target list --installed 2>/dev/null | grep -qx "$(IOS_TARGET)" || { echo "Rust target $(IOS_TARGET) is required for the iOS gate; run rustup target add $(IOS_TARGET)" >&2; exit 1; }
	cargo check --manifest-path $(RUST_MANIFEST) --workspace --exclude solstone-core --exclude solstone-core-indexer-store --lib --target $(IOS_TARGET) --locked

check-rust-deny:
	@$(REQUIRE_CARGO)
	@python3 scripts/check_release_preflight.py cargo-deny
	cargo deny --manifest-path $(RUST_MANIFEST) --locked --offline check bans licenses sources

audit:
	@$(REQUIRE_CARGO)
	@python3 scripts/check_release_preflight.py cargo-deny
	@cargo deny --manifest-path $(RUST_MANIFEST) fetch db || { echo "ERROR: RustSec advisory refresh failed; no current advisory result was produced. Restore network access and rerun 'make audit'." >&2; exit 1; }
	cargo deny --manifest-path $(RUST_MANIFEST) --locked --offline check advisories

# Setup skill symlinks
skills:
	@$(VENV_BIN)/sol skills build
	@$(VENV_BIN)/sol skills install --project journal --agent all

# Start local dev stack against fixture journal (no observers, no daily processing)
dev: .installed
	$(TEST_ENV) PATH=$(CURDIR)/$(VENV_BIN):$$PATH $(VENV_BIN)/journal supervisor 0 --no-daily

# Start sandbox stack: fixture copy + background supervisor + readiness wait
sandbox: .installed
	@# Fail if sandbox already running
	@if [ -f .sandbox.pid ] && kill -0 $$(cat .sandbox.pid) 2>/dev/null; then \
		echo "Sandbox already running (PID $$(cat .sandbox.pid))"; \
		echo "Run 'make sandbox-stop' first."; \
		exit 1; \
	fi
	@# Clean up stale state from a previous crashed sandbox
	@if [ -f .sandbox.journal ]; then \
		rm -rf "$$(cat .sandbox.journal)" 2>/dev/null; \
		rm -f .sandbox.pid .sandbox.journal; \
	fi
	@# Copy fixtures to temp dir
	@SANDBOX_JOURNAL=$$(mktemp -d /tmp/solstone-sandbox-XXXXXX); \
	cp -r tests/fixtures/journal/* "$$SANDBOX_JOURNAL/"; \
	echo "$$SANDBOX_JOURNAL" > .sandbox.journal; \
	echo "Sandbox journal: $$SANDBOX_JOURNAL"; \
	: "Boot supervisor in background"; \
	SOLSTONE_JOURNAL="$$SANDBOX_JOURNAL" SANDBOX_PATH="$(CURDIR)/$(VENV_BIN):$$PATH" SANDBOX_LOG="$$SANDBOX_JOURNAL/health/service.log" JOURNAL_BIN="$(CURDIR)/$(VENV_BIN)/journal" \
		$(VENV_PY) -c 'import os, subprocess; log = open(os.environ["SANDBOX_LOG"], "ab", buffering=0); env = os.environ.copy(); env["PATH"] = os.environ["SANDBOX_PATH"]; proc = subprocess.Popen([os.environ["JOURNAL_BIN"], "supervisor", "0", "--no-daily"], stdin=subprocess.DEVNULL, stdout=log, stderr=subprocess.STDOUT, env=env, start_new_session=True); print(proc.pid)' > .sandbox.pid; \
	echo "Supervisor PID: $$(cat .sandbox.pid)"; \
	: "Poll for readiness"; \
	echo "Waiting for services..."; \
	READY=false; \
	for i in $$(seq 1 20); do \
		if SOLSTONE_JOURNAL="$$SANDBOX_JOURNAL" $(VENV_BIN)/journal health > /dev/null 2>&1; then \
			READY=true; \
			break; \
		fi; \
		sleep 1; \
	done; \
	if [ "$$READY" = "false" ]; then \
		echo "Readiness timeout - killing supervisor"; \
		kill $$(cat .sandbox.pid) 2>/dev/null || true; \
		rm -rf "$$SANDBOX_JOURNAL" .sandbox.pid .sandbox.journal; \
		exit 1; \
	fi; \
	CONVEY_PORT=$$(cat "$$SANDBOX_JOURNAL/health/convey.port" 2>/dev/null); \
	echo ""; \
	echo "Sandbox is ready!"; \
	echo "  Convey: http://localhost:$$CONVEY_PORT/"; \
	echo "  Journal: $$SANDBOX_JOURNAL"; \
	echo "  Stop:   make sandbox-stop"

# Stop sandbox: terminate supervisor, clean up temp dir and state files
sandbox-stop:
	@if [ ! -f .sandbox.pid ]; then \
		echo "No sandbox running."; \
		exit 0; \
	fi; \
	PID=$$(cat .sandbox.pid); \
	echo "Stopping supervisor (PID $$PID)..."; \
	kill "$$PID" 2>/dev/null || true; \
	: "Wait up to 5s for clean shutdown"; \
	for i in $$(seq 1 10); do \
		kill -0 "$$PID" 2>/dev/null || break; \
		sleep 0.5; \
	done; \
	kill -9 "$$PID" 2>/dev/null || true; \
	if [ -f .sandbox.journal ]; then \
		SANDBOX_JOURNAL=$$(cat .sandbox.journal); \
		rm -rf "$$SANDBOX_JOURNAL"; \
		echo "Removed $$SANDBOX_JOURNAL"; \
	fi; \
		rm -f .sandbox.pid .sandbox.journal; \
		echo "Sandbox stopped."

.PHONY: sandbox-seed-observers
sandbox-seed-observers: ## Seed 4 sample observers into the running sandbox journal
	@test -s .sandbox.journal || (echo "No sandbox running. Run 'make sandbox' first." && exit 1)
	@SOLSTONE_JOURNAL=$$(cat .sandbox.journal) $(VENV_BIN)/python tests/fixtures/seed_observers.py

# Verify API baselines against running sandbox
verify-api: .installed
	@echo "Verifying API baselines (sandbox)..."
	@$(MAKE) sandbox
	@SANDBOX_JOURNAL=$$(cat .sandbox.journal); \
	CONVEY_PORT=$$(cat "$$SANDBOX_JOURNAL/health/convey.port"); \
	RESULT=0; \
	SOLSTONE_JOURNAL="$$SANDBOX_JOURNAL" $(VENV_BIN)/journal indexer --rescan-full > /dev/null; \
	SOLSTONE_JOURNAL="$$SANDBOX_JOURNAL" $(VENV_BIN)/python tests/verify_api.py verify --base-url "http://localhost:$$CONVEY_PORT" || RESULT=$$?; \
	$(MAKE) sandbox-stop; \
	exit $$RESULT

# tests/conftest.py overwrites SOLSTONE_JOURNAL; pass sandbox via a private env var.
verify-schemathesis: .installed ## Run Schemathesis read allowlist against disposable live sandbox
	@echo "Verifying OpenAPI contract with Schemathesis (disposable live sandbox)..."
	@$(MAKE) sandbox
	@SANDBOX_JOURNAL=$$(cat .sandbox.journal); \
	RESULT=0; \
	SOLSTONE_SCHEMATHESIS_JOURNAL="$$SANDBOX_JOURNAL" \
	SOLSTONE_SCHEMATHESIS_LIVE=1 \
	$(VENV_BIN)/pytest tests/test_openapi_schemathesis.py -q || RESULT=$$?; \
	$(MAKE) sandbox-stop; \
	exit $$RESULT

eval-schemas: .installed
	$(VENV_BIN)/python tests/eval_schemas.py

# Regenerate API baseline files. By default uses the deterministic Flask
# test-client path (frozen time). For sandbox-only endpoints (graph, search,
# badge-count, updated-days), pass SANDBOX=1 to regenerate from the live
# sandbox — these rely on the indexer and real clock.
update-api-baselines: .installed
	@if [ "$(SANDBOX)" = "1" ]; then \
		echo "Updating API baselines (sandbox, includes sandbox-only endpoints)..."; \
		$(MAKE) sandbox; \
		SANDBOX_JOURNAL=$$(cat .sandbox.journal); \
		CONVEY_PORT=$$(cat "$$SANDBOX_JOURNAL/health/convey.port"); \
		RESULT=0; \
		SOLSTONE_JOURNAL="$$SANDBOX_JOURNAL" $(VENV_BIN)/journal indexer --rescan-full > /dev/null; \
		SOLSTONE_JOURNAL="$$SANDBOX_JOURNAL" $(VENV_BIN)/python tests/verify_api.py update --base-url "http://localhost:$$CONVEY_PORT" || RESULT=$$?; \
		$(MAKE) sandbox-stop; \
		exit $$RESULT; \
	else \
		echo "Updating API baselines (test client)..."; \
		$(VENV_BIN)/python tests/verify_api.py update; \
	fi


# Install and verify local ML models
install-models:
	@test -x "$(VENV_BIN)/sol" || { echo "missing $(VENV_BIN)/sol; run make install first" >&2; exit 1; }
	$(VENV_BIN)/journal install-models

# Build the parakeet helper binary (macOS/arm64 only, requires Xcode CLT)
parakeet-helper:
	cd solstone/observe/transcribe/parakeet_helper && swift build -c release
	@echo "built: $$(pwd)/solstone/observe/transcribe/parakeet_helper/.build/release/parakeet-helper"

# Remove parakeet helper build artifacts
parakeet-helper-clean:
	rm -rf solstone/observe/transcribe/parakeet_helper/.build solstone/observe/transcribe/parakeet_helper/.swiftpm solstone/observe/transcribe/parakeet_helper/Package.resolved

# Build a signed/notarized macOS Apple Silicon platform wheel
# (Darwin/arm64 only; requires Xcode CLT, Developer ID cert, and the
# `sol-pbc-notary` notarytool keychain profile in sol-signing.keychain-db).
# `uv build` runs in its own PEP 517 isolated env, so this target intentionally
# does not depend on `.installed` — the wheel build is fully decoupled from
# the dev venv install state.
ifeq ($(shell uname -s)/$(shell uname -m),Darwin/arm64)
wheel-macos: parakeet-helper
	@ROOT_FACTS=$$(mktemp); \
	trap 'rm -f "$$ROOT_FACTS"' EXIT; \
	echo "==> signing and notarizing parakeet-helper"; \
	./scripts/sign-and-notarize-helper.sh solstone/observe/transcribe/parakeet_helper/.build/release/parakeet-helper > "$$ROOT_FACTS"; \
	echo "==> staging helper into _bin/"; \
	mkdir -p solstone/observe/transcribe/parakeet_helper/_bin; \
	cp solstone/observe/transcribe/parakeet_helper/.build/release/parakeet-helper solstone/observe/transcribe/parakeet_helper/_bin/parakeet-helper; \
	echo "==> building macosx_14_0_arm64 platform wheel"; \
	rm -rf build/ *.egg-info/; \
	$(UV) build --wheel -C--build-option=--plat-name=macosx_14_0_arm64; \
	ROOT_MAC_WHEEL=$$(ls dist/solstone-*-macosx_14_0_arm64.whl); \
	SOURCE_COMMIT=$$(git rev-parse HEAD); \
	CORE_LOCK_SHA256=$$(shasum -a 256 core/Cargo.lock | awk '{print $$1}'); \
	python3 scripts/record_macos_native_wheel.py --role root --wheel "$$ROOT_MAC_WHEEL" --signing-facts "$$ROOT_FACTS" --source-commit "$$SOURCE_COMMIT" --core-lock-sha256 "$$CORE_LOCK_SHA256" --out dist/macos-native-root.json
	@echo "==> building macosx_14_0_arm64 solstone-core wheel"
	MACOSX_DEPLOYMENT_TARGET=14.0 MATURIN_PEP517_ARGS="--locked --target aarch64-apple-darwin" $(UV) build --package solstone-core --wheel
	@echo "==> signing and notarizing solstone-core"
	@CORE_MAC_WHEEL=$$(ls dist/solstone_core-*-macosx_14_0_arm64.whl); \
	CORE_FACTS=$$(mktemp); \
	CORE_TMP=$$(mktemp -d); \
	trap 'rm -rf "$$CORE_TMP" "$$CORE_FACTS"' EXIT; \
	python3 -m zipfile -e "$$CORE_MAC_WHEEL" "$$CORE_TMP"; \
	CORE_BINARY=$$(find "$$CORE_TMP" -path '*.data/scripts/solstone-core' -type f -print -quit); \
	test -n "$$CORE_BINARY" || { echo "missing solstone-core binary in $$CORE_MAC_WHEEL" >&2; exit 1; }; \
	./scripts/sign-and-notarize-helper.sh "$$CORE_BINARY" > "$$CORE_FACTS"; \
	python3 scripts/repack_wheel_record.py "$$CORE_TMP" "$$CORE_MAC_WHEEL"; \
	SOURCE_COMMIT=$$(git rev-parse HEAD); \
	CORE_LOCK_SHA256=$$(shasum -a 256 core/Cargo.lock | awk '{print $$1}'); \
	python3 scripts/record_macos_native_wheel.py --role core --wheel "$$CORE_MAC_WHEEL" --signing-facts "$$CORE_FACTS" --source-commit "$$SOURCE_COMMIT" --core-lock-sha256 "$$CORE_LOCK_SHA256" --out dist/macos-native-core.json
else
wheel-macos:
	@echo "wheel-macos: only supported on Darwin/arm64 (got $(shell uname -s)/$(shell uname -m))" >&2
	@exit 1
endif

# Remove the staged helper copy that wheel-macos installs into _bin/
wheel-macos-clean:
	rm -rf solstone/observe/transcribe/parakeet_helper/_bin

# Test environment - use fixtures journal for all tests
TEST_ENV = SOLSTONE_JOURNAL=tests/fixtures/journal

# Venv tool shortcuts
PYTEST := $(VENV_BIN)/pytest
RUFF := $(VENV_BIN)/ruff
MYPY := $(VENV_BIN)/mypy

# Keep the default full-suite fan-out LOW: this box runs many concurrent
# sessions/lodes each invoking `make test`, and their worker pools multiply —
# concurrent 8-worker suites OOM'd the box on 2026-07-03 (systemd-oomd killed
# the hub daemon, hopper, and several agent sessions). Override with
# `make PYTEST_MAX_WORKERS=16 test` on a dedicated/idle box.
PYTEST_MAX_WORKERS ?= 2
PYTEST_XDIST_ARGS := -n auto --maxprocesses $(PYTEST_MAX_WORKERS) --dist loadgroup
PYTEST_UNIT_ARGS := -m "not integration and not performance"

# Check formatting without modifying files — gates `make test`
format-check: .installed
	@$(RUFF) format --check . || { echo "Run 'make format' to fix formatting"; exit 1; }

# Run all unit tests — core (tests/) + every app (solstone/apps/*/tests/).
# xdist lives here, not in pyproject addopts, so bare pytest / pytest-watch /
# IDE runs stay serial.
test: .installed format-check
	@echo "Running unit tests (core + apps)..."
	$(PYTEST_BASETEMP_INIT) $(TEST_ENV) $(PYTEST) $(PYTEST_BASETEMP_FLAG) tests/ solstone/apps/ -q $(PYTEST_UNIT_ARGS) $(PYTEST_XDIST_ARGS)

# Same suite with full-repo coverage (operator opt-in; used by verify).
test-cov: .installed format-check
	@echo "Running unit tests with coverage (core + apps)..."
	$(PYTEST_BASETEMP_INIT) $(TEST_ENV) $(PYTEST) $(PYTEST_BASETEMP_FLAG) tests/ solstone/apps/ -q --cov=. $(PYTEST_UNIT_ARGS) $(PYTEST_XDIST_ARGS)

# Real external-process probes are valuable operator validation, not unit CI.
test-integration: .installed format-check
	@echo "Running integration tests..."
	$(PYTEST_BASETEMP_INIT) $(TEST_ENV) $(PYTEST) $(PYTEST_BASETEMP_FLAG) tests/ solstone/apps/ -q -m integration $(PYTEST_XDIST_ARGS)

# Wall-clock thresholds are intentionally opt-in so loaded hosts do not make CI flaky.
test-performance: .installed format-check
	@echo "Running performance tests..."
	$(PYTEST_BASETEMP_INIT) $(TEST_ENV) $(PYTEST) $(PYTEST_BASETEMP_FLAG) tests/ solstone/apps/ -q -m performance

# Run a single app's tests
test-app: .installed
	@if [ -z "$(APP)" ]; then \
		echo "Usage: make test-app APP=<app_name>"; \
		echo "Example: make test-app APP=todos"; \
		exit 1; \
	fi
	$(PYTEST_BASETEMP_INIT) PYTHONPATH=$(CURDIR) $(TEST_ENV) $(PYTEST) $(PYTEST_BASETEMP_FLAG) solstone/apps/$(APP)/tests/ -v

# Run specific test file or pattern
test-only: .installed
	@if [ -z "$(TEST)" ]; then \
		echo "Usage: make test-only TEST=<test_file_or_pattern>"; \
		echo "Example: make test-only TEST=tests/test_utils.py"; \
		echo "Example: make test-only TEST=\"-k test_function_name\""; \
		exit 1; \
	fi
	$(PYTEST_BASETEMP_INIT) $(TEST_ENV) $(PYTEST) $(PYTEST_BASETEMP_FLAG) $(TEST)

# Auto-format and fix code, then report any remaining issues
format: .installed
	@echo "Formatting and fixing code with ruff..."
	@$(RUFF) format .
	@$(RUFF) check --fix .
	@echo ""
	@echo "Checking for remaining issues..."
	@RUFF_OK=true; MYPY_OK=true; \
	$(RUFF) check . || RUFF_OK=false; \
	$(MYPY) . || MYPY_OK=false; \
	if $$RUFF_OK && $$MYPY_OK; then \
		echo ""; \
		echo "All clean!"; \
	else \
		echo ""; \
		echo "Issues above need manual fixes."; \
	fi

# Clean build artifacts and cache files
clean:
	@echo "Cleaning build artifacts and cache files..."
	rm -rf build/ dist/ *.egg-info/
	rm -rf .pytest_cache/ .coverage .mypy_cache/
	rm -rf journal/.agents/ journal/.claude/
	find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
	find . -type f -name "*.pyc" -delete
	find . -type f -name "*.pyo" -delete
	find . -type f -name ".DS_Store" -delete
	rm -f .installed

# Follow installed service logs
service-logs:
	$(VENV_BIN)/journal service logs -f

uninstall:
	@echo "Error: 'make uninstall' is disabled. Use 'journal service uninstall', 'sol skills uninstall', and 'python -m solstone.think.install_guard uninstall' to remove installed user artifacts, or 'make clean-install' to rebuild the local dev environment." >&2
	@exit 1

FORCE:

# Clean everything and reinstall
clean-install: clean
	rm -rf $(VENV) .installed
	$(MAKE) install

# Run continuous integration checks (what CI would run)
install-checks: .installed
	@echo "=== Checking formatting ==="
	@$(RUFF) format --check . || { echo "Run 'make format' to fix formatting"; exit 1; }
	@echo ""
	@echo "=== Running ruff ==="
	@$(RUFF) check . || { echo "Run 'make format' to auto-fix"; exit 1; }
	@echo ""
	@echo "=== Running layer-hygiene check ==="
	@$(MAKE) check-layer-hygiene
	@echo ""
	@echo "=== Running API-conventions check ==="
	@$(MAKE) check-api-conventions
	@echo ""
	@echo "=== Running journal-io access check ==="
	@$(MAKE) check-journal-io-access
	@echo ""
	@echo "=== Running journal-io mechanic check ==="
	@$(MAKE) check-journal-io-mechanic
	@echo ""
	@echo "=== Running journal-config owner check ==="
	@$(MAKE) check-journal-config-owner
	@echo ""
	@echo "=== Running provider-install owner check ==="
	@$(MAKE) check-provider-install-owner
	@echo ""
	@echo "=== Running provider-start-command check ==="
	@$(MAKE) check-provider-start-commands
	@echo ""
	@echo "=== Running call-http-only check ==="
	@$(MAKE) check-call-http-only
	@echo ""
	@echo "=== Running legacy-chat surface check ==="
	@$(MAKE) check-no-legacy-chat
	@echo ""
	@echo "=== Running brain-health cutover check ==="
	@$(MAKE) check-brain-health-cutover
	@echo ""
	@echo "=== Running schema-bounds check ==="
	@$(MAKE) check-schema-bounds
	@echo ""
	@echo "=== Running rust release-manifest check ==="
	@$(MAKE) check-rust-release-manifest
	@echo ""
	@echo "=== Running tools-http-only check ==="
	@$(MAKE) check-tools-http-only
	@echo ""
	@echo "=== Running access-imports-clean check ==="
	@$(MAKE) check-access-imports-clean
	@echo ""
	@echo "=== Running convey-bind-imports-clean check ==="
	@$(MAKE) check-convey-bind-imports-clean
	@echo ""
	@echo "=== Running cogitate-prompt check ==="
	@$(MAKE) check-cogitate-prompts
	@echo ""
	@echo "=== Checking generated skill references ==="
	@$(MAKE) check-skill-references
	@echo ""
	@echo "=== Checking OpenAPI contract ==="
	@$(MAKE) check-openapi
	@echo ""
	@echo "=== Checking journal format contract ==="
	@$(MAKE) check-contract
	@echo ""
	@echo "=== Checking core fixtures ==="
	@$(MAKE) check-core-fixtures
	@echo ""
	@echo "=== Checking packaging render ==="
	@python3 scripts/render_packaging.py --check
	@echo ""
	@echo "=== Checking journal resolution vectors ==="
	@$(MAKE) check-journal-resolution-vectors
	@echo ""
	@echo "=== Running rust format check ==="
	@$(MAKE) check-rust-fmt
	@echo ""
	@echo "=== Running rust MSRV check ==="
	@$(MAKE) check-rust-msrv
	@echo ""
	@echo "=== Running rust clippy check ==="
	@$(MAKE) check-rust-clippy
	@echo ""
	@echo "=== Running rust test check ==="
	@$(MAKE) check-rust-test
	@echo ""
	@echo "=== Running rust iOS check ==="
	@$(MAKE) check-rust-ios
	@echo ""
	@echo "=== Running rust dependency policy check ==="
	@$(MAKE) check-rust-deny
	@echo ""
	@echo "=== Running release advisory liveness check ==="
	@$(MAKE) check-release-advisory-liveness
	@echo ""
	@echo "=== Checking extras consistency ==="
	@$(VENV_BIN)/python scripts/check_extras_consistency.py
	@echo ""
	@echo "=== Running mypy ==="
	@$(MYPY) . || true
	@echo ""

ci: install-checks
	@echo "=== Running tests ==="
	@$(MAKE) test
	@echo ""
	@echo "All CI checks passed; evidence classes:"
	@echo "  GNU-host checks: fmt, MSRV, clippy, tests, dependency policy"
	@echo "  iOS cross-target canary: check-rust-ios"

verify: install-checks
	@echo "=== Running tests ==="
	@$(MAKE) test-cov
	@echo ""
	@echo "Verification complete!"

# Watch for changes and run tests (requires pytest-watch)
watch: .installed
	@$(UV) pip show pytest-watch >/dev/null 2>&1 || { echo "Installing pytest-watch..."; $(UV) pip install pytest-watch; }
	$(VENV_BIN)/ptw -- -q

# Generate HTML coverage report (core + apps)
coverage: .installed
	$(PYTEST_BASETEMP_INIT) $(TEST_ENV) $(PYTEST) $(PYTEST_BASETEMP_FLAG) tests/ solstone/apps/ $(PYTEST_UNIT_ARGS) --cov=. --cov-report=html --cov-report=term
	@echo "Coverage report generated in htmlcov/index.html"

# Update all dependencies to latest versions and refresh genai-prices
update: .installed
	@echo "Updating all dependencies to latest versions..."
	$(UV) lock -U
	$(UV) sync
	@echo "Done. All packages updated to latest."

# Update genai-prices to get latest model pricing data
# Run this when adding new models or if pricing tests fail
update-prices: .installed
	@echo "Updating genai-prices to latest version..."
	$(UV) lock -P genai-prices
	$(UV) sync
	@echo "Done. Re-run tests to verify model pricing support."

# Show installed package versions
versions: .installed
	@echo "=== Python version ==="
	$(PYTHON) --version
	@echo ""
	@echo "=== Key package versions ==="
	@$(UV) pip list | grep -E "^(pytest|ruff|mypy|Flask|numpy|Pillow|openai|anthropic)" || true

# Install pre-commit hooks (if using pre-commit)
pre-commit: .installed
	@$(UV) pip show pre-commit >/dev/null 2>&1 || { echo "Installing pre-commit..."; $(UV) pip install pre-commit; }
	$(VENV_BIN)/pre-commit install
	@echo "Pre-commit hooks installed!"

# Low-bar layer-hygiene check (see docs/coding-standards.md § Layer Hygiene)
check-layer-hygiene: .installed
	$(VENV_BIN)/python scripts/check_layer_hygiene.py

# HTTP API conventions check (see docs/CONVEY.md § HTTP API conventions)
check-api-conventions: .installed
	$(VENV_BIN)/python scripts/check_api_conventions.py

# Journal-io write-primitive access check (see AGENTS.md §7 L2)
check-journal-io-access: .installed
	$(VENV_BIN)/python scripts/check_journal_io_access.py

# Journal raw-mechanic check (see AGENTS.md §7 L2)
check-journal-io-mechanic: .installed
	$(VENV_BIN)/python scripts/check_journal_io_mechanic.py

# Journal config owner transaction boundary gate
check-journal-config-owner: .installed
	$(VENV_BIN)/python scripts/check_journal_config_owner.py

# Provider install ownership boundary gate
check-provider-install-owner: .installed
	$(VENV_BIN)/python scripts/check_provider_install_owner.py

# Provider runtime start-command boundary gate
check-provider-start-commands: .installed
	$(VENV_BIN)/python scripts/check_provider_start_commands.py

# sol call HTTP-only gate (call.py reaches the journal only over HTTP)
check-call-http-only: .installed
	$(VENV_BIN)/python scripts/check_call_http_only.py

# Removed chat surfaces stay out of tracked Python, HTML, and JavaScript.
check-no-legacy-chat: .installed
	$(VENV_BIN)/python scripts/check_no_legacy_chat.py

# Brain health cutover guard
check-brain-health-cutover: .installed
	$(VENV_BIN)/python scripts/check_brain_health_cutover.py

# Generation schema bounds ratchet
check-schema-bounds: .installed
	$(VENV_BIN)/python scripts/check_schema_bounds.py

# Rust release-manifest schema, semantic, determinism, and transaction gate
check-rust-release-manifest: .installed
	$(VENV_BIN)/python scripts/check_rust_release_manifest.py

# Built-in sol call tools HTTP-only gate
check-tools-http-only: .installed
	$(VENV_BIN)/python scripts/check_tools_http_only.py

# Thin sol access surface import-clean gate (fast meta_path simulation; in ci)
check-access-imports-clean: .installed
	$(VENV_BIN)/python scripts/check_access_imports_clean.py

# Convey bind path import-clean gate
check-convey-bind-imports-clean: .installed
	$(VENV_BIN)/python scripts/check_convey_bind_imports_clean.py

# Faithful thin-base gate: build a fresh venv with the REAL base partition (no
# extras) and assert the access surface imports clean against it. Heavier than
# check-access-imports-clean (does a real install) — operator/release opt-in,
# NOT part of `make ci` (which uses the fast simulation above).
check-thin-base-install:
	python3 scripts/check_access_imports_clean.py --real-install

# Cogitate-prompt static gate (prompts use only on-contract command forms)
check-cogitate-prompts: .installed
	$(VENV_BIN)/python scripts/check_cogitate_prompts.py

# Generated router skill references gate
check-skill-references: .installed
	$(VENV_BIN)/sol skills build --check

openapi:
	$(VENV_BIN)/python scripts/build_openapi_contract.py

check-openapi: .installed
	$(VENV_BIN)/python scripts/check_openapi_contract.py
	$(VENV_BIN)/python scripts/build_openapi_contract.py --check
	$(MAKE) check-openapi-observer-client-contract

check-openapi-observer-client-contract: .installed
	$(VENV_BIN)/python scripts/check_observer_client_contract_bundle.py

journal-resolution-vectors:
	$(VENV_BIN)/python scripts/build_journal_resolution_vectors.py

check-journal-resolution-vectors: .installed
	$(VENV_BIN)/python scripts/build_journal_resolution_vectors.py --check

contract:
	$(VENV_BIN)/python -m solstone.think.contract_cli build

check-contract: .installed
	$(VENV_BIN)/python -m solstone.think.contract_cli check
	$(VENV_BIN)/python -m solstone.think.contract_cli build --check

core-fixtures:
	$(VENV_BIN)/python scripts/build_core_fixtures.py

check-core-fixtures: .installed
	$(VENV_BIN)/python scripts/build_core_fixtures.py --check

check-release-advisory-liveness: .installed
	$(VENV_BIN)/python scripts/check_release_advisory_liveness.py

# Re-run the live four-backend integrated-façade cogitate smoke. Spawns an
# external runner script against this venv so the real openhands-sdk Agent path
# is exercised end-to-end. Requires real API keys in env (`ANTHROPIC_API_KEY`,
# `OPENAI_API_KEY`, `GOOGLE_API_KEY`) and `llama-server` on PATH for the `local`
# backend. Catches v1.23-style Agent schema regressions that the openhands-fake
# unit tests cannot. Set COGITATE_SMOKE_RUNNER=/path/to/script to point at the
# runner; there is no default.
COGITATE_SMOKE_RUNNER ?=

smoke-cogitate: .installed
	@test -f "$(COGITATE_SMOKE_RUNNER)" || { echo "cogitate smoke runner not found: $(COGITATE_SMOKE_RUNNER)" >&2; echo "set COGITATE_SMOKE_RUNNER=/path/to/script to override" >&2; exit 1; }
	$(VENV_PY) "$(COGITATE_SMOKE_RUNNER)"

# Operator-opt-in install-state smoke: drives the real install primitives
# (real uv Popen for bundled providers, real httpx for local llama-server +
# GGUF download, real huggingface_hub for MLX snapshot) against a tmp
# journal_config and asserts canonical phase transitions, byte-count
# surfacing, and post-restart state persistence. Hits the same code paths
# the dashboard hits, end-to-end. Heavier than `make test` because it does
# real network fetches; lighter than `make smoke-cogitate` because it does
# not require API keys or a running supervisor.
smoke-install-providers: .installed
	@echo "Running install-state integration smoke..."
	$(PYTEST_BASETEMP_INIT) $(TEST_ENV) $(PYTEST) $(PYTEST_BASETEMP_FLAG) \
	  solstone/apps/settings/tests/test_providers_payload_extended.py \
	  -v --tb=short --timeout=120

release: ## Locked publication entrypoint
	@bash scripts/release.sh

release-test: ## Locked test-publication entrypoint
	@bash scripts/release.sh --test
