# Context Vault — developer tasks.  Run `make` (or `make help`) to list targets.
.DEFAULT_GOAL := help
DIAGRAMS_DIR := docs/diagrams

.PHONY: help diagrams diagrams-check diagrams-install hooks test test-durations harness-validate learning-drift learning-validate learning-evidence-paths harness-install bench verify sandbox deploy loop-build loop-build-smoke

help: ## list available targets
	@grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) \
		| awk 'BEGIN{FS=":.*?## "}{printf "  \033[36m%-18s\033[0m %s\n", $$1, $$2}'

diagrams: ## render §17 SVG + PNG from docs/SYSTEM_DESIGN.md (needs system Chrome)
	python3 $(DIAGRAMS_DIR)/render.py

diagrams-check: ## verify committed diagrams are in sync (browser-free; CI / pre-commit)
	python3 $(DIAGRAMS_DIR)/render.py --check

diagrams-install: ## install the mermaid CLI locally (reuses system Chrome, no Chromium dl)
	cd $(DIAGRAMS_DIR) && PUPPETEER_SKIP_DOWNLOAD=true npm install --no-fund --no-audit

hooks: ## activate committed git hooks (pre-commit auto-refreshes §17 diagrams)
	@git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { \
		echo "not a git repository yet — run 'git init' first, then 'make hooks'"; exit 1; }
	git config core.hooksPath scripts/git-hooks
	@echo "git hooks active → scripts/git-hooks (pre-commit refreshes §17 diagrams)"

test: ## run the python test suite
	uv run --extra dev pytest -q

# Regenerate .test_durations for the sharded test-unit lane (.github/workflows/ci.yml → test-unit).
# pytest-split reads this file at rootdir to TIME-balance the 3 shards; regenerate when the suite grows
# materially (it is drift-tolerant meanwhile — new/unknown tests get the average).
# RULE: the capture MUST mirror the lane, or the balance is wrong by construction —
#   • SAME SELECTION (`-m "not live_db"`): the lane deselects the shared-DB tests, so they carry NO
#     entry here (do NOT record them; do NOT hand-zero them either — never hand-edit this file).
#   • CLEAN SLATE (`rm -f` first): `--store-durations` MERGES into any existing file — it updates the
#     tests that RAN and KEEPS stale entries for ones that did not (the deselected live_db set). Over a
#     stale file that leaves old live_db stall/zero entries in forever (they pass through untouched);
#     removing it first rebuilds the file from ONLY the not-live_db tests actually run.
#   • SERIAL (no `-n auto`): committed balancing input, so fidelity beats speed — a parallel capture
#     mis-attributes wall-clock across xdist workers.
#   • DBLESS + DEAD-PORT DSNs (below): the lane runs with no service DBs, so mirror that. The dead
#     ports also make the capture self-healing: a dev box with `docker compose up` cannot contaminate
#     it, and any FUTURE unmarked DB-touching test records its TRUE ~30s CI cost (the psycopg-pool
#     reconnect window on a refused port) instead of a fast local hit — so it lands in its own balanced
#     shard rather than surprising CI. Serial, so run it expecting several minutes.
test-durations: ## regenerate .test_durations for the CI shard-balancer (dbless; mirrors the test-unit lane)
	rm -f .test_durations   # clean slate — --store-durations MERGES, so purge stale/deselected entries first
	VAULT_EMBEDDER=hash VAULT_JUDGE=heuristic \
	VAULT_PG_DSN=postgresql://vault:x@localhost:9/vault_audit NEO4J_URI=bolt://localhost:9 \
	VAULT_VECTOR_QDRANT_URL=http://localhost:9 \
	uv run --extra dev --extra self-host --extra api --extra mcp \
		pytest -q -m "not live_db" --store-durations
	@echo "wrote .test_durations — commit it; the test-unit shards auto-read it at rootdir"

harness-validate: ## validate the dev-team harness (agents/commands/skills/rules/hooks well-formed + wired)
	python3 scripts/ci/validate_harness.py

learning-drift: ## describe-only gate: fail if learning-loop output (a learned instinct / promotion banner) leaked into the committed catalog (skills/commands/agents/hooks)
	@python3 -c "import sys, pathlib; sys.path.insert(0, 'scripts/hooks/lib'); import instincts; \
ps = instincts.learning_store_drift(pathlib.Path('.')); \
[print(f\"  - {p['path']}:{p['line']} [{p['signature']}] {p['detail']}\") for p in ps]; \
print('learning-store drift: OK (catalog clean — no loop output leaked)') if not ps else print(f'FAILED: {len(ps)} drift issue(s) — the loop must never write the catalog', file=sys.stderr); \
sys.exit(1 if ps else 0)"

learning-validate: ## /learn-time diagnostic: report learned/*.yaml that won't recall (missing id/confidence/action, below the 0.70 floor). NOT a CI gate — learned/ is gitignored runtime state, absent in CI.
	@python3 -c "import sys, pathlib; sys.path.insert(0, 'scripts/hooks/lib'); import instincts; \
ps = instincts.validate_instinct_files(pathlib.Path('.')); \
[print(f\"  - {p['path']} [{p['id']}] {p['problem']}: {p['reason']}\") for p in ps]; \
print('instinct files: OK (all parse + will recall)') if not ps else print(f'{len(ps)} instinct-file problem(s) — these will not recall as intended', file=sys.stderr)"

learning-evidence-paths: ## OPTIONAL manual /learn diagnostic: list learned instincts whose ## Evidence cites a repo path that no longer resolves. ~50% FALSE POSITIVES (package-relative shorthand). NOT a gate, NOT wired to ledger/nudge/CI.
	@python3 scripts/hooks/check_evidence_paths.py .

harness-install: ## wire the harness into Claude Code + Cursor (.claude|.cursor/{agents,commands,skills} → the root catalog)
	@for d in agents commands skills; do rm -rf ".claude/$$d"; ln -s "../$$d" ".claude/$$d"; done
	@for d in agents commands skills; do rm -rf ".cursor/$$d"; ln -s "../$$d" ".cursor/$$d"; done
	@echo "harness wired → .claude|.cursor/{agents,commands,skills}; Claude Code + Cursor load the agents/skills/commands next session (reload the Cursor window)"

bench: ## silent-corruption backstop (offline heuristic judge, the make-or-break metric)
	uv run python eval/bench.py --subject heuristic --gold core \
		--max-silent-corruption 0.30 --min-accuracy 0.45

sandbox: ## seed an ephemeral evaluation sandbox (synthetic data) into the local stack
	uv run python eval/seed_sandbox.py

saas-demo: ## (ADVISORY, never gates) offline SaaS control-plane demo: provision/meter/entitle/suspend/offboard two tenants over the shipped orchestrators. No Neo4j/Postgres/key. See eval/saas_demo.py.
	uv run python eval/saas_demo.py

deploy: ## one-command self-host: generate secrets (first run) + bring up the production stack
	./scripts/deploy.sh

loop-build: ## (slice-1) contained+audited build-green loop: drives a worktree branch to `make verify` green, then STOPS (a human runs /ship). Foreground+human-watched. See commands/loop-build.md.
	./scripts/loop/loop_build.sh $(LOOP_ARGS)

# (founder-run, CI-EXCLUDED) the real-model end-to-end SMOKE — NOT a gate (ADR-0044 §S2-E.2). Seeds a
# deterministic RED fixture on a throwaway base branch, then drives it GREEN with a REAL `claude -p`
# via the existing model-step seam: <=4 iter / <=300s hard wall / token-capped. Needs ANTHROPIC_API_KEY
# on the founder host (the trusted launcher's own egress; the jailed model step stays --unshare-net).
# It proves the real-model RED→GREEN wiring on a SEEDED target; it STOPS without merging (a human runs
# /ship). The CI lanes (bench, `pytest -q`) never invoke it. See commands/loop-build.md → "Real-model
# smoke". Override caps/args via LOOP_SMOKE_ARGS; the token cap via LOOP_SMOKE_MAX_TOKENS.
LOOP_SMOKE_MAX_TOKENS ?= 200000
loop-build-smoke: ## (founder-run, CI-EXCLUDED) real-model smoke: seed a RED fixture + drive it GREEN with real `claude -p` (<=4 iter, <=300s, token-capped). Needs ANTHROPIC_API_KEY + (for the jail) Linux. NOT a gate. See commands/loop-build.md.
	@command -v claude >/dev/null 2>&1 || { echo "loop-build-smoke: no \`claude\` CLI on PATH (this is a founder-run real-model smoke — install claude + set ANTHROPIC_API_KEY)"; exit 2; }
	@base_ref="$$(./tests/fixtures/loop_smoke/red_branch.sh)"; \
	  echo "loop-build-smoke: seeded RED fixture on base ref: $$base_ref"; \
	  LOOP_BASE_REF="$$base_ref" \
	  LOOP_VERIFY_CMD="python3 -m pytest tests/fixtures/loop_smoke/test_widget.py -q" \
	  ./scripts/loop/loop_build.sh \
	    --task 'fix the one failing test in tests/fixtures/loop_smoke/widget.py — edit ONLY that file; do not touch eval/ or context_vault/conflict/' \
	    --max-iterations 4 --hard-wall-seconds 300 --max-tokens $(LOOP_SMOKE_MAX_TOKENS) \
	    $(LOOP_SMOKE_ARGS); \
	  rc=$$?; \
	  echo "loop-build-smoke: cleanup — removing the throwaway base branch $$base_ref"; \
	  git branch -D "$$base_ref" >/dev/null 2>&1 || true; \
	  exit $$rc

# (opt-in maintenance, CI-EXCLUDED) Sweep stale `loop/build-green/*` launcher-debris branches — the loop
# spawns one throwaway branch per run (loop_build.sh:608) and the test suite leaks ~one per launcher
# subprocess, so hundreds pile up. DRY-RUN ONLY + read-only here: it prints the CANDIDATE/SKIP/KEEP verdict
# per branch plus the exact `--apply` command. Deletion stays a human-gated DIRECT script call (that
# printed command) — never a make target, so `make` alone can never delete a branch. Not wired into
# pytest / verify / any gating CI (same spirit as loop-build-smoke).
loop-debris-sweep: ## (opt-in maintenance, CI-EXCLUDED) list stale `loop/build-green/*` launcher-debris candidates — DRY-RUN only; deletion is human-gated via the script's `--apply` (the dry run prints the exact command). NEVER wired into verify/CI.
	uv run python scripts/loop/loop_debris_sweep.py

verify: ## the one-shot green check: harness + tests + diagrams + bench backstop
	@echo "==> harness" && $(MAKE) -s harness-validate
	@echo "==> diagrams" && $(MAKE) -s diagrams-check
	@echo "==> tests" && uv run --extra dev pytest -q
	@echo "==> bench (heuristic backstop)" && $(MAKE) -s bench
	@echo "verify: all checks passed"
