# 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 harness-sync learning-drift learning-validate learning-evidence-paths harness-install bench verify sandbox deploy stack-up stack-check loop-build loop-build-smoke jail-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"

# ADR-0054 Phase 3: the harness mechanism + generic catalog live in the pinned `harness/` submodule; the
# validator runs from there in the CONSUMER profile against THIS vault tree (--root .). An UNINITIALIZED
# submodule fails LOUD with the exact recovery command (never a silent skip — a missing mount must RED).
harness-validate: ## validate the consumed harness (consumer profile) against this vault tree
	@test -f harness/scripts/ci/validate_harness.py || { echo "harness/ is not initialized — run: make harness-sync"; exit 1; }
	python3 harness/scripts/ci/validate_harness.py --profile consumer --root .

harness-sync: ## initialize/update the pinned harness submodule to the committed pin, then compose the catalog
	git submodule update --init harness
	@$(MAKE) -s harness-install

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, 'harness/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, 'harness/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 harness/scripts/hooks/check_evidence_paths.py .

# ADR-0054 Phase 3: the catalog is a COMPOSITION — the vault OVERLAY (the crown-jewel specialist agents,
# product skills, /bench + /diagrams, committed as real files under root {agents,commands,skills}) UNIONED
# with the harness submodule's GENERICS (per-entry symlinks into harness/{agents,commands,skills}, `overlay
# wins` on any name collision, gitignored — a symlink into the submodule is regenerated, never committed).
# Then .claude|.cursor/{agents,commands,skills} are wired to that composed root catalog so Claude Code AND
# Cursor discover the full roster. Idempotent + re-run safe: stale generic symlinks are pruned first.
harness-install: ## compose the harness catalog (vault overlay ∪ submodule generics) + wire Claude Code/Cursor
	@test -d harness/agents || { echo "harness/ is not initialized — run: make harness-sync (git submodule update --init harness)"; exit 1; }
	@for d in agents commands skills; do \
		find "$$d" -maxdepth 1 -type l -exec rm -f {} + 2>/dev/null || true; \
		for src in harness/$$d/*; do \
			name="$$(basename "$$src")"; \
			[ -e "$$d/$$name" ] || ln -s "../harness/$$d/$$name" "$$d/$$name"; \
		done; \
	done
	@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 catalog composed → root {agents,commands,skills} = vault overlay ∪ harness/ generics (overlay wins); .claude|.cursor wired (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

# The staleness-proof DEV turnkey path (2026-08-04 footgun). `docker compose up -d` builds the api +
# web images ONCE and then keeps them indefinitely — it reports "Healthy" while serving a stale
# locally-built image (e.g. an api that predates a new route → 404 → broken UI mid-demo), and even
# `build … && up -d` won't swap a running container without --force-recreate. This target makes that
# impossible: it (re)builds ONLY the app images at the current commit and force-recreates ONLY
# api+web, leaving neo4j/postgres/qdrant — your data — untouched. GIT_SHA bakes the commit into each
# image's provenance label so `stack-check` (run last, advisory) can flag drift later.
stack-up: ## (dev) build app images at HEAD + force-recreate ONLY api+web (DB data preserved) — the stale-image-proof turnkey path
	docker compose up -d neo4j postgres qdrant
	GIT_SHA=$$(git rev-parse HEAD 2>/dev/null || echo unknown) docker compose build api web
	docker compose up -d --force-recreate --no-deps api web
	@$(MAKE) -s stack-check || true

stack-check: ## (dev) warn if the running vault-api / vault-web predates the working tree (stale-image tripwire; reads image metadata, no network surface)
	@scripts/stack_preflight.sh

# ADR-0054 Phase 3: the build-green loop launcher + its bwrap jail + the loop/jail test suites now live in
# the `harness/` submodule and CERTIFY in the paramind-harness repo's own CI (the vault CI no longer runs
# the loop-jail-kernel-deny lane). These targets DELEGATE to the submodule scripts for a founder-run pass;
# the launcher reads the vault's `.harness.toml [success_gate]` from the superproject root at startup.
loop-build: ## (slice-1) contained+audited build-green loop (delegates to the harness submodule launcher; certifies in the harness repo CI). See harness/docs + commands/loop-build.md.
	@test -x harness/scripts/loop/loop_build.sh || { echo "harness/ is not initialized — run: make harness-sync"; exit 1; }
	./harness/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; }
	@test -x harness/scripts/loop/loop_build.sh || { echo "harness/ is not initialized — run: make harness-sync"; exit 2; }
	@base_ref="$$(./harness/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 harness/tests/fixtures/loop_smoke/test_widget.py -q" \
	  ./harness/scripts/loop/loop_build.sh \
	    --task 'fix the one failing test in harness/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

# (founder-run, CI-EXCLUDED) Docker+bwrap mirror of the jail kernel-deny suite — delegates to the harness
# submodule (harness/scripts/loop/jail_smoke.sh). ADR-0054: the loop-jail-kernel-deny lane + its node-id /
# named-test SOURCE OF TRUTH moved to the paramind-harness repo's own CI (the vault CI no longer carries
# it), so this local mirror + its lists now live and certify there. Needs Docker; the host repo is mounted
# READ-ONLY + copied container-local so `git status` stays clean. NOT a gate. Diagnostic dump via
# JAIL_SMOKE_DIAG=1; negative control (PROVE should RED) via JAIL_SMOKE_DROP_SECCOMP=1.
jail-smoke: ## (founder-run, CI-EXCLUDED) Docker+bwrap jail mirror — delegates to the harness submodule; the kernel-deny lane now certifies in the paramind-harness repo CI. Needs Docker. NOT a gate.
	@test -x harness/scripts/loop/jail_smoke.sh || { echo "harness/ is not initialized — run: make harness-sync"; exit 2; }
	./harness/scripts/loop/jail_smoke.sh

# (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`. Delegates to the harness submodule. NEVER wired into verify/CI.
	@test -f harness/scripts/loop/loop_debris_sweep.py || { echo "harness/ is not initialized — run: make harness-sync"; exit 2; }
	uv run python harness/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"
