# aborist — Makefile entry points
# Every workflow lives behind a `make` target. Bare python commands are not
# the user interface.

# Tools and config
PYTHON       ?= python3
VENV         ?= .venv
PIP          := $(VENV)/bin/pip
PY           := $(VENV)/bin/python
ABORIST      := $(VENV)/bin/aborist

# Data + DB
DATA_DIR     ?= data
WP_BASE_URL  ?= https://dumps.wikimedia.org/archive/2003/2003-05-16/en
WP_CUR       := $(DATA_DIR)/20030516_cur_tablesql.bz2
WP_OLD_1     := $(DATA_DIR)/old_tablesqlbz2.1
WP_OLD_2     := $(DATA_DIR)/old_tablesqlbz2.2
WP_OLD       := $(DATA_DIR)/20030516_old_tablesql.bz2
# Back-compat alias (older callers used WP_DUMP for the cur snapshot).
WP_DUMP      := $(WP_CUR)
DB           ?= $(HOME)/.aborist/aborist.db

# Smoke-test caps so make all stays fast
INGEST_LIMIT ?= 500
VERIFY_N     ?= 10
SEARCH_Q     ?= computer

.PHONY: all bootstrap fetch fetch-cur fetch-old fetch-xml fetch-abstract \
        ingest ingest-cur ingest-old ingest-xml ingest-xml-history \
        ingest-xml-attached ingest-abstract \
        ingest-self ingest-self-providence ingest-git ingest-hg \
        verify search stats test test-live docs docs-api docs-api-clean \
        chain-check chain-check-shards \
        falsify burn burn-kindergarten inspect bootstrap-crawler test-crawler crawl-ingest \
        recrawl-check bench-qa clean clean-db clean-data help

all: bootstrap fetch-cur ingest-cur verify stats ## bootstrap → fetch cur → ingest cur → verify → stats

help: ## show this help
	@awk 'BEGIN{FS=":.*##"} /^[a-zA-Z0-9_-]+:.*##/{printf "  %-16s %s\n", $$1, $$2}' $(MAKEFILE_LIST)

$(VENV)/bin/activate: pyproject.toml
	$(PYTHON) -m venv $(VENV)
	$(PIP) install --upgrade pip wheel
	$(PIP) install -e '.[dev]'
	@touch $(VENV)/bin/activate

bootstrap: $(VENV)/bin/activate ## create venv and install editable package

$(DATA_DIR):
	mkdir -p $(DATA_DIR)

$(WP_CUR): | $(DATA_DIR)
	@echo ">> fetching $(WP_BASE_URL)/$$(basename $@)"
	curl -fL --retry 3 -o $@ "$(WP_BASE_URL)/$$(basename $@)"

$(WP_OLD_1): | $(DATA_DIR)
	@echo ">> fetching $(WP_BASE_URL)/$$(basename $@)"
	curl -fL --retry 3 -o $@ "$(WP_BASE_URL)/$$(basename $@)"

$(WP_OLD_2): | $(DATA_DIR)
	@echo ">> fetching $(WP_BASE_URL)/$$(basename $@)"
	curl -fL --retry 3 -o $@ "$(WP_BASE_URL)/$$(basename $@)"

# The two old_tablesqlbz2.{1,2} parts are split halves of a single bzip2
# stream (.1 is exactly 640 MiB). Concatenate to get a working bz2 file.
$(WP_OLD): $(WP_OLD_1) $(WP_OLD_2)
	@echo ">> concatenating old dump parts"
	cat $(WP_OLD_1) $(WP_OLD_2) > $@

fetch-cur: $(WP_CUR) ## download cur table dump (~82 MB)

fetch-old: $(WP_OLD) ## download old (revision history) parts and concatenate (~893 MB)

fetch: fetch-cur fetch-old ## download all 3 files (cur + old.1 + old.2 + concat)

ingest-cur: bootstrap fetch-cur ## ingest INGEST_LIMIT cur articles
	$(ABORIST) --db $(DB) ingest --source wikipedia_cur --path $(WP_CUR) --limit $(INGEST_LIMIT)

ingest-old: bootstrap fetch-old ## ingest INGEST_LIMIT old (history) revisions
	$(ABORIST) --db $(DB) ingest --source wikipedia_old --path $(WP_OLD) --limit $(INGEST_LIMIT)

# Phase 2: attach-forever sharding. Each shard owns its own SQLite file —
# no WAL contention. Reads via `aborist --shards-dir <dir> <cmd>` attach
# all shards as UNION views. "Merge cost" = 0.
SHARDS ?= 4
SHARDS_DIR ?= $(HOME)/.aborist/shards
ingest-cur-attached: bootstrap fetch-cur ## sharded ingest, no WAL contention (Phase 2)
	@mkdir -p $(SHARDS_DIR)
	@for i in $$(seq 0 $$(($(SHARDS) - 1))); do \
	  $(ABORIST) ingest --source wikipedia_cur --path $(WP_CUR) \
	    --shards-dir $(SHARDS_DIR) --shard $$i/$(SHARDS) & \
	done; wait

ingest-old-attached: bootstrap fetch-old ## sharded ingest of old history (Phase 2)
	@mkdir -p $(SHARDS_DIR)
	@for i in $$(seq 0 $$(($(SHARDS) - 1))); do \
	  $(ABORIST) ingest --source wikipedia_old --path $(WP_OLD) \
	    --shards-dir $(SHARDS_DIR) --shard $$i/$(SHARDS) & \
	done; wait

stats-shards: bootstrap ## cross-shard stats via UNION views over $(SHARDS_DIR)
	$(ABORIST) --shards-dir $(SHARDS_DIR) stats

ACTIVITY_LIMIT ?= 10
activity: bootstrap ## recent Q&A + freshly cached docs (agent timeline)
	$(ABORIST) --shards-dir $(SHARDS_DIR) activity --limit $(ACTIVITY_LIMIT)

inspect: bootstrap ## sidecar diagnose unverified spans for a cache_key: make inspect KEY=hex [JSON=1]
	@if [ -z "$(KEY)" ]; then echo "usage: make inspect KEY=<cache_key> [JSON=1]" >&2; exit 2; fi
	$(ABORIST) --shards-dir $(SHARDS_DIR) inspect --cache-key $(KEY) $(if $(JSON),--json,)

falsify: bootstrap ## mark a cached answer wrong: make falsify KEY=hex REASON='why'
	@if [ -z "$(KEY)" ]; then echo "usage: make falsify KEY=<cache_key> REASON='why'" >&2; exit 2; fi
	$(ABORIST) --shards-dir $(SHARDS_DIR) providence --falsify $(KEY) --reason "$(REASON)"

burn: bootstrap ## delete a leaf with no children. providence: KEY=<cache_key>; document/core: KIND=document|core ROOT=<hex>. REASON='why' [FORCE=1]
	@kind="$${KIND:-providence}"; \
	if [ "$$kind" = "providence" ]; then \
	  if [ -z "$(KEY)" ]; then echo "usage: make burn KEY=<cache_key> REASON='why' [FORCE=1]" >&2; exit 2; fi; \
	  $(ABORIST) --shards-dir $(SHARDS_DIR) burn --kind providence --cache-key $(KEY) --reason "$(REASON)" $(if $(FORCE),--force,); \
	elif [ "$$kind" = "document" ] || [ "$$kind" = "core" ]; then \
	  if [ -z "$(ROOT)" ]; then echo "usage: make burn KIND=$$kind ROOT=<document_root> REASON='why' [FORCE=1]" >&2; exit 2; fi; \
	  $(ABORIST) --shards-dir $(SHARDS_DIR) burn --kind $$kind --root $(ROOT) --reason "$(REASON)" $(if $(FORCE),--force,); \
	else \
	  echo "unknown KIND: $$kind (expected: providence|document|core)" >&2; exit 2; \
	fi

# Mass-burn providence_cache rows younger than the kindergarten window.
# Mirrors mesh sync's kindergarten so what's still un-broadcast is what's
# safe to bust without confusing peers. Useful while iterating on
# retrieval/verifier tunings — wipe recent test runs in one shot.
KG_SECONDS ?= 3600
burn-kindergarten: bootstrap ## bust providence rows < SECONDS old [SECONDS=3600 FORCE=1 DRY_RUN=1 REASON='why']
	$(ABORIST) --shards-dir $(SHARDS_DIR) burn-kindergarten \
	    --kindergarten-seconds $(KG_SECONDS) \
	    $(if $(REASON),--reason "$(REASON)",) \
	    $(if $(FORCE),--force,) \
	    $(if $(DRY_RUN),--dry-run,)

# Multi-source RAG query against the shard cluster.
# Usage: make query Q="What is anarcho-capitalism?"
QUERY_TOP_K ?= 8
# G0 / CTI — pointer-mode is the testing default. Library DEFAULT_POLICY
# stays "quote" so Python callers aren't surprised; the Makefile
# harness ships pointer-mode-on so `make query` exercises the new path
# end-to-end. ANSWER_MODE default flipped to claim_lattice (JSON) on
# 2026-04-30 after the post-retry bench showed it leading on
# strict-rate (50%) and grounded count (54) with 0 errors. Override
# with ANSWER_MODE=claim_lattice_pointer for prose-distribution path
# (Hermes-3 8B reflexive output without grammar guidance), or
# ANSWER_MODE=quote for the legacy substring verifier. ANSWER_MODE=
# (empty) defers to DEFAULT_QUERY_POLICY.
ANSWER_MODE ?= claim_lattice
# BROAD=1 → flip on the Ticket #000008 quantifier-cap apply-gate for
# this call. Default-off per §10.11.3 dry-run discipline; operator
# opts in here for broad-quantifier shapes (winners-of-all,
# tell-me-everything-about-X). Pairs cleanly with the broad-
# quantifier reminder which is default-on for lattice modes.
# Bench (#000008 §12.10): cap-on JSON wins +14pp on STRICT-rate.
# REJECT_BROAD=1 → strict reject for ALL/COMPREHENSIVE/OPEN_REQUEST
# unbounded shapes; returns UNGROUNDED before the LLM call.
# ALLOW_BROAD=1 → emergent search; classifier on, caps off.
query: bootstrap ## ask the corpus a question [JSON=1 BURN=1 REPAIR=1 REPROMPTS=N K="extra retrieval keywords" ANSWER_MODE=claim_lattice|claim_lattice_pointer|quote BROAD=1 REJECT_BROAD=1 ALLOW_BROAD=1]; JSON by default
	@if [ -z "$$Q" ] && [ -z "$(Q)" ]; then \
	  echo "usage: make query Q=\"your question\" [JSON=1 BURN=1 REPAIR=1 REPROMPTS=N K=\"extra retrieval keywords\" ANSWER_MODE=claim_lattice|claim_lattice_pointer|quote BROAD=1 REJECT_BROAD=1 ALLOW_BROAD=1]"; exit 2; \
	fi
	$(ABORIST) --shards-dir $(SHARDS_DIR) query --top-k $(QUERY_TOP_K) $(if $(JSON),--json,) $(if $(BURN),--burn,) $(if $(REPAIR),--repair,) $(if $(REPROMPTS),--repair-reprompts $(REPROMPTS),) $(if $(ANSWER_MODE),--answer-mode $(ANSWER_MODE),) $(if $(K),--retrieval-keywords "$(K)",) $(if $(BROAD),--apply-quantifier-caps,) $(if $(REJECT_BROAD),--reject-broad,) $(if $(ALLOW_BROAD),--allow-broad,) "$(Q)"

query-dry: bootstrap ## like 'make query' but skip the LLM call (dry-run) [JSON=1 BURN=1 ANSWER_MODE=... BROAD=1 REJECT_BROAD=1 ALLOW_BROAD=1]
	@if [ -z "$$Q" ] && [ -z "$(Q)" ]; then \
	  echo "usage: make query-dry Q=\"your question\" [JSON=1 BURN=1 ANSWER_MODE=claim_lattice|claim_lattice_pointer|quote BROAD=1 REJECT_BROAD=1 ALLOW_BROAD=1]"; exit 2; \
	fi
	$(ABORIST) --shards-dir $(SHARDS_DIR) query --top-k $(QUERY_TOP_K) --dry-run $(if $(JSON),--json,) $(if $(BURN),--burn,) $(if $(ANSWER_MODE),--answer-mode $(ANSWER_MODE),) $(if $(BROAD),--apply-quantifier-caps,) $(if $(REJECT_BROAD),--reject-broad,) $(if $(ALLOW_BROAD),--allow-broad,) "$(Q)"

BENCH_QA_QUESTIONS ?= bench/qa_questions.txt
BENCH_QA_OUT       ?= bench/qa_results
BENCH_QA_MODES     ?= quote,claim_lattice_pointer,claim_lattice
BENCH_QA_LIMIT     ?= 0
BENCH_QA_N         ?= 3
BENCH_QA_CONCURRENCY ?= 4
bench-qa: bootstrap ## QA-quality sweep: questions × modes × N samples [BENCH_QA_N=3 BENCH_QA_LIMIT=N BENCH_QA_MODES=... BENCH_QA_CONCURRENCY=4]
	PYTHONUNBUFFERED=1 $(PY) bench/qa_sweep.py \
	    --questions $(BENCH_QA_QUESTIONS) \
	    --shards-dir $(SHARDS_DIR) \
	    --out-dir $(BENCH_QA_OUT) \
	    --top-k $(QUERY_TOP_K) \
	    --modes $(BENCH_QA_MODES) \
	    --limit $(BENCH_QA_LIMIT) \
	    --n $(BENCH_QA_N) \
	    --concurrency $(BENCH_QA_CONCURRENCY)

# Smoke fixture: 5 questions, all anchor classes, all currently failing
# pointer mode 100% while JSON aces 100%. Inner loop for prompt iteration.
# ~30s wall-clock at concurrency=4. Use this between full sweeps.
bench-qa-smoke: bootstrap ## quick 5-question smoke (all anchor classes; ~30s)
	PYTHONUNBUFFERED=1 $(PY) bench/qa_sweep.py \
	    --questions bench/qa_questions_smoke.txt \
	    --shards-dir $(SHARDS_DIR) \
	    --out-dir $(BENCH_QA_OUT) \
	    --top-k $(QUERY_TOP_K) \
	    --modes $(BENCH_QA_MODES) \
	    --n 1 \
	    --concurrency $(BENCH_QA_CONCURRENCY)

test-live: bootstrap ## live QA quality tests against Hermes (gated; -n auto parallel)
	ABORIST_LIVE_TESTS=1 ABORIST_LIVE_SHARDS_DIR=$(SHARDS_DIR) \
	    .venv/bin/pytest tests/test_qa_quality_live.py -v -n auto

# Concept-layer backfill targets. Each runs an extractor across every
# wiki shard; per-shard work is independent so we use GNU-parallel-
# style concurrency with `xargs -P` to overlap the slow paths
# (link_reciprocity ~50s/shard, token_idf ~12s/shard, documents_fts
# ~3s/shard). Total wall-clock with -P 4 vs serial:
#   serial:  link 200s + idf 50s + fts 10s = 260s
#   parallel: link 50s + idf 12s + fts 3s ≈ 65s  (~4× speedup)
CONCEPTS_WORKERS ?= 4
backfill-concepts: bootstrap ## backfill all concept extractors in parallel across shards
	PYTHONUNBUFFERED=1 $(PY) scripts/backfill_concepts.py \
	    --shards-dir $(SHARDS_DIR) \
	    --workers $(CONCEPTS_WORKERS)

# Quick bench mode — 1 sample, smoke fixture, all 3 modes. ~10s.
# For pure smoke after a code change before the longer bench-qa-smoke.
# Emergent stress-test: random word triangulation. Pick 3 words from
# /usr/share/dict/words, ask Hermes @ temp=0.8 to weave them into a
# creative question, send to aborist, append the journey to
# bench/emergent_log.jsonl. Designed for blue-moon cadence — surfaces
# combinatoric failure modes the curated bench-qa fixture set can't.
# Teacher review (Opus) runs separately via `--print-pending`; fox
# brings entries here & gets back guidance to append to the log.
EMERGENT_N    ?= 10
EMERGENT_SEED ?=
bench-emergent: bootstrap ## blue-moon emergent stress test (3-word triangulation; N=10)
	PYTHONUNBUFFERED=1 $(PY) scripts/bench_emergent.py \
	    --n $(EMERGENT_N) \
	    $(if $(EMERGENT_SEED),--seed $(EMERGENT_SEED),) \
	    --shards-dir $(SHARDS_DIR) \
	    --qa-db $(SHARDS_DIR)/qa.db

bench-emergent-pending: bootstrap ## print log entries awaiting teacher review
	$(PY) scripts/bench_emergent.py --print-pending


verify-shards: bootstrap ## cross-shard Merkle round-trip on a random sample
	$(ABORIST) --shards-dir $(SHARDS_DIR) verify -n $(VERIFY_N)

analyze-shards: bootstrap ## cross-shard compression spectrum + audit integrity
	$(ABORIST) --shards-dir $(SHARDS_DIR) analyze

# Audit-chain integrity probe: counts dangling prev_event_hash references.
# Faster than `analyze` and trivially scriptable. 0 = chain intact.
define CHAIN_CHECK_SQL
SELECT COUNT(*) AS chain_breaks FROM audit_events a1
 LEFT JOIN audit_events a2 ON a2.event_hash = a1.prev_event_hash
 WHERE a1.prev_event_hash IS NOT NULL AND a2.event_hash IS NULL
endef
export CHAIN_CHECK_SQL

chain-check: ## audit-chain break count for $(DB) (0 = intact)
	@printf '%s ' "$(DB)"; sqlite3 $(DB) "$$CHAIN_CHECK_SQL"

chain-check-shards: ## audit-chain break count for every *.db in $(SHARDS_DIR)
	@for db in $(SHARDS_DIR)/*.db; do \
	  printf '%s ' "$$db"; sqlite3 "$$db" "$$CHAIN_CHECK_SQL"; \
	done

# Parallel per-shard distill: one process per shard. No DB contention
# because each shard is its own file.
distill-shards-parallel: bootstrap ## one distill process per shard (parallel)
	@for shard in $(SHARDS_DIR)/*.db; do \
	  $(ABORIST) --db $$shard distill --process first-sentence-v1 --kind surface & \
	done; wait

# TF-IDF cores serve as enriched titles for retrieval — distinctive
# low-frequency terms that surface in body text get promoted into
# something queryable without a real title match.
distill-shards-tfidf-parallel: bootstrap ## TF-IDF cores per shard, in parallel
	@for shard in $(SHARDS_DIR)/*.db; do \
	  $(ABORIST) --db $$shard distill --process tfidf-keywords-v1 --kind surface & \
	done; wait

ingest: ingest-cur ## default ingest = cur (use ingest-old or *-parallel for full)

# Grok account-export ETL.
# Point GROK_EXPORT at the directory xAI delivered (the one containing
# `ttl/30d/export_data/<user-id>/prod-grok-backend.json`). The source class
# auto-walks down to find the JSON.
GROK_EXPORT ?= $(HOME)/Downloads/ab8ef1f0-0d08-4f87-89c2-d4509e18115b
# Grok lives in its own shard inside the attach-forever cluster so cross-
# shard queries (`make query`) see it alongside the Wikipedia 2003 corpus.
# Single shard (rank 0/1) — Grok conversations are private and small.
GROK_SHARD := $(SHARDS_DIR)/grok.db
ingest-grok-attached: bootstrap ## ingest Grok conversations into $(GROK_SHARD)
	@mkdir -p $(SHARDS_DIR)
	$(ABORIST) --db $(GROK_SHARD) ingest --source grok_export --path $(GROK_EXPORT) --resume

ingest-grok-media-attached: bootstrap ## ingest Grok media prompts into $(GROK_SHARD)
	@mkdir -p $(SHARDS_DIR)
	$(ABORIST) --db $(GROK_SHARD) ingest --source grok_media --path $(GROK_EXPORT) --resume

# ----------------------------------------------------------------------------
# Phase IV (2006+) Wikipedia XML dumps. Drop-in for any dated snapshot in
# the dumps.wikimedia.org/archive tree by overriding WP_XML_YEAR/MONTH/DATE
# (and WP_XML_LANG for non-English wikis).
#
# Defaults point at enwiki 20101011 (2010-11 archive), the largest snapshot
# in the archive — 6.2 GB compressed, ~3.4M articles.
# Other useful snapshots:
#   make fetch-xml WP_XML_YEAR=2006 WP_XML_MONTH=2006-07 WP_XML_DATE=20061104
#   make fetch-xml WP_XML_YEAR=2006 WP_XML_MONTH=2006-12 WP_XML_DATE=20061130
# ----------------------------------------------------------------------------
WP_XML_LANG  ?= en
WP_XML_YEAR  ?= 2010
WP_XML_MONTH ?= 2010-11
WP_XML_DATE  ?= 20101011
WP_XML_BASE_URL ?= https://dumps.wikimedia.org/archive/$(WP_XML_YEAR)/$(WP_XML_MONTH)/$(WP_XML_LANG)wiki/$(WP_XML_DATE)
WP_XML_FILE     ?= $(WP_XML_LANG)wiki-$(WP_XML_DATE)-pages-articles.xml.bz2
WP_XML          := $(DATA_DIR)/$(WP_XML_FILE)
WP_ABSTRACT_FILE ?= $(WP_XML_LANG)wiki-$(WP_XML_DATE)-abstract.xml
WP_ABSTRACT      := $(DATA_DIR)/$(WP_ABSTRACT_FILE)

$(WP_XML): | $(DATA_DIR)
	@echo ">> fetching $(WP_XML_BASE_URL)/$(WP_XML_FILE)"
	curl -fL --retry 3 -o $@ "$(WP_XML_BASE_URL)/$(WP_XML_FILE)"

$(WP_ABSTRACT): | $(DATA_DIR)
	@echo ">> fetching $(WP_XML_BASE_URL)/$(WP_ABSTRACT_FILE)"
	curl -fL --retry 3 -o $@ "$(WP_XML_BASE_URL)/$(WP_ABSTRACT_FILE)"

fetch-xml: $(WP_XML) ## download Phase IV XML cur dump (default: enwiki 20101011, 6.2 GB)

fetch-abstract: $(WP_ABSTRACT) ## download Phase IV abstract.xml (default: enwiki 20101011, ~3 GB)

ingest-xml: bootstrap fetch-xml ## ingest INGEST_LIMIT pages from $(WP_XML)
	$(ABORIST) --db $(DB) ingest --source wikipedia_xml --path $(WP_XML) --limit $(INGEST_LIMIT)

ingest-xml-history: bootstrap ## ingest every revision (multi-revision mode); set WP_XML to a pages-meta-history file
	$(ABORIST) --db $(DB) ingest --source wikipedia_xml_history --path $(WP_XML) --limit $(INGEST_LIMIT)

# Sharded XML ingest into the attach-forever cluster — same pattern as
# ingest-cur-attached. One process per shard, one SQLite file per shard,
# zero WAL contention.
ingest-xml-attached: bootstrap fetch-xml ## sharded XML ingest, one process per shard
	@mkdir -p $(SHARDS_DIR)
	@for i in $$(seq 0 $$(($(SHARDS) - 1))); do \
	  $(ABORIST) ingest --source wikipedia_xml --path $(WP_XML) \
	    --shards-dir $(SHARDS_DIR) --shard $$i/$(SHARDS) & \
	done; wait

ingest-abstract: bootstrap fetch-abstract ## ingest INGEST_LIMIT abstract docs from $(WP_ABSTRACT)
	$(ABORIST) --db $(DB) ingest --source wikipedia_abstract --path $(WP_ABSTRACT) --limit $(INGEST_LIMIT)

# ----------------------------------------------------------------------------
# Self-ingest: aborist consults its own source code as a queryable corpus.
# Re-running picks up new commits — same path + new content gets a fresh
# document_root chained to the prior version via a `supersedes` edge, so the
# audit chain grows as the repo grows. Lands in a dedicated shard file so it
# doesn't compete with the wikipedia/grok shards' WAL writer lock.
#
# Override SELF_REPO to ingest a different repo's tree.
# ----------------------------------------------------------------------------
SELF_REPO  ?= $(CURDIR)
SELF_SHARD := $(SHARDS_DIR)/aborist-self.db

ingest-self: bootstrap ## ingest this repo's HEAD into a dedicated shard
	@mkdir -p $(SHARDS_DIR)
	$(ABORIST) --db $(SELF_SHARD) ingest --source git_repo --path $(SELF_REPO)

# Self-reference: promote STRICT live providence records past the
# kindergarten window into each shard's documents table. Each shard
# self-promotes only its own records; cross-shard sharing happens
# via the existing shards-dir UNION at retrieval time. Run on a cron
# (e.g. hourly) to keep the substrate fresh.
# See docs/self-reference-thought-chains-design.md.
KG_SECONDS ?= 3600
ingest-self-providence: bootstrap ## promote STRICT live providence records into the document corpus [KG_SECONDS=3600]
	@mkdir -p $(SHARDS_DIR)
	@for db in $(SHARDS_DIR)/*.db; do \
	  echo ">> promoting providence records: $$db"; \
	  $(ABORIST) --db $$db ingest --source providence --kindergarten-seconds $(KG_SECONDS); \
	done

# Generic git-repo ingest: aim it at any local clone via GIT_REPO=...
GIT_REPO   ?= $(CURDIR)
GIT_SHARD  := $(SHARDS_DIR)/$(notdir $(GIT_REPO))-git.db
ingest-git: bootstrap ## ingest GIT_REPO=<path> into its own shard
	@mkdir -p $(SHARDS_DIR)
	$(ABORIST) --db $(GIT_SHARD) ingest --source git_repo --path $(GIT_REPO)

# Generic hg-repo ingest. HG_REPO=<path>.
HG_REPO    ?=
HG_SHARD   := $(SHARDS_DIR)/$(notdir $(HG_REPO))-hg.db
ingest-hg: bootstrap ## ingest HG_REPO=<path> (mercurial) into its own shard
	@if [ -z "$(HG_REPO)" ]; then echo "usage: make ingest-hg HG_REPO=/path/to/repo" >&2; exit 2; fi
	@mkdir -p $(SHARDS_DIR)
	$(ABORIST) --db $(HG_SHARD) ingest --source hg_repo --path $(HG_REPO)

verify: bootstrap ## round-trip Merkle proofs for VERIFY_N random documents
	$(ABORIST) --db $(DB) verify -n $(VERIFY_N)

search: bootstrap ## keyword search; override SEARCH_Q (or pass Q=...)
	$(ABORIST) --db $(DB) search '$(if $(Q),$(Q),$(SEARCH_Q))'

stats: bootstrap ## counts: documents, chunks, edges, audit chain
	$(ABORIST) --db $(DB) stats

test: bootstrap ## run pytest suite (excludes opt-in crawler tests)
	$(VENV)/bin/pytest -q --ignore=tests/crawler -n auto

# Crawler tests are off-by-default — they hit the network in many cases
# and require the heavy [crawler] extras (aiohttp, bs4, lxml, etc.).
# Bootstrap installs the extras into the existing venv idempotently.
bootstrap-crawler: bootstrap ## install [crawler] extras into the venv
	$(PIP) install -e '.[crawler]'

test-crawler: bootstrap-crawler ## run only the lifted crawler tests
	$(VENV)/bin/pytest -q tests/crawler

# Bridge target: BFS a seed URL, ingest discovered pages into a shard,
# capture ETag/Last-Modified for each so recrawl-check can do conditional
# HEAD requests later. URL is required; DEPTH and MAX have safe defaults.
CRAWL_DEPTH ?= 2
CRAWL_MAX   ?= 0
# Per-domain shard so `make query` (which reads $(SHARDS_DIR)) sees the
# crawled content. Shard filename derived from the seed URL's hostname:
#   https://russell.ballestrini.net  ->  $(SHARDS_DIR)/crawl_russell_ballestrini_net.db
# Override with CRAWL_SHARD=... when you want a custom path.
crawl-ingest: bootstrap-crawler ## crawl URL=https://x.com [DEPTH=2 MAX=0 FAST=1] into $(SHARDS_DIR)/crawl_<domain>.db
	@if [ -z "$(URL)" ]; then echo "usage: make crawl-ingest URL=https://example.com [DEPTH=2 MAX=0 FAST=1 CRAWL_SHARD=path]" >&2; exit 2; fi
	@mkdir -p $(SHARDS_DIR)
	@shard="$(CRAWL_SHARD)"; \
	if [ -z "$$shard" ]; then \
	  domain=$$(echo "$(URL)" | sed -E 's,^https?://([^/]+).*$$,\1,' | tr '.' '_'); \
	  shard="$(SHARDS_DIR)/crawl_$${domain}.db"; \
	fi; \
	echo "  shard: $$shard" >&2; \
	$(ABORIST) --db "$$shard" crawl --seed-url "$(URL)" --depth $(CRAWL_DEPTH) --max-pages $(CRAWL_MAX) $(if $(FAST),--fast,) --ingest

# Fast freshness probe: conditional HEAD per doc, classify fresh/stale/gone.
# Send only If-None-Match + If-Modified-Since headers — server returns 304
# with no body when content unchanged.
RECRAWL_LIMIT ?= 100
# By default check every shard under $(SHARDS_DIR). Pass CRAWL_SHARD=path
# to scope to one. DOMAIN= filters to URLs containing the substring.
recrawl-check: bootstrap-crawler ## conditional HEAD per ingested doc [DOMAIN=x.com LIMIT=100 CRAWL_SHARD=path]
	@if [ -n "$(CRAWL_SHARD)" ]; then \
	  $(ABORIST) --db $(CRAWL_SHARD) crawler recrawl-check $(if $(DOMAIN),--domain $(DOMAIN),) --limit $(RECRAWL_LIMIT); \
	else \
	  for db in $(SHARDS_DIR)/*.db; do \
	    case "$$(basename $$db)" in qa.db|snapshots.db) continue;; esac; \
	    echo "  shard: $$db" >&2; \
	    $(ABORIST) --db "$$db" crawler recrawl-check $(if $(DOMAIN),--domain $(DOMAIN),) --limit $(RECRAWL_LIMIT); \
	  done; \
	fi

DOT_SRCS := $(wildcard docs/diagrams/*.dot)
DOT_PNGS := $(DOT_SRCS:.dot=.png)
DOT_SVGS := $(DOT_SRCS:.dot=.svg)

docs/diagrams/%.png: docs/diagrams/%.dot
	dot -Tpng $< -o $@

docs/diagrams/%.svg: docs/diagrams/%.dot
	dot -Tsvg $< -o $@

docs: $(DOT_PNGS) $(DOT_SVGS) ## render docs/diagrams/*.dot -> .png + .svg via graphviz

docs-api: ## generate Sphinx API reference from docstrings (output: docs/_source/_build/html/)
	$(PY) -m sphinx.cmd.build -b html docs/_source docs/_source/_build/html

docs-api-clean: ## remove Sphinx build artifacts
	rm -rf docs/_source/_build/

# Reproducible micro-benchmark over a fixed slice of cur. Lets you compare
# ETL throughput across configs and catches regressions on optimization
# work. Override BENCH_DOCS=N (default 5000).
BENCH_DOCS ?= 5000
BENCH_DIR  := /tmp/aborist-bench
bench: bootstrap fetch-cur ## benchmark serial vs parallel-shared vs attached at $(BENCH_DOCS) docs
	@bash bench/run.sh $(BENCH_DOCS)

clean: ## remove venv + caches (keeps fetched data and db)
	rm -rf $(VENV) .pytest_cache **/__pycache__ aborist.egg-info
	find . -type d -name __pycache__ -prune -exec rm -rf {} +

clean-db: ## drop the aborist db (keeps fetched data and venv)
	rm -f $(DB) $(DB)-journal $(DB)-wal $(DB)-shm

clean-data: ## remove fetched dumps
	rm -rf $(DATA_DIR)
