arborist/arborist/cli.py
russell@unturf.com 331e748bcb
cloud ask: unified multi-shard via bucket manifest + read-ahead tuning
`BUCKET_URL` (one env var) → client GETs `clones/manifest.json` →
opens HttpRangeVFS per listed shard → FTS5 across all shards in
parallel (ThreadPoolExecutor; per-thread apsw.Connection) → merge by
BM25 score → pull chunks from the owning shard → LLM + verify.
No per-query --shard-url, no path proliferation.

Two manifests published on s3://arborist/clones/:
  manifest.json       — default: virtback only (2.5MB, ~5s/query)
  manifest-full.json  — opt-in: all 5 shards (35GB, prohibitive over
                        WAN due to FTS5 b-tree walk pattern; needs
                        smaller shards or co-located query proxy)

HttpRangeVFS read-ahead tuned from per-page (4KB) to 64KB block-aligned
cache. Each cache miss fetches one 64KB block; subsequent reads within
the block are local-fast. Lower miss count, similar bytes-on-wire
(64KB amortizes well over typical 4-16 page b-tree clusters; larger
read-ahead like 4MB over-fetches on random FTS5 reads).

Sample run (default manifest):
    make cloud-ask Q="who developed virt-back?"
    → EVIDENCE-WARRANTED · via claim_lattice  1/1  4.71s  (bucket-direct)
       21 HTTP requests · 1344 KB

ACL: genesis full-bench shards flipped to public-read (CC-BY-SA
Wikipedia content). Reachable now if you want to play with the slow
multi-shard path; not in the default manifest because chat latency
matters more than coverage breadth.
2026-05-30 13:33:58 -04:00

7794 lines
289 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Arborist CLI: ingest / search / verify / stats."""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
from arborist import __version__
from arborist.ingest import ingest_source, verify_random_sample
from arborist.progress import Progress
from arborist.search import FTS5Backend
from arborist.sources import WikipediaCurDump
from arborist.store import (
DEFAULT_DB_PATH,
append_audit,
connect,
connect_query,
connect_readonly,
stats,
transaction,
)
def _cmd_ingest(args: argparse.Namespace) -> int:
"""Ingest a corpus (Wikipedia, HTML, git, etc.) into a shard."""
if args.source in ("wikipedia_cur", "wikipedia_old"):
if not args.path:
print(f"--path is required for {args.source}", file=sys.stderr)
return 2
from arborist.sources import WikipediaSqlDump
table = "cur" if args.source == "wikipedia_cur" else "old"
shard = None
if args.shard:
rank_str, total_str = args.shard.split("/", 1)
shard = (int(rank_str), int(total_str))
src = WikipediaSqlDump(path=args.path, table=table, shard=shard)
elif args.source == "html": # noqa: SIM114 — keep branch shape
try:
from arborist.sources import HtmlPageSource
except ImportError:
print(
"html source requires extras: pip install 'arborist[html]'",
file=sys.stderr,
)
return 2
urls: list[str] = list(args.url or [])
if args.urls_from:
urls.extend(
line.strip()
for line in Path(args.urls_from).read_text(encoding="utf-8").splitlines()
if line.strip() and not line.lstrip().startswith("#")
)
if not urls:
print("html source needs --url or --urls-from", file=sys.stderr)
return 2
src = HtmlPageSource(
urls,
respect_robots=not args.no_robots,
default_author=getattr(args, "author", None),
)
elif args.source in ("grok_export", "grok_media"):
if not args.path:
print(f"--path is required for {args.source}", file=sys.stderr)
return 2
from arborist.sources import GrokExportSource, GrokMediaPostsSource
cls = GrokExportSource if args.source == "grok_export" else GrokMediaPostsSource
src = cls(path=args.path)
elif args.source == "textbook_tex":
# PG-style LaTeX source ingest (#000031). Each --url or --bundle
# points at a TeX URL (e.g., PG eBook /files/N/N-t/N-t.tex).
# `--urls-from FILE` for a list. The strip-tex pipeline produces
# plain prose suitable for the standard 512-token chunker.
urls: list[str] = list(getattr(args, "url", None) or [])
if getattr(args, "bundle", None):
urls.extend(args.bundle)
if args.urls_from:
urls.extend(
line.strip()
for line in Path(args.urls_from).read_text(encoding="utf-8").splitlines()
if line.strip() and not line.lstrip().startswith("#")
)
if not urls:
print(
"textbook_tex source needs --url, --bundle, or --urls-from",
file=sys.stderr,
)
return 2
from arborist.sources import TextbookTexSource
src = TextbookTexSource(
urls,
default_author=getattr(args, "author", None),
)
elif args.source == "claim_pack":
# Companion JSON bundles (axiom + theorem packs) — see ticket
# #000029. --bundle is repeatable so axiom-bundle and theorem-bundle
# parse together, allowing cross-bundle pillar_reference edges to
# resolve to specific record URIs. A single --path also works for
# one-bundle ingest.
paths: list[str] = []
if getattr(args, "bundle", None):
paths.extend(args.bundle)
if args.path:
paths.append(args.path)
if not paths:
print(
"claim_pack needs --bundle (repeatable) or --path",
file=sys.stderr,
)
return 2
from arborist.sources import ClaimPackSource
src = ClaimPackSource(paths)
elif args.source in ("wikipedia_xml", "wikipedia_xml_history", "wikipedia_abstract"):
if not args.path:
print(f"--path is required for {args.source}", file=sys.stderr)
return 2
from arborist.sources import WikipediaAbstractDump, WikipediaXmlDump
if args.source == "wikipedia_abstract":
src = WikipediaAbstractDump(path=args.path)
else:
shard = None
if args.shard:
rank_str, total_str = args.shard.split("/", 1)
shard = (int(rank_str), int(total_str))
src = WikipediaXmlDump(
path=args.path,
shard=shard,
multi_revision=(args.source == "wikipedia_xml_history"),
)
elif args.source in ("git_repo", "hg_repo"):
if not args.path:
print(f"--path is required for {args.source}", file=sys.stderr)
return 2
from arborist.sources import GitRepoSource, MercurialRepoSource
cls = GitRepoSource if args.source == "git_repo" else MercurialRepoSource
src = cls(repo_path=args.path)
elif args.source == "providence":
# Self-reference: promote STRICT live providence_cache records
# past the kindergarten window into the document corpus.
# See docs/self-reference-design.md.
#
# NOTE: ``connect`` lives at module scope (line 19). Re-importing
# it inside this branch makes Python's compiler treat ``connect``
# as a function-local for the entire ``_cmd_ingest`` body, which
# breaks the module-level binding used at line 177 for *every*
# non-providence source. Don't add a local re-import here.
from arborist.sources.providence import (
DEFAULT_KINDERGARTEN_SECONDS,
ProvidenceSource,
)
# The source reads from the SAME shard it's writing into —
# promote each shard's own STRICT records to its own
# documents table. Cross-shard promotion runs as a separate
# invocation per shard.
target_db_for_read = args.db
if args.shards_dir and args.shard:
rank_str, total_str = args.shard.split("/", 1)
rank = int(rank_str)
total = int(total_str)
digits = max(3, len(str(total - 1)))
target_db_for_read = Path(args.shards_dir) / f"{rank:0{digits}d}.db"
if not target_db_for_read:
print("--db or --shards-dir + --shard required for providence source", file=sys.stderr)
return 2
kg_seconds = int(getattr(args, "kindergarten_seconds", None) or DEFAULT_KINDERGARTEN_SECONDS)
# Open a separate connection for reading; ingest opens its own
# write connection downstream.
read_conn = connect(target_db_for_read)
src = ProvidenceSource(read_conn, kindergarten_seconds=kg_seconds)
else:
print(f"unknown source: {args.source}", file=sys.stderr)
return 2
# Resolve target DB: if --shards-dir is set with --shard, write to a
# per-shard file. Each shard owns its own SQLite file, so N parallel
# ingests have ZERO writer-lock contention.
target_db = args.db
if args.shards_dir:
if not args.shard:
print(
"--shards-dir requires --shard rank/total",
file=sys.stderr,
)
return 2
rank_str, total_str = args.shard.split("/", 1)
rank = int(rank_str)
total = int(total_str)
shards_dir = Path(args.shards_dir)
shards_dir.mkdir(parents=True, exist_ok=True)
digits = max(3, len(str(total - 1)))
target_db = shards_dir / f"{rank:0{digits}d}.db"
progress: Progress | None = None
if not args.quiet:
prefix = ""
if args.shard:
prefix = f"[shard {args.shard}] "
progress = Progress(
interval=args.progress_interval,
total_estimate=args.total_estimate,
prefix=prefix,
)
conn = connect(target_db)
embedded = None
try:
result = ingest_source(
conn,
src,
chunker_name=args.chunker,
limit=args.limit,
batch_size=args.batch_size,
resume=args.resume,
progress=progress,
loss_report_enabled=not args.no_loss_report,
loss_report_excerpts=not args.no_loss_excerpts,
loss_report_max_excerpt_bytes=args.loss_excerpt_bytes,
)
if getattr(args, "embed", False):
# Eager opt-in: after the chunk+Merkle-commit pass, embed the
# chunks this run added (incremental — only chunk_ids not
# already in chunk_vecs). Default ingest does NOT embed; the
# lazy out-of-band pass (`arborist embed`, or a cron, or the
# Prometheus-Σ unconscious sweep) is the usual path. See #000039.
from arborist.search.vec import embed_documents
embedded = embed_documents(conn, incremental=True)
finally:
conn.close()
out = dict(result.__dict__)
if embedded is not None:
out["chunks_embedded"] = embedded
print(json.dumps(out, indent=2, ensure_ascii=False))
return 0
def _cmd_search(args: argparse.Namespace) -> int:
"""Lexical (FTS5) or semantic (vec) query against chunks; hits as JSON/text."""
backend_name = getattr(args, "backend", "fts5")
conn = (
connect_query(args.db, shards_dir=args.global_shards_dir)
if args.global_shards_dir
else connect(args.db)
)
try:
if backend_name == "vec":
from arborist.search import VecBackend # type: ignore
backend = VecBackend(conn)
if not backend.populated():
print(
"chunk_vecs is empty or missing on this DB — run "
"`arborist --db <db> embed` first.",
file=sys.stderr,
)
return 1
else:
backend = FTS5Backend(conn)
hits = backend.search(args.query, limit=args.limit)
finally:
conn.close()
if args.json:
print(
json.dumps(
[
{
"document_root": h.document_root,
"document_uri": h.document_uri,
"chunk_idx": h.chunk_idx,
"snippet": h.snippet,
"score": h.score,
"audit_mode": h.audit_mode.value,
"title": h.title,
}
for h in hits
],
indent=2, ensure_ascii=False
)
)
else:
for h in hits:
print(f"[{h.audit_mode.value}] {h.score:7.3f} {h.title or h.document_uri}")
print(f" chunk {h.chunk_idx} root={h.document_root[:16]}")
print(f" {h.snippet}")
print()
return 0
def _cmd_embed(args: argparse.Namespace) -> int:
"""Populate chunk_vecs (semantic embeddings) for the --db shard.
Writes a sibling vec0 virtual table; does not touch chunks /
documents / the audit chain. First run loads (and downloads) the
fastembed bge-small-en-v1.5 ONNX model (~130 MB).
"""
import time as _time
from arborist.search.vec import embed_documents, vec_backend_version
quant = getattr(args, "quant", "float32")
rebuild = getattr(args, "rebuild", False)
conn = connect(args.db)
t0 = _time.monotonic()
last_print = [0.0]
def _progress(done: int, total: int) -> None:
now = _time.monotonic()
if now - last_print[0] >= 1.0 or done == total:
pct = (100.0 * done / total) if total else 100.0
rate = done / max(now - t0, 1e-6)
print(
f" embedded {done}/{total} chunks ({pct:.1f}%, {rate:.0f}/s)",
file=sys.stderr,
)
last_print[0] = now
try:
n = embed_documents(
conn,
limit=args.limit,
batch_size=args.batch_size,
incremental=not rebuild,
rebuild=rebuild,
quant=quant,
progress=_progress,
)
except ValueError as e:
print(f"embed error: {e}", file=sys.stderr)
return 2
finally:
conn.close()
elapsed = _time.monotonic() - t0
print(json.dumps({
"db": str(args.db),
"backend_version": vec_backend_version(quant),
"quant": quant,
"mode": "rebuild" if rebuild else "incremental",
"chunks_embedded": n,
"elapsed_s": round(elapsed, 2),
"rate_per_s": round(n / max(elapsed, 1e-6), 1),
}, indent=2))
return 0
def _cmd_verify(args: argparse.Namespace) -> int:
"""Round-trip Merkle proofs on N random documents (chunk 0 each)."""
conn = (
connect_query(args.db, shards_dir=args.global_shards_dir)
if args.global_shards_dir
else connect(args.db)
)
try:
result = verify_random_sample(conn, n=args.n)
finally:
conn.close()
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0 if result["failed"] == 0 else 1
def _cmd_distill(args: argparse.Namespace) -> int:
"""Distill existing documents into cores (surface→core, or core→core+1)."""
from arborist.distill import get_distiller
from arborist.distill.runner import distill_existing
from arborist.store import discover_shards
try:
distiller = get_distiller(args.process)
except ValueError as e:
print(str(e), file=sys.stderr)
return 2
# Sharded mode: iterate over each shard's DB and distill in place.
# Cores stay in their source shard so the per-shard audit/derivation
# chains remain self-contained.
if args.global_shards_dir:
shard_paths = discover_shards(args.global_shards_dir)
if not shard_paths:
print(f"no shards in {args.global_shards_dir}", file=sys.stderr)
return 2
per_shard: list[dict] = []
totals = {
"scanned": 0,
"distilled": 0,
"skipped_existing": 0,
"skipped_cold": 0,
"skipped_empty": 0,
}
for sp in shard_paths:
conn = connect(sp)
try:
r = distill_existing(
conn,
distiller,
kind=args.kind,
source_type=args.source_type,
limit=args.limit,
chunker_name=args.chunker,
batch_size=args.batch_size,
)
finally:
conn.close()
per_shard.append({"shard": sp.name, **r})
for k in totals:
totals[k] += r[k]
print(json.dumps({**totals, "shards": per_shard}, indent=2, ensure_ascii=False))
return 0
conn = connect(args.db)
try:
result = distill_existing(
conn,
distiller,
kind=args.kind,
source_type=args.source_type,
limit=args.limit,
chunker_name=args.chunker,
batch_size=args.batch_size,
)
finally:
conn.close()
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
def _cmd_ask(args: argparse.Namespace) -> int:
"""Ask a question against one document; verifier classifies the answer."""
import os
from arborist.qa import ask
from arborist.qa.client import OpenAICompatibleClient, StubClient
base_url = args.endpoint or os.environ.get(
"ARBORIST_LLM_ENDPOINT", "https://hermes.ai.unturf.com/v1"
)
model = args.model or os.environ.get(
"ARBORIST_LLM_MODEL",
"adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
)
revision = os.environ.get("ARBORIST_LLM_REVISION", "")
quantization = os.environ.get("ARBORIST_LLM_QUANTIZATION", "fp8-dynamic")
api_key = os.environ.get("ARBORIST_LLM_API_KEY")
client: object
if args.dry_run:
client = StubClient(
answer=f"[STUB] would have answered '{args.question}' against root {args.document_root[:16]}"
)
else:
client = OpenAICompatibleClient(base_url=base_url, api_key=api_key)
conn = (
connect_query(args.db, shards_dir=args.global_shards_dir)
if args.global_shards_dir
else connect(args.db)
)
# Per-call policy override for --answer-mode. Other knobs flow from
# DEFAULT_POLICY.
from arborist.qa.runner import DEFAULT_POLICY as _DEFAULT_ASK_POLICY
call_policy = dict(_DEFAULT_ASK_POLICY)
if getattr(args, "answer_mode", None):
call_policy["answer_mode"] = args.answer_mode
if getattr(args, "user_payload_layout", None):
call_policy["user_payload_layout"] = args.user_payload_layout
if getattr(args, "demote_on_missed_answer", False):
call_policy["answerability_demote_enabled"] = True
try:
result = ask(
conn,
document_root=args.document_root,
question=args.question,
client=client,
model_id=model,
revision=revision,
quantization=quantization,
policy=call_policy,
)
finally:
conn.close()
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0 if result.get("status") in ("cache_hit", "cache_miss_then_written") else 1
def _default_crawl_db() -> Path:
"""Path of the local crawl db auto-included on the read path.
A single central file for locally web-crawled content (``make
crawl-ingest``). Kept SEPARATE from the peer-shared main shards so
it is never bundled into cold-packs / mesh by default; only the read
path attaches it. Override with ``ARBORIST_CRAWL_DB``."""
import os
env = os.environ.get("ARBORIST_CRAWL_DB")
if env:
return Path(env).expanduser()
return Path.home() / ".arborist" / "crawl" / "web.db"
def _resolve_extra_shards(args: argparse.Namespace) -> list[Path]:
"""Extra shard files to search alongside the main corpus: the local
crawl db (auto-included unless ``--no-crawl-db``) plus any explicit
``--include-shard`` paths. Missing files are dropped silently — the
crawl db only exists once something has been crawled locally."""
extras: list[Path] = []
if not getattr(args, "no_crawl_db", False):
crawl_db = _default_crawl_db()
if crawl_db.is_file():
extras.append(crawl_db)
for p in (getattr(args, "include_shard", None) or []):
pp = Path(p).expanduser()
if pp.is_file() and pp not in extras:
extras.append(pp)
return extras
def _cmd_query(args: argparse.Namespace) -> int:
"""Multi-source RAG: question -> top-K corpus docs -> Hermes -> cache."""
import os
from arborist.qa.client import OpenAICompatibleClient, StubClient
from arborist.qa.progress import from_env as _progress_from_env
from arborist.qa.query import query
base_url = args.endpoint or os.environ.get(
"ARBORIST_LLM_ENDPOINT", "https://hermes.ai.unturf.com/v1"
)
model = args.model or os.environ.get(
"ARBORIST_LLM_MODEL",
"adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
)
revision = os.environ.get("ARBORIST_LLM_REVISION", "")
quantization = os.environ.get("ARBORIST_LLM_QUANTIZATION", "fp8-dynamic")
api_key = os.environ.get("ARBORIST_LLM_API_KEY")
client: object
if args.dry_run:
client = StubClient(
answer="[STUB] dry-run: would have asked Hermes-3 with the assembled context."
)
else:
client = OpenAICompatibleClient(base_url=base_url, api_key=api_key)
qa_db = args.qa_db
if qa_db is None:
if args.global_shards_dir:
qa_db = Path(args.global_shards_dir) / "qa.db"
else:
qa_db = Path.home() / ".arborist" / "qa.db"
qa_db = Path(qa_db)
shards_dir = (
Path(args.global_shards_dir) if args.global_shards_dir else None
)
single_db = None if shards_dir else args.db
extra_shards = _resolve_extra_shards(args)
# Apply per-call policy overrides (question_dedup, repair, answer_mode)
# on top of the default. fidelity is a function-level kwarg, not in
# the policy.
from arborist.qa.query import DEFAULT_QUERY_POLICY
call_policy = dict(DEFAULT_QUERY_POLICY)
if getattr(args, "question_dedup", None):
call_policy["question_dedup"] = args.question_dedup
if getattr(args, "answer_mode", None):
call_policy["answer_mode"] = args.answer_mode
if getattr(args, "user_payload_layout", None):
call_policy["user_payload_layout"] = args.user_payload_layout
# Ticket #000068 Phase 3 — opt-in missed-answer demote. Flag
# default OFF (Phase 4 NO-GO on default-on until human spot-check
# confirms low FP rate; Phase 2 bench showed 100% precision at
# n=228 but that's not enough samples to flip the default).
if getattr(args, "demote_on_missed_answer", False):
call_policy["answerability_demote_enabled"] = True
if getattr(args, "repair", False):
# Mechanical-only repair when --repair is set; --repair-reprompts
# adds the optional re-prompt tier on top. Both default off so
# `make query` stays single-shot unless a knob is flipped.
call_policy["repair_enabled"] = True
call_policy["repair_max_reprompts"] = max(
0, int(getattr(args, "repair_reprompts", 0))
)
# Ticket #000008 Phase 4 — quantifier-guard CLI overrides.
# Six-level disable hierarchy at Levels 2 (per-call CLI flag)
# via these flags; Level 3 policy fields are reachable via the
# underlying policy dict.
if getattr(args, "no_quantifier_guard", False):
call_policy["quantifier_guard_enabled"] = False
if getattr(args, "allow_broad", False):
# Keeps the classifier on (telemetry stays useful) but
# zeroes out the apply_caps gate so broad shapes don't
# get clipped during emergent search.
call_policy["quantifier_guard_apply_caps"] = False
if getattr(args, "reject_broad", False):
# Phase 4 reject-broad: the actual rejection happens inside
# query() via the policy field; this CLI flag just sets the
# field. See arborist/qa/query.py for the early-return path.
call_policy["quantifier_reject_broad"] = True
if getattr(args, "apply_quantifier_caps", False):
# Operator opts in to flipping the dry-run gate per-call.
# Bench-first per §10.11.3 — this flag is the path from
# dry-run to live-cap.
call_policy["quantifier_guard_apply_caps"] = True
if getattr(args, "crosslang_guard", False):
# Ticket #000001 §7 Phase 0 — opt in to the cross-language
# guard per-call (default OFF). Pure policy-field set; the
# behaviour lives in query() (crosslang.guard + the two seams).
call_policy["crosslang_guard_enabled"] = True
if getattr(args, "crosslang_translate", False):
# Ticket #000056 — Operation Sandwich implies the Phase-0
# guard (it rides the same non-English signal).
call_policy["crosslang_guard_enabled"] = True
call_policy["crosslang_translate_enabled"] = True
# Ticket #000010 — meta-cognition CLI overrides.
if getattr(args, "no_preflight", False):
call_policy["metacognition_enabled"] = False
if getattr(args, "block_on_contradiction", False):
# Strict mode: hard-block on lexical contradictions instead
# of label-only.
call_policy["metacognition_block_on_contradiction"] = True
if getattr(args, "soft_preflight", False):
# Ticket #000011 — opt-in to model-assisted soft preflight
# sidecar. Adds one short LLM round-trip; NEVER gates
# admissibility (D1 preserved).
call_policy["soft_preflight_enabled"] = True
# Ticket #000028 — multi-modality witness override. CLI > policy.
_witness_override = getattr(args, "witness_override", None)
if _witness_override == "on":
call_policy["canonical_witness_enabled"] = True
elif _witness_override == "off":
call_policy["canonical_witness_enabled"] = False
if getattr(args, "no_canonical_preflight", False):
# Disable the math/logic π* short-circuit per-call. Forces
# RAG even on pure-arithmetic / pure-propositional input —
# useful when bench-comparing against the LLM path.
call_policy["canonical_projection_preflight"] = False
progress = _progress_from_env(
cli_override=getattr(args, "progress_override", None),
)
result = query(
question=args.question,
qa_db=qa_db,
chat_client=client,
model_id=model,
revision=revision,
quantization=quantization,
shards_dir=shards_dir,
single_db=single_db,
extra_shards=extra_shards,
top_k=args.top_k,
over_fetch=args.over_fetch,
max_context_chars=args.max_context_chars,
policy=call_policy,
fidelity=getattr(args, "fidelity", None),
burn_existing=bool(getattr(args, "burn", False)),
retrieval_keywords=getattr(args, "retrieval_keywords", None),
progress=progress,
)
# Emit unfirehose-compatible session journal. One JSONL file per
# `make query` invocation, written to ~/.arborist/unfirehose/{slug}/
# {session_uuid}.jsonl. Unfirehose's native-harness watcher picks
# this up automatically (no registration). Failures here must NEVER
# break the query path — wrap in a broad except & swallow.
try:
_emit_query_journal(args.question, result, model)
except Exception: # pragma: no cover — best-effort journaling
pass
# Ticket #000031 Phase 3 — stash shards_dir on the result dict
# so the render-layer warrant-chain tail can look up
# warrant-resolver derivations without needing args. Stripped
# before JSON output to keep the json shape stable.
_shards_dir = args.global_shards_dir or getattr(args, "shards_dir", None)
if _shards_dir:
result["_shards_dir"] = str(_shards_dir)
if args.json:
# Don't leak the internal-only `_shards_dir` field into JSON
# output — strip it before serialize.
json_result = {k: v for k, v in result.items() if not k.startswith("_")}
print(json.dumps(json_result, indent=2, ensure_ascii=False))
else:
print(_render_query_human(result, args.question))
return (
0
if result.get("status") in (
"cache_hit",
"cache_miss_then_written",
"canonical_projection",
)
else 1
)
def _emit_query_journal(question: str, result: dict, model: str) -> None:
"""Write one unfirehose/1.0 session for this query invocation."""
from arborist.journal import SessionWriter
timings = result.get("timings") or {}
answer = result.get("answer_text") or ""
arborist_meta = {
"audit_mode": result.get("audit_mode"),
"verifier_method": result.get("verifier_method"),
"n_quotes": result.get("n_quotes"),
"n_verified": result.get("n_verified"),
"cache_key": result.get("cache_key"),
"cache_status": result.get("status"),
"lookup_path": result.get("lookup_path"),
"violations": [
{"kind": v.get("kind"), "rationale": (v.get("rationale") or "")[:160]}
for v in (result.get("violations") or [])
],
"sources": [
{"title": s.get("title"), "uri": s.get("document_uri"), "used": s.get("used"), "role": s.get("source_role")}
for s in (result.get("sources") or [])
],
"timings_ms": timings,
"answer_mode": (result.get("policy") or {}).get("answer_mode"),
}
with SessionWriter(first_prompt=question) as s:
s.user_message(question)
s.assistant_message(
answer,
model=model,
provider="hermes",
stop_reason="end_turn",
duration_ms=int(timings.get("total_ms") or 0) or None,
arborist_meta=arborist_meta,
)
# Soft-demote violation kinds that demote STRICT to HYBRID without
# rejecting the pointer outright. WARRANT_MISSING / TITLE_MISMATCH /
# DEFLECTION_DETECTED handled separately as hard demotes (their
# presence determines POINTER-LINKED vs ANCHOR-WARRANTED).
_SOFT_DEMOTE_VIOLATION_KINDS = frozenset({
"LAZY_ANCHOR_DEMOTED",
"POINTER_OVERFLOW_TRIMMED",
"TOO_MANY_CLAIMS",
"BARE_NAME_CLAIM",
# FORMAT_COLLAPSED — model abandoned the claim_lattice_pointer
# protocol (multi-line prose, zero [E\d+] tags). Soft-demotes
# STRICT → HYBRID; pairs with the bottom UNGROUNDED rung when the
# parser found nothing groundable, but at least surfaces the
# collapse cause to the operator at audit-line glance.
"FORMAT_COLLAPSED",
# Ticket #000008 Phase 4 — broad-quantifier soft-demotes (§10.3).
# Per §10.3 these stay as soft demotes (cap at ANCHOR-WARRANTED)
# rather than minting a new audit_mode token. The audit-line tail
# (rendered by _render_warrant_tail) names which one fired so an
# operator can tell at a glance.
"BROAD_QUANTIFIER_RUNAWAY", # raw_line_count >> pointer_count
"BROAD_QUANTIFIER_CAP_APPLIED", # preflight cap fired below default
"BROAD_QUANTIFIER_SCOPE_UNBOUND", # unbounded universal reached the LLM
# BROAD_QUANTIFIER_REJECTED is a HARD demote (UNGROUNDED via
# early-return) — listed here for completeness but doesn't
# belong in the soft-demote set.
})
def _ladder_rung_for_lattice(audit_mode: str, violations: list[dict] | None) -> str:
"""Map (audit_mode, violations) → four-rung ladder for claim-lattice
methods. Renderer-only transformation; schema column unchanged.
The ladder names a strictly stronger property at each rung:
POINTER-LINKED pointer/source/chunk verified;
warrant either didn't apply or failed
ANCHOR-WARRANTED pointer-linked AND cited evidence contains
required anchors (warrant ran & passed);
other soft-demote violations may be present
EVIDENCE-WARRANTED anchor-warranted AND no soft demotes
UNGROUNDED n_verified == 0 (existing audit_mode)
HYBRID adds `-PARTIAL` suffix to whichever rung applies.
The rung logic uses the violations list as the signal — no new
verifier output field needed. Three discriminators:
- WARRANT_MISSING in violations → warrant ran and failed at
least one claim → POINTER-LINKED (pointer ok but warrant
didn't anchor the claim)
- any soft-demote kind in violations → ANCHOR-WARRANTED (the
rung is reached but other demotes pulled it back from STRICT)
- no warrant miss AND no soft demotes → EVIDENCE-WARRANTED
"""
if audit_mode == "UNGROUNDED":
return "UNGROUNDED"
kinds = {v.get("kind") for v in (violations or [])}
# Hard demotes: WARRANT_MISSING, TITLE_MISMATCH, DEFLECTION_DETECTED
# all indicate the citation is structurally misaligned with the
# claim or the answer is structurally off-topic — pointer resolved
# but the cited evidence (span, source, or whole answer) doesn't
# actually support the user's question. All three drop to
# POINTER-LINKED. (DEFLECTION_DETECTED was previously a soft demote
# to ANCHOR-WARRANTED; the comeliness/fetish/investitures emergent
# case showed that "the model totally shifted topic but anchored
# the new topic" earned ANCHOR-WARRANTED unfairly. Off-topic
# belongs at the lower rung.)
if (
"WARRANT_MISSING" in kinds
or "TITLE_MISMATCH" in kinds
or "DEFLECTION_DETECTED" in kinds
):
rung = "POINTER-LINKED"
elif kinds & _SOFT_DEMOTE_VIOLATION_KINDS:
rung = "ANCHOR-WARRANTED"
else:
rung = "EVIDENCE-WARRANTED"
if audit_mode == "HYBRID":
rung = rung + "-PARTIAL"
return rung
def _render_audit_label(
audit_mode: str,
verifier_method: str,
violations: list[dict] | None = None,
*,
answerability: dict | None = None,
demote_enabled: bool = False,
) -> str:
"""Map (audit_mode, verifier_method, violations) → human-readable
display label.
Schema-level audit_mode names what the lexical verifier could
confirm; the display label names what THAT means in honesty
terms. STRICT in claim_lattice mode = "every pointer resolves
to a valid evidence object whose source_role is allowed AND
every cited span passes the citation-overlap coverage check."
That is NOT full semantic entailment. The display label spells
out the actual property so users don't read STRICT as "the
answer is correct."
Four-rung ladder for claim-lattice methods (per ticket #000005):
POINTER-LINKED pointer verified; warrant either didn't
apply or failed for some claim
ANCHOR-WARRANTED pointer-linked + warrant passed where
it ran; other soft demotes may apply
EVIDENCE-WARRANTED anchor-warranted + no soft demotes
UNGROUNDED no verified pairs
HYBRID audits get a `-PARTIAL` suffix on whichever rung applies.
Plus the verifier-method tail (`· via claim_lattice`,
`· via claim_lattice_pointer`) so the user can see WHICH
verifier path produced the verdict.
For quote / span / entity / paraphrase mode the audit_mode
labels carry less risk of overclaiming (they verify against
pinned spans, not synthesis) — keep them as-is.
`violations` defaults to None for backward compatibility with
callers that don't have access to the violation list. With
None, the ladder falls back to EVIDENCE-WARRANTED (the most
optimistic rung) — operators get the same surface as before
until callers thread violations through.
"""
# Ticket #000068 Phase 3 — missed-answer demote projection. When
# the demote flag is on AND the sidecar fired at strong/medium
# confidence, demote EVIDENCE-WARRANTED → EVIDENCE-MISSED-PARTIAL
# (lattice modes) or append a `· missed-answer` tail (other modes
# / lower rungs). Weak-confidence fires never demote — the bench
# data (2026-05-27 Phase 2) showed STRONG was the only firing
# tier on real failures; weak is reserved for the future expanded
# detection ladder. Demote-enabled is folded into verifier_policy_hash
# via _VERIFIER_POLICY_FIELDS so cache partitions cleanly on flip.
demote_triggers = (
demote_enabled
and isinstance(answerability, dict)
and bool(answerability.get("answerability_warning"))
and answerability.get("confidence_class") in ("strong", "medium")
)
is_claim_lattice = verifier_method.startswith("claim_lattice")
if is_claim_lattice:
rung = _ladder_rung_for_lattice(audit_mode, violations)
if demote_triggers:
if rung == "EVIDENCE-WARRANTED":
rung = "EVIDENCE-MISSED-PARTIAL"
else:
# Lower rungs already signal degradation — surface the
# missed-answer signal as a tail tag instead of a rung
# transition. Same convention as `· warrant missing`.
rung = f"{rung} · missed-answer"
return f"{rung} · via {verifier_method}"
if verifier_method == "canonical_projection":
# Display label for #000027 — persisted canonical answers
# (math/logic π* outputs). The line-rendering site appends
# pi_star_ref as a tail since this helper doesn't have it.
return "CANONICAL · via canonical_projection"
# Quote / span / entity / paraphrase: keep audit_mode as the
# primary token; append method for clarity.
label = f"{audit_mode} · via {verifier_method}"
if demote_triggers:
label = f"{label} · missed-answer"
return label
def _render_warrant_tail(result: dict) -> str:
"""Append a tail to the audit-line label that names the specific
warrant failure mode when one fired. Surfaces the cap reason at
the user-facing layer without overloading audit_mode.
Two failure modes today:
- WARRANT_MISSING: relation/date/etc. anchor extracted from
claim doesn't appear in any cited span (per-span check).
- TITLE_MISMATCH: cited evidence's source title shares no
content tokens with the claim (per-source check). 2026-05-02
spin-glass case: claim about spin glass cited to *Quantum
chromodynamics*.
When both fire on different claims of the same answer, surface
both tails so the operator sees the full picture."""
violations = result.get("violations") or []
kinds = {v.get("kind") for v in violations}
parts: list[str] = []
if "WARRANT_MISSING" in kinds:
parts.append("warrant missing")
# Phase 3 of #000031 — positive signal when a per-claim
# warrant_check would have failed but a Merkle-bound warrant
# chain to a primary-source surface exists (cited chunk's
# document is a claim-pack record with a warrant-resolver
# derivation row). Distinct from the source-level
# `_render_warrant_chain_tail` which counts cited SOURCES with
# chains; this counts CLAIMS that survived because of a chain.
proven_idxs = result.get("warrant_proven_claim_idxs") or []
if proven_idxs:
n = len(proven_idxs)
parts.append(f"warrant proven via chain ×{n}")
if "TITLE_MISMATCH" in kinds:
parts.append("title mismatch")
if "FORMAT_COLLAPSED" in kinds:
parts.append("format collapsed")
# Ticket #000008 Phase 4 — broad-quantifier tails (§10.3 / §10.7).
# Each names what the preflight detected so operators don't have to
# parse violation lists by hand. Cap value comes from
# `claim_cap_applied` on the result when present.
if "BROAD_QUANTIFIER_REJECTED" in kinds:
parts.append("broad rejected")
elif "BROAD_QUANTIFIER_CAP_APPLIED" in kinds:
cap = result.get("claim_cap_applied")
parts.append(f"broad cap {cap}" if cap else "broad cap")
elif "BROAD_QUANTIFIER_SCOPE_UNBOUND" in kinds:
parts.append("broad unbounded")
elif "BROAD_QUANTIFIER_RUNAWAY" in kinds:
parts.append("broad runaway")
# Ticket #000010 — meta-cognition logical-status tails. Pulled
# from result["question_state"]["logical_statuses"] when present.
# Doesn't double up with the broad-quantifier tails above (those
# come from the verifier violation list, not the preflight).
qs = result.get("question_state") or {}
statuses = set(qs.get("logical_statuses") or [])
if "false_premise_suspected" in statuses:
parts.append("false premise")
if "contradictory_question" in statuses:
parts.append("contradictory")
if "stale_risk" in statuses:
parts.append("stale risk")
if "out_of_corpus_risk" in statuses:
parts.append("out of corpus")
if "reference_frame_ambiguous" in statuses:
parts.append("frame ambiguous")
# Ticket #000011 — soft preflight sidecar hint. Renders distinctly
# from the hard tails above so an operator can tell at a glance
# that the signal is advisory. Skips SOFT_DISABLED / SOFT_PARSE_FAIL
# / SOFT_WELL_FORMED (no actionable signal).
soft = result.get("soft_preflight_hint") or {}
soft_label = soft.get("classifier_label") or ""
if soft_label and soft_label not in (
"SOFT_DISABLED", "SOFT_PARSE_FAIL", "SOFT_WELL_FORMED",
):
# Strip SOFT_ prefix + lowercase for tail readability
# (e.g. SOFT_FALSE_PREMISE_SUSPECTED → "false premise suspected").
readable = soft_label.removeprefix("SOFT_").lower().replace("_", " ")
parts.append(f"soft: {readable}")
# Ticket #000026 Phase 3 — authorship warrant tail. Shows operator
# the strongest tier the cited evidence supports for an
# authorship-shaped question. Strong tiers (1-4) advertise the
# win; weak tiers (5-6) flag that the warrant rests on shaky
# evidence. NO_AUTHORSHIP_SIGNAL stays silent (sidecar's
# "doesn't apply" verdict).
authorship = result.get("authorship") or {}
auth_tier = authorship.get("tier")
if auth_tier and auth_tier != "NO_AUTHORSHIP_SIGNAL":
# Strip AUTHOR_ prefix + lowercase for tail readability
# (AUTHOR_COPYRIGHT_FOOTER → "copyright-footer").
readable = auth_tier.removeprefix("AUTHOR_").lower().replace("_", "-")
parts.append(f"warrant: {readable}")
if not parts:
return ""
return " · " + " · ".join(parts)
def _maybe_render_json_envelope_as_bullets(answer: str) -> str:
"""If `answer` is a claim_lattice JSON envelope, render bullets.
JSON-mode runs that land UNGROUNDED have no verified claims so the
runtime's bullet renderer produces empty text and `answer_text`
falls back to the raw model output — a `{"claims":[...]}` envelope.
The user then sees raw JSON for failed runs and bullets for
successful ones, which reads as inconsistent. Detect the JSON
shape, parse it (lenient), and render each claim's `text` as a
bullet line tagged with its evidence_ids so the surface stays
consistent across grounded / ungrounded outcomes.
Falls back to the raw input unchanged if:
- input doesn't look like JSON (no leading `{`)
- parse fails (lenient parser exception)
- parse succeeds but the shape isn't `{"claims": [...]}`
"""
stripped = (answer or "").lstrip()
if not stripped.startswith("{") and not stripped.startswith("```"):
return answer
if "claims" not in stripped:
return answer
try:
from arborist.qa.verify import _lenient_json_parse
parsed, _fixups = _lenient_json_parse(answer)
except Exception:
return answer
if not isinstance(parsed, dict):
return answer
raw_claims = parsed.get("claims")
if not isinstance(raw_claims, list) or not raw_claims:
return answer
out_lines: list[str] = []
for c in raw_claims:
if not isinstance(c, dict):
continue
text = c.get("text") or ""
if not isinstance(text, str) or not text.strip():
continue
eids = c.get("evidence_ids") or []
if isinstance(eids, list) and eids:
ids = ",".join(str(x) for x in eids if isinstance(x, str))
out_lines.append(f"- {text.strip()} [{ids}: unverified]")
else:
out_lines.append(f"- {text.strip()}")
return "\n".join(out_lines) if out_lines else answer
def _render_query_human(result: dict, question: str) -> str:
"""Pretty-print a query result for terminal reading.
Layout:
question
AUDIT_MODE N/M verified via verifier_method Xs (cached|fresh)
answer text...
sources (K):
[1] Title — host/path (shard.db)
[2] ...
unverified (J):
- "..."
cache_key: 35ab7d33… <run with --json for full record>
Errors / no-source paths fall back to a short status line.
"""
status = result.get("status")
if status == "broad_quantifier_rejected":
# Phase 4 reject-broad early-return path. The result carries
# an answer_text with the rejection rationale + a violations
# list; render both so the operator sees WHY without --json.
answer_text = result.get("answer_text") or ""
violations = result.get("violations") or []
kind = next(
(v.get("kind") for v in violations
if v.get("kind") == "BROAD_QUANTIFIER_REJECTED"),
"BROAD_QUANTIFIER_REJECTED",
)
intensity = result.get("quantifier_intensity") or "?"
token = result.get("quantifier_matched_token") or "?"
cap = result.get("claim_cap_applied")
cap_str = f" · cap was {cap}" if cap else ""
return (
f"{question}\n"
f" UNGROUNDED · via {kind} · {intensity} (\"{token}\")"
f"{cap_str} 0/0 0.0s (preflight)\n\n"
f"{answer_text}"
)
if status == "canonical_projection":
# Math/logic π* short-circuited the RAG path — the canonical
# bytes ARE the answer. No retrieval, no LLM, no cache.
pi_star_ref = result.get("pi_star_ref", "?")
answer_text = result.get("answer_text") or ""
timings = result.get("timings") or {}
total_ms = timings.get("total_ms")
elapsed = (
f"{total_ms / 1000:.1f}s"
if isinstance(total_ms, (int, float))
else "?"
)
# Ticket #000028 — render-layer witness tail. When witness ran,
# surface the agreement label + per-modality status. Pure
# render-only; cache_key / governance_policy_hash unaffected.
witness = result.get("witness")
witness_tail = ""
witness_block = ""
if witness:
label = witness.get("agreement_label", "?")
mods = witness.get("modalities") or {}
ground_truth_hex = witness.get("canonical_answer_bytes_hex") or ""
agree_count = sum(
1 for name, m in mods.items()
if name != "kernel"
and m.get("ok")
and (m.get("canonical_bytes_hex") or "") == ground_truth_hex
)
checkable = sum(
1 for name, m in mods.items()
if name != "kernel" and m.get("error") != "ABSENT"
)
witness_tail = f" [{label} · {agree_count}/{checkable} modalities agree]"
lines = ["", "witness:"]
for name in ("kernel", "cache", "llm"):
m = mods.get(name)
if not m:
continue
raw = m.get("raw_answer")
err = m.get("error")
ms = int(m.get("elapsed_ms") or 0)
if err == "ABSENT":
detail = "absent"
elif err == "PIS_REJECT":
detail = f'rejected: "{raw}"'
elif err == "TIMEOUT":
detail = "timeout"
elif err and err.startswith("LLM_ERROR"):
detail = err.lower()
else:
detail = f'"{raw}"'
lines.append(f" {name:<7} {detail} ({ms}ms)")
witness_block = "\n".join(lines)
return (
f"{question}\n"
f" CANONICAL · via {pi_star_ref}{witness_tail} {elapsed} (projected)\n\n"
f"{answer_text}"
f"{witness_block}"
)
if status not in ("cache_hit", "cache_miss_then_written"):
msg = result.get("msg") or status or "unknown error"
return f" {status or 'error'}: {msg}"
audit = result.get("audit_mode", "?")
n_quotes = result.get("n_quotes", 0) or 0
n_verified = result.get("n_verified", 0) or 0
method = result.get("verifier_method", "?")
timings = result.get("timings") or {}
total_ms = timings.get("total_ms")
elapsed = f"{total_ms / 1000:.1f}s" if isinstance(total_ms, (int, float)) else "?"
cache_status = "cached" if status == "cache_hit" else "fresh"
lookup_path = result.get("lookup_path")
# Annotate cache_hits when they came from a fallback ckey, not the
# primary one — useful when an agent ran with fidelity=equivalence_class
# and reused another agent's record.
if lookup_path and lookup_path.endswith("_fallback"):
cache_status = f"cached via {lookup_path}"
# Render-layer label honesty: schema audit_mode (STRICT/HYBRID/
# UNGROUNDED) describes what the lexical verifier checked, not
# full semantic entailment. The display label combines audit_mode
# with verifier_method so the user sees what was actually
# verified. Four-rung ladder for claim-lattice methods (#000005):
# POINTER-LINKED pointer verified; warrant didn't apply
# or failed for some claim
# ANCHOR-WARRANTED pointer-linked + warrant passed where
# it ran; other soft demotes may apply
# EVIDENCE-WARRANTED anchor-warranted + no soft demotes
# UNGROUNDED no verified pairs
# Schema column stays unchanged; pure display.
display_label = _render_audit_label(
audit, method, result.get("violations"),
answerability=result.get("answerability"),
demote_enabled=bool(result.get("answerability_demote_enabled", False)),
)
warrant_tail = _render_warrant_tail(result)
# Ticket #000031 Phase 3 — when a cited source has a warrant-
# resolver derivation row tying it to a primary-source surface
# chunk, surface a `· warrant: N proven` tail so the user sees
# the chain-of-custody. Pure render layer; cache_key /
# audit_mode / governance_policy_hash all unchanged. The
# _shards_dir is stashed on the result dict by `_cmd_query`
# before render time (the CLI has it; the renderer doesn't).
warrant_chain_tail = _render_warrant_chain_tail(result)
lines: list[str] = []
lines.append(question)
lines.append(
f" {display_label}{warrant_tail}{warrant_chain_tail} {n_verified}/{n_quotes} "
f"{elapsed} ({cache_status})"
)
lines.append("")
answer = result.get("answer_text") or ""
# When JSON-mode runs land UNGROUNDED, rendered_text is empty and
# answer_text falls back to the raw model output — a JSON envelope.
# Parse it and render each claim as a bullet so the user gets the
# same shape whether the run grounded or not. Falls back to raw
# display if parse fails or output isn't JSON-shaped.
answer = _maybe_render_json_envelope_as_bullets(answer)
lines.append(answer)
lines.append("")
sources = result.get("sources") or []
if sources:
lines.append(f"sources ({len(sources)}):")
for i, s in enumerate(sources, start=1):
uri = s.get("document_uri", "")
title = (s.get("title") or "").strip() or _short_path(uri)
shard = s.get("shard")
shard_part = f" ({shard})" if shard else ""
# Render-layer source-role display + used/unused annotation
# (claim_lattice modes only — quote-mode results don't carry
# the per-source `used` flag). Honestly surfaces "the system
# retrieved noise but did not rely on it" so the user can
# see the model ignoring distractors instead of having to
# infer it. Pre-2026-05-01 the source list showed every
# retrieved doc indistinguishably.
role = s.get("source_role")
used = s.get("used")
pointer_ids = s.get("used_pointer_ids") or []
annotations: list[str] = []
if role:
annotations.append(role)
if used is True:
if pointer_ids:
annotations.append(f"used ({','.join(pointer_ids)})")
else:
annotations.append("used")
elif used is False:
annotations.append("unused")
if annotations:
annotation_part = "" + "".join(annotations)
else:
annotation_part = ""
lines.append(
f" [{i}] {title}{annotation_part}{_strip_scheme(uri)}{shard_part}"
)
lines.append("")
# Retrieval-purity one-line summary (claim_lattice modes only).
# "primary at #R · used N/M sources · M-N noise unused"
# Surfaces noise-resistance at a glance without making the
# user count rows themselves.
purity = result.get("retrieval_purity")
if purity:
primary_rank = purity.get("primary_rank") or 0
used = purity.get("used_sources", 0)
total = purity.get("total_sources", 0)
noise_unused = (
purity.get("noise_sources_count", 0)
- purity.get("noise_sources_used", 0)
)
primary_part = (
f"primary at #{primary_rank}"
if primary_rank > 0 else "no primary in top-K"
)
noise_part = (
f" · {noise_unused} noise unused" if noise_unused else ""
)
lines.append(
f" retrieval purity: {primary_part} · used "
f"{used}/{total} sources{noise_part}"
)
lines.append("")
partially = result.get("partially_verified_quotes") or []
if partially:
lines.append(f"partially grounded ({len(partially)}):")
for q in partially:
qtxt = q if len(q) <= 100 else q[:97] + "..."
lines.append(f' - "{qtxt}"')
lines.append("")
unverified = result.get("unverified_quotes") or []
if unverified:
lines.append(f"unverified ({len(unverified)}):")
for q in unverified:
qtxt = q if len(q) <= 100 else q[:97] + "..."
lines.append(f' - "{qtxt}"')
lines.append("")
# Anchor-smell sidecar (claim_lattice mode only). Soft signal —
# never persisted, never in cache_key. Surfaces when ≥50% of
# verified claim-pointer pairs share one pointer_id AND there
# are at least 3 verified pairs to compare; below 3 the ratio is
# vacuous (1/1 always = 1.00 even when nothing is wrong).
ratio = result.get("lazy_anchor_ratio")
distribution = result.get("pointer_id_distribution") or {}
total_pairs = sum(distribution.values()) if distribution else 0
if (
method == "claim_lattice"
and isinstance(ratio, (int, float))
and ratio >= 0.5
and total_pairs >= 3
):
top_pid, top_count = max(distribution.items(), key=lambda kv: kv[1])
lines.append(
f"lazy-anchor smell: {top_count} of {total_pairs} verified "
f"pairs cite [{top_pid}] (ratio {ratio:.2f}); "
f"distinct pointers cited: {len(distribution)}"
)
lines.append("")
pc = result.get("prompt_chars") or {}
if pc:
# Compact one-liner — operator at a glance: did STRICT come
# from a tight prompt or a context-stuffed one?
lines.append(
f"capacity: prompt {pc.get('messages_total', 0):,} chars "
f"(sys {pc.get('system_prompt', 0):,} + "
f"reminder {pc.get('grounding_reminder', 0):,} + "
f"evidence {pc.get('evidence_or_context', 0):,} + "
f"question {pc.get('user_question', 0):,}) → "
f"answer {result.get('answer_chars', 0):,} chars"
)
# Per-phase timings — surfaces where the per-call cost lands.
# Hermes-bound queries should show `llm_ms` dominating; if
# search_ms or persist_ms creeps up that's a retrieval / WAL
# signal the operator wants visible. Cache-hit rows skip llm
# entirely so the breakdown also tells you which path you're
# paying for.
timings = result.get("timings") or {}
if timings:
parts: list[str] = []
order = (
("cache_lookup_ms", "cache"),
("search_ms", "search"),
("context_ms", "context"),
("llm_ms", "llm"),
("persist_ms", "persist"),
)
for key, label in order:
v = timings.get(key)
if isinstance(v, (int, float)) and v > 0:
parts.append(f"{label} {v / 1000:.2f}s")
total = timings.get("total_ms")
if isinstance(total, (int, float)):
parts.append(f"**total {total / 1000:.2f}s**")
if parts:
lines.append("timings: " + " · ".join(parts))
# Reference-frame notes (Ticket #000002). When detect_frame
# classified the query as `reference`, surface the named work
# so an operator knows the substrate routed to a fictional
# source. Skipped for literal / no-phrase-route / ambiguous
# rows — the line only appears when there's something to say.
fd = result.get("frame_detection") or {}
if fd.get("kind") == "reference" and fd.get("reference_title"):
lines.append("")
lines.append(f"reference frame: {fd['reference_title']}")
if fd.get("reference_uri"):
lines.append(f" cited as the named work in the answer")
cache_key = (result.get("cache_key") or "")[:8]
lines.append(f"cache_key: {cache_key}… <run with --json for full record>")
return "\n".join(lines)
def _strip_scheme(uri: str) -> str:
"""`https://en.wikipedia.org/wiki/X` -> `en.wikipedia.org/wiki/X`."""
for prefix in ("https://", "http://"):
if uri.startswith(prefix):
return uri[len(prefix):]
return uri
def _short_path(uri: str) -> str:
"""Last URL segment as a fallback display name."""
s = _strip_scheme(uri).rstrip("/")
if "/" in s:
return s.rsplit("/", 1)[1]
return s
def _cmd_inspect(args: argparse.Namespace) -> int:
"""Sidecar diagnostic — pulls source chunks for a cache_key and
classifies each unverified span. Read-only; no audit events, no
providence_cache mutations.
"""
from arborist.qa.inspect import inspect_cache_key
qa_db = args.qa_db
if qa_db is None:
qa_db = (
Path(args.global_shards_dir) / "qa.db"
if args.global_shards_dir
else Path.home() / ".arborist" / "qa.db"
)
shards_dir = (
Path(args.global_shards_dir) if args.global_shards_dir else None
)
single_db = None if shards_dir else Path(args.db) if args.db else None
result = inspect_cache_key(
args.cache_key,
qa_db=Path(qa_db),
shards_dir=shards_dir,
single_db=single_db,
)
if args.json:
print(json.dumps(result, indent=2, ensure_ascii=False))
else:
print(_render_inspect_human(result))
return 0 if result.get("status") == "ok" else 1
def _render_inspect_human(result: dict) -> str:
"""Pretty-print an inspect result so an operator can scan
paraphrase vs invention vs trailing-artifact at a glance."""
if result.get("status") != "ok":
return f" {result.get('status', 'error')}: cache_key={result.get('cache_key', '?')}"
rec = result["record"]
ctx = result["context"]
sources = result["sources"]
diagnoses = result["unverified"]
lines: list[str] = []
lines.append(rec["question_text"])
lines.append(
f" {rec['audit_mode']} {rec['n_verified']}/{rec['n_quotes']} verified "
f"via {rec['verifier_method']} state={rec['falsification_state']}"
)
lines.append("")
lines.append(
f"context: {ctx['raw_chars']:,} raw -> {ctx['base_chars']:,} base "
f"(wikitext-strip {'on' if ctx['wikitext_strip_active'] else 'off'})"
)
if sources:
lines.append(f"sources ({len(sources)}):")
for i, s in enumerate(sources, start=1):
lines.append(
f" [{i}] {s.get('title') or '(untitled)'}"
f"{s.get('chunk_count', '?')} chunks, "
f"{s.get('raw_chars', 0):,} chars"
)
lines.append("")
coh = result.get("coherence") or {}
if coh.get("kind") not in (None, "ok", "empty"):
ev = coh.get("evidence") or {}
if coh["kind"] == "phrase_component_reuse":
detail = f" (reuses {ev.get('reused_tokens')} from a quoted phrase)"
else: # circular | vacuous
detail = f" (subject={ev.get('subject_tokens')})"
lines.append(f" · incoherent: {coh['kind']}{detail}")
lines.append(f" sentence: {_short(ev.get('sentence', ''), 140)}")
lines.append("")
if not diagnoses:
lines.append("(no unverified spans)")
return "\n".join(lines)
lines.append(f"unverified diagnoses ({len(diagnoses)}):")
for i, d in enumerate(diagnoses, start=1):
span = d.get("span", "")
diag = d.get("diagnosis", "?")
lines.append("")
lines.append(f" [{i}] {diag}")
lines.append(f" span: {_short(span, 140)}")
if diag == "trailing_artifact":
lines.append(f" matched_prefix_chars: {d.get('matched_prefix_chars')}")
lines.append(f" trailing_artifact: {_short(d.get('trailing_artifact', ''), 100)}")
elif diag == "synthetic_elision_inside_quote":
lines.append(
f" [...] inserted by model — "
f"{d.get('prefix_chars', 0)} prefix chars "
f"({'in source' if d.get('prefix_in_source') else 'NOT in source'}), "
f"{d.get('suffix_chars', 0)} suffix chars "
f"({'in source' if d.get('suffix_in_source') else 'NOT in source'})"
)
elif diag == "interior_elision":
lines.append(
f" matched: {d.get('matched_prefix_chars')} prefix + "
f"{d.get('matched_suffix_chars')} suffix chars (parenthetical aside dropped)"
)
lines.append(f" dropped_aside: {_short(d.get('dropped_aside', ''), 120)}")
elif diag in ("paraphrase", "partial_paraphrase"):
lines.append(f" token_coverage: {d.get('token_coverage')}")
counts = d.get("token_counts", {})
if counts:
top = ", ".join(f"{k}×{v}" for k, v in list(counts.items())[:6])
lines.append(f" tokens_in_base: {top}")
missing = d.get("missing_tokens", [])
if missing:
lines.append(f" missing_tokens: {missing[:8]}")
elif diag == "no_overlap":
lines.append(f" tokens_checked: {d.get('tokens_checked', '?')}")
lines.append(f" tokens_present: {d.get('tokens_present', '?')}")
repair = d.get("repair")
if repair:
action = repair.get("action", "?")
reason = repair.get("reason", "")
line = f" repair: {action}"
if reason:
line += f" ({reason})"
lines.append(line)
return "\n".join(lines)
def _short(s: str, n: int) -> str:
"""Truncate string to n chars with ellipsis."""
return s if len(s) <= n else s[: n - 3] + "..."
def _cmd_losses(args: argparse.Namespace) -> int:
"""Sidecar diagnostic: list adapter_loss_reports rows.
Read-only. Filters: --document-root, --chunk-id, --kind, --stage.
With --summary, aggregates per (stage, loss_kind) for a single
document. See ticket #000022 §3.6.
"""
conn = (
connect_query(args.db, shards_dir=args.global_shards_dir)
if args.global_shards_dir
else connect(args.db)
)
try:
where: list[str] = []
params: list = []
if args.document_root:
where.append("document_root = ?")
params.append(args.document_root)
if args.chunk_id is not None:
where.append("chunk_id = ?")
params.append(args.chunk_id)
if args.kind:
where.append("loss_kind = ?")
params.append(args.kind)
if args.stage:
where.append("stage = ?")
params.append(args.stage)
clause = (" WHERE " + " AND ".join(where)) if where else ""
if args.summary:
sql = (
"SELECT stage, loss_kind, loss_mode, "
" SUM(bytes_dropped) AS total_bytes, "
" SUM(occurrence_count) AS total_occ, "
" COUNT(*) AS row_count "
"FROM adapter_loss_reports"
+ clause +
" GROUP BY stage, loss_kind, loss_mode "
"ORDER BY stage, total_bytes DESC"
)
rows = conn.execute(sql, params).fetchall()
if args.json:
out = [dict(r) for r in rows]
print(json.dumps(out, indent=2, ensure_ascii=False))
else:
if not rows:
print("(no loss rows match)")
return 0
print(f"{'stage':<18} {'kind':<28} {'mode':<11} {'occ':>7} {'bytes':>10} {'rows':>6}")
for r in rows:
print(
f"{r['stage']:<18} {r['loss_kind']:<28} "
f"{r['loss_mode']:<11} {r['total_occ']:>7,} "
f"{r['total_bytes']:>10,} {r['row_count']:>6,}"
)
return 0
sql = (
"SELECT chunk_id, document_root, stage, canonicalization_version, "
" loss_kind, loss_mode, bytes_dropped, occurrence_count, "
" input_length_bytes, output_length_bytes, sample_excerpt, "
" sample_hash, adapter_name, adapter_version, "
" loss_report_policy_hash, created_at "
"FROM adapter_loss_reports"
+ clause +
" ORDER BY chunk_id, stage, loss_kind LIMIT ?"
)
params.append(args.limit)
rows = conn.execute(sql, params).fetchall()
if args.json:
out = [dict(r) for r in rows]
print(json.dumps(out, indent=2, ensure_ascii=False))
return 0
if not rows:
print("(no loss rows match)")
return 0
for r in rows:
print(
f"chunk={r['chunk_id']} doc={r['document_root'][:12]}.. "
f"stage={r['stage']} kind={r['loss_kind']} "
f"mode={r['loss_mode']} occ={r['occurrence_count']} "
f"bytes={r['bytes_dropped']}"
)
if r["sample_excerpt"]:
print(f" excerpt: {_short(r['sample_excerpt'], 120)!r}")
elif r["sample_hash"]:
print(f" sample_hash: {r['sample_hash'][:16]}..")
return 0
finally:
conn.close()
def _falsify_cache_key(
cache_key_value: str,
*,
state: str,
reason: str,
by_actor: str,
shards_dir: Path | None,
db_path: Path | None,
) -> dict:
"""Mark a providence_cache record as failed/stale/quarantined across shards.
Searches every shard for the cache_key (it lives in exactly one).
Updates the row's falsification_state, appends a falsification log
entry, and writes a 'falsify' audit event so the chain records the act.
"""
import time as _time
from arborist.store import append_audit, discover_shards, transaction
if state not in ("failed", "stale", "quarantined"):
return {"status": "invalid_state", "value": state}
paths: list[Path] = (
discover_shards(shards_dir) if shards_dir else [Path(db_path)]
)
for sp in paths:
c = connect(sp)
try:
row = c.execute(
"SELECT cache_key, falsification_state FROM providence_cache "
"WHERE cache_key = ?",
(cache_key_value,),
).fetchone()
if row is None:
continue
now = int(_time.time())
with transaction(c):
event_hash = append_audit(
c,
event_type="falsify",
subject_root=cache_key_value,
body={
"cache_key": cache_key_value,
"from_state": row["falsification_state"],
"to_state": state,
"reason": reason,
"by_actor": by_actor,
},
ts=now,
)
c.execute(
"UPDATE providence_cache "
"SET falsification_state = ?, audit_event_hash = ? "
"WHERE cache_key = ?",
(state, event_hash, cache_key_value),
)
c.execute(
"INSERT INTO falsifications "
"(cache_key, state, reason, by_actor, at, audit_event_hash) "
"VALUES (?, ?, ?, ?, ?, ?)",
(cache_key_value, state, reason, by_actor, now, event_hash),
)
return {
"status": "falsified",
"cache_key": cache_key_value,
"shard": sp.name,
"from_state": row["falsification_state"],
"to_state": state,
"reason": reason,
"by_actor": by_actor,
"audit_event_hash": event_hash,
"ts": now,
}
finally:
c.close()
return {"status": "not_found", "cache_key": cache_key_value}
def _burn_cache_key(
cache_key_value: str,
*,
reason: str,
by_actor: str,
shards_dir: Path | None,
db_path: Path | None,
force: bool = False,
) -> dict:
"""Delete a providence_cache leaf, but only if it has no children.
"Kindergarten of a tree's genesis" — early/scratch use. Falsify keeps
history; burn removes the row. Children today = falsifications
referencing this cache_key. If any exist, refuse without ``--force``.
Always writes a 'providence_burn' audit event so the chain records
that a leaf was removed and why. Use ``arborist providence --falsify``
instead when downstream consumers may have built on this answer.
"""
import time as _time
from arborist.store import append_audit, discover_shards, transaction
paths: list[Path] = (
discover_shards(shards_dir) if shards_dir else [Path(db_path)]
)
for sp in paths:
c = connect(sp)
try:
row = c.execute(
"SELECT cache_key, audit_mode, n_verified, falsification_state, "
" question_text FROM providence_cache WHERE cache_key = ?",
(cache_key_value,),
).fetchone()
if row is None:
continue
child_falsifications = c.execute(
"SELECT COUNT(*) FROM falsifications WHERE cache_key = ?",
(cache_key_value,),
).fetchone()[0]
if child_falsifications > 0 and not force:
return {
"status": "refused_has_children",
"cache_key": cache_key_value,
"shard": sp.name,
"child_falsifications": int(child_falsifications),
"hint": "use --force to burn anyway, or 'providence --falsify' to keep history",
}
now = int(_time.time())
# DELETE + burn audit event are one atomic unit — never a burned
# cache row without its audit event, never the reverse.
with transaction(c):
c.execute(
"DELETE FROM providence_cache WHERE cache_key = ?",
(cache_key_value,),
)
event_hash = append_audit(
c,
event_type="providence_burn",
subject_root=cache_key_value,
body={
"cache_key": cache_key_value,
"burned_audit_mode": row["audit_mode"],
"burned_n_verified": int(row["n_verified"]),
"burned_state": row["falsification_state"],
"question_text": row["question_text"],
"reason": reason,
"by_actor": by_actor,
"child_falsifications_at_burn": int(child_falsifications),
"forced": bool(child_falsifications > 0 and force),
},
ts=now,
)
return {
"status": "burned",
"cache_key": cache_key_value,
"shard": sp.name,
"burned_audit_mode": row["audit_mode"],
"reason": reason,
"by_actor": by_actor,
"audit_event_hash": event_hash,
"ts": now,
}
finally:
c.close()
return {"status": "not_found", "cache_key": cache_key_value}
def _count_document_children(c, document_root: str) -> dict:
"""Count outbound child references that 'burn' must protect.
For a document/core leaf, "children" = anything downstream that built on
this row. Specifically:
- derivations rows where ``src_root = root`` (a core was distilled
from this — burning would orphan or silently cascade-truncate the
derivation, leaving the descendant core dangling).
- edges rows where ``dst_root = root`` (other documents link to this
one; burning leaves dangling references).
- providence_cache rows where ``source_root = root`` (Q&A grounded
in this document).
NOTE on schema: derivations has ON DELETE CASCADE on BOTH ``core_root``
AND ``src_root``. Without this gate, a bare DELETE FROM documents would
silently cascade-prune derivations and orphan downstream cores.
"""
derivations_downstream = c.execute(
"SELECT COUNT(*) FROM derivations WHERE src_root = ?",
(document_root,),
).fetchone()[0]
incoming_edges = c.execute(
"SELECT COUNT(*) FROM edges WHERE dst_root = ?",
(document_root,),
).fetchone()[0]
providence_refs = c.execute(
"SELECT COUNT(*) FROM providence_cache WHERE source_root = ?",
(document_root,),
).fetchone()[0]
return {
"derivations_downstream": int(derivations_downstream),
"incoming_edges": int(incoming_edges),
"providence_refs": int(providence_refs),
}
def _burn_document_root(
document_root_value: str,
*,
reason: str,
by_actor: str,
shards_dir: Path | None,
db_path: Path | None,
force: bool = False,
) -> dict:
"""Delete a surface document leaf, but only if it has no children.
Children:
- derivations.src_root = root (downstream cores derived from it)
- edges.dst_root = root (other docs link to it)
- providence_cache.source_root = root (Q&A grounded in it)
On burn:
- DELETE FROM chunks_fts (FTS5 has no FK; clear before chunks vanish).
- DELETE FROM documents — cascades to chunks + merkle_nodes via FK.
- Append a 'document_burn' audit event recording counts + forced flag.
Refuses with status='refused_has_children' (and skips the audit event)
when any child count > 0 and ``--force`` is not set, so callers can fix
state and retry idempotently.
"""
import time as _time
from arborist.store import append_audit, discover_shards, transaction
paths: list[Path] = (
discover_shards(shards_dir) if shards_dir else [Path(db_path)]
)
for sp in paths:
c = connect(sp)
try:
row = c.execute(
"SELECT document_root, document_uri, kind, title, source_type, "
" chunking_version, canonicalization_version, schema_version "
"FROM documents WHERE document_root = ? AND kind = 'surface'",
(document_root_value,),
).fetchone()
if row is None:
continue
counts = _count_document_children(c, document_root_value)
total_children = sum(counts.values())
if total_children > 0 and not force:
return {
"status": "refused_has_children",
"document_root": document_root_value,
"kind": "surface",
"shard": sp.name,
**counts,
"hint": "use --force to burn anyway (orphans descendants); "
"prefer evict for cold-tier compression",
}
chunk_count = c.execute(
"SELECT COUNT(*) FROM chunks WHERE document_root = ?",
(document_root_value,),
).fetchone()[0]
now = int(_time.time())
with transaction(c):
# FTS5 has no FK to chunks; clear by chunk_id before the
# CASCADE on documents wipes the rows that resolve them.
for cr in c.execute(
"SELECT chunk_id FROM chunks WHERE document_root = ?",
(document_root_value,),
).fetchall():
c.execute(
"DELETE FROM chunks_fts WHERE rowid = ?",
(cr["chunk_id"],),
)
# Outbound edges (src_root = this) carry no FK; clean them
# explicitly so we don't leave half-edges pointing from a
# ghost. Incoming edges (dst_root = this) are already gated
# above by the children check.
c.execute(
"DELETE FROM edges WHERE src_root = ?",
(document_root_value,),
)
# documents -> chunks/merkle_nodes/derivations cascade via FK.
c.execute(
"DELETE FROM documents WHERE document_root = ?",
(document_root_value,),
)
event_hash = append_audit(
c,
event_type="document_burn",
subject_root=document_root_value,
body={
"document_root": document_root_value,
"document_uri": row["document_uri"],
"kind": "surface",
"title": row["title"],
"source_type": row["source_type"],
"burned_chunk_count": int(chunk_count),
"child_counts_at_burn": counts,
"reason": reason,
"by_actor": by_actor,
"forced": bool(total_children > 0 and force),
},
ts=now,
)
return {
"status": "burned",
"document_root": document_root_value,
"kind": "surface",
"shard": sp.name,
"burned_chunk_count": int(chunk_count),
"child_counts_at_burn": counts,
"reason": reason,
"by_actor": by_actor,
"audit_event_hash": event_hash,
"ts": now,
}
finally:
c.close()
return {"status": "not_found", "document_root": document_root_value, "kind": "surface"}
def _burn_core_root(
document_root_value: str,
*,
reason: str,
by_actor: str,
shards_dir: Path | None,
db_path: Path | None,
force: bool = False,
) -> dict:
"""Delete a core document leaf, but only if it has no children.
Same children gates as ``_burn_document_root`` (derivations.src_root,
edges.dst_root, providence_cache.source_root). The "PLUS no further
derivations build cores from this core" rule from the spec is
structurally identical to derivations.src_root > 0 — a core acts as a
src_root only when something deeper distilled from it.
CLAUDE.md says "Cores never evict" — that's the eviction subsystem,
which only touches kind='surface'. Burn is operator-driven removal:
cores CAN be burned, but the children gate is enforced.
Audit event type is 'core_burn' so chain consumers can distinguish
surface vs core leaf removals at a glance.
"""
import time as _time
from arborist.store import append_audit, discover_shards, transaction
paths: list[Path] = (
discover_shards(shards_dir) if shards_dir else [Path(db_path)]
)
for sp in paths:
c = connect(sp)
try:
row = c.execute(
"SELECT document_root, document_uri, kind, title, source_type, "
" compression_depth, chunking_version, "
" canonicalization_version, schema_version "
"FROM documents WHERE document_root = ? AND kind = 'core'",
(document_root_value,),
).fetchone()
if row is None:
continue
counts = _count_document_children(c, document_root_value)
total_children = sum(counts.values())
if total_children > 0 and not force:
return {
"status": "refused_has_children",
"document_root": document_root_value,
"kind": "core",
"shard": sp.name,
**counts,
"hint": "use --force to burn anyway; cores carry "
"downstream derivations that will be orphaned",
}
chunk_count = c.execute(
"SELECT COUNT(*) FROM chunks WHERE document_root = ?",
(document_root_value,),
).fetchone()[0]
# Inbound derivations (where this core is core_root, i.e. its
# binding back to source surfaces). These are NOT children —
# they're the core's own provenance and cascade-delete with it.
inbound_derivations = c.execute(
"SELECT COUNT(*) FROM derivations WHERE core_root = ?",
(document_root_value,),
).fetchone()[0]
now = int(_time.time())
with transaction(c):
for cr in c.execute(
"SELECT chunk_id FROM chunks WHERE document_root = ?",
(document_root_value,),
).fetchall():
c.execute(
"DELETE FROM chunks_fts WHERE rowid = ?",
(cr["chunk_id"],),
)
c.execute(
"DELETE FROM edges WHERE src_root = ?",
(document_root_value,),
)
c.execute(
"DELETE FROM documents WHERE document_root = ?",
(document_root_value,),
)
event_hash = append_audit(
c,
event_type="core_burn",
subject_root=document_root_value,
body={
"document_root": document_root_value,
"document_uri": row["document_uri"],
"kind": "core",
"title": row["title"],
"source_type": row["source_type"],
"compression_depth": int(row["compression_depth"]),
"burned_chunk_count": int(chunk_count),
"burned_inbound_derivations": int(inbound_derivations),
"child_counts_at_burn": counts,
"reason": reason,
"by_actor": by_actor,
"forced": bool(total_children > 0 and force),
},
ts=now,
)
return {
"status": "burned",
"document_root": document_root_value,
"kind": "core",
"shard": sp.name,
"burned_chunk_count": int(chunk_count),
"burned_inbound_derivations": int(inbound_derivations),
"child_counts_at_burn": counts,
"reason": reason,
"by_actor": by_actor,
"audit_event_hash": event_hash,
"ts": now,
}
finally:
c.close()
return {"status": "not_found", "document_root": document_root_value, "kind": "core"}
def _cmd_burn_kindergarten(args: argparse.Namespace) -> int:
"""Burn all providence_cache rows younger than the kindergarten window.
Test-ergonomic mass burn: when iterating on retrieval/verifier knobs
you want to wipe recent test runs without finding each cache_key.
Mirrors the kindergarten window from `mesh sync` so what's still
"private" (un-broadcast) is also what's safe to bust without
confusing peers.
Each row goes through the standard `_burn_cache_key` so the
children gate is honored (use `--force` to override en masse).
Each successful burn writes one ``providence_burn`` audit event;
chain integrity is verifiable via `make chain-check-shards` after.
"""
import time as _time
from arborist.store import discover_shards
now = int(_time.time())
# `kindergarten_seconds <= 0` means "no time gate — burn every live
# row" per the verb's docstring. The earlier `cutoff = now - 0`
# treated 0 as a same-second-only window, which made
# test_burn_kindergarten_zero_seconds_burns_everything timing-flaky:
# if the wall-clock second rolled over between seed and burn,
# cutoff > seed.created_at and nothing matched.
kindergarten_seconds = max(0, args.kindergarten_seconds)
no_time_gate = kindergarten_seconds == 0
cutoff = 0 if no_time_gate else now - kindergarten_seconds
shards_dir = Path(args.global_shards_dir) if args.global_shards_dir else None
single_db = Path(args.db) if args.db else None
paths: list[Path] = (
discover_shards(shards_dir) if shards_dir else [single_db]
)
actor = args.by_actor or os.environ.get("USER", "unknown")
reason = args.reason or f"burn-kindergarten window={args.kindergarten_seconds}s"
examined = 0
burned = 0
refused = 0
not_found = 0
items: list[dict] = []
for sp in paths:
c = connect(sp)
try:
if no_time_gate:
rows = c.execute(
"SELECT cache_key, created_at, audit_mode "
"FROM providence_cache "
"WHERE falsification_state = 'live' "
"ORDER BY created_at DESC"
).fetchall()
else:
rows = c.execute(
"SELECT cache_key, created_at, audit_mode "
"FROM providence_cache "
"WHERE created_at >= ? AND falsification_state = 'live' "
"ORDER BY created_at DESC",
(cutoff,),
).fetchall()
finally:
c.close()
for r in rows:
examined += 1
if args.dry_run:
items.append({
"cache_key": r["cache_key"],
"audit_mode": r["audit_mode"],
"created_at": r["created_at"],
"would_burn": True,
})
continue
result = _burn_cache_key(
r["cache_key"],
reason=reason,
by_actor=actor,
shards_dir=shards_dir,
db_path=single_db,
force=bool(args.force),
)
status = result.get("status")
if status == "burned":
burned += 1
elif status == "refused_has_children":
refused += 1
else:
not_found += 1
items.append(result)
print(json.dumps({
"status": "dry_run" if args.dry_run else "burned",
"kindergarten_seconds": args.kindergarten_seconds,
"cutoff_at": cutoff,
"now": now,
"examined": examined,
"burned": burned,
"refused_has_children": refused,
"not_found": not_found,
"items": items[: args.verbose],
}, indent=2, ensure_ascii=False))
return 0
def _cmd_burn(args: argparse.Namespace) -> int:
"""CLI: burn a leaf with no children.
Dispatches on ``--kind`` to the matching helper. Default 'providence'
preserves the original surface (`--cache-key` only) so existing scripts
keep working. Document/core kinds use ``--root``.
"""
kind = getattr(args, "kind", "providence") or "providence"
shards = Path(args.global_shards_dir) if args.global_shards_dir else None
db = Path(args.db) if args.db else None
actor = args.by_actor or os.environ.get("USER", "unknown")
reason = args.reason or ""
force = bool(args.force)
if kind == "providence":
if not getattr(args, "cache_key", None):
print("burn --kind providence requires --cache-key", file=sys.stderr)
return 2
result = _burn_cache_key(
args.cache_key,
reason=reason,
by_actor=actor,
shards_dir=shards,
db_path=db,
force=force,
)
elif kind in ("document", "core"):
if not getattr(args, "root", None):
print(f"burn --kind {kind} requires --root", file=sys.stderr)
return 2
helper = _burn_document_root if kind == "document" else _burn_core_root
result = helper(
args.root,
reason=reason,
by_actor=actor,
shards_dir=shards,
db_path=db,
force=force,
)
else:
print(f"unknown burn kind: {kind}", file=sys.stderr)
return 2
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0 if result.get("status") == "burned" else 1
def _cmd_providence(args: argparse.Namespace) -> int:
"""List providence_cache records or falsify one by cache_key."""
if getattr(args, "falsify", None):
result = _falsify_cache_key(
args.falsify,
state=args.state,
reason=args.reason or "",
by_actor=args.by_actor or os.environ.get("USER", "unknown"),
shards_dir=Path(args.global_shards_dir) if args.global_shards_dir else None,
db_path=Path(args.db) if args.db else None,
)
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0 if result.get("status") == "falsified" else 1
if getattr(args, "show_preflight", None):
# Ticket #000009 §7.2 — pull the preflight stage payload
# from a row's run_dag_blob. Operator tool for inspecting
# the policy state that governed the cached row.
return _cmd_providence_show_preflight(
cache_key_prefix=args.show_preflight,
shards_dir=args.global_shards_dir,
db=args.db,
)
conn = (
connect_query(args.db, shards_dir=args.global_shards_dir)
if args.global_shards_dir
else connect(args.db)
)
try:
if args.document_uri:
rows = conn.execute(
"SELECT cache_key, question_text, answer_text, falsification_state, "
" hit_count, created_at FROM providence_cache "
"WHERE document_uri = ? ORDER BY created_at DESC",
(args.document_uri,),
).fetchall()
elif args.source_root:
rows = conn.execute(
"SELECT cache_key, question_text, answer_text, falsification_state, "
" hit_count, created_at FROM providence_cache "
"WHERE source_root = ? ORDER BY created_at DESC",
(args.source_root,),
).fetchall()
else:
rows = conn.execute(
"SELECT cache_key, question_text, answer_text, falsification_state, "
" hit_count, created_at FROM providence_cache "
"ORDER BY created_at DESC LIMIT ?",
(args.limit,),
).fetchall()
finally:
conn.close()
out = [
{
"cache_key": r["cache_key"],
"state": r["falsification_state"],
"hit_count": r["hit_count"],
"question": r["question_text"],
"answer": r["answer_text"],
"created_at": r["created_at"],
}
for r in rows
]
print(json.dumps(out, indent=2, ensure_ascii=False))
return 0
def _cmd_providence_show_preflight(
*,
cache_key_prefix: str,
shards_dir: str | None,
db: str | None,
) -> int:
"""Render the ``preflight`` stage payload for a cached row.
Ticket #000009 §7.2. Pulls ``run_dag_blob`` for the matching
cache row, parses the JSON, finds the ``preflight`` node, and
pretty-prints the five nested CTI clauses (classifier,
answer_contract, prompt_contract, evidence_contract,
policy_refs) plus the metacognition QuestionState.
Match is by 12-char prefix on ``cache_key`` (matches what
bench rows + `_render_query_human` already truncate to).
Returns 0 on success, 1 on miss / parse failure.
"""
conn = (
connect_query(db, shards_dir=shards_dir)
if shards_dir
else connect(db)
)
try:
rows = conn.execute(
"SELECT cache_key, question_text, run_dag_blob "
"FROM providence_cache WHERE cache_key LIKE ? "
"ORDER BY created_at DESC LIMIT 5",
(cache_key_prefix + "%",),
).fetchall()
finally:
conn.close()
if not rows:
print(
f" no providence_cache row matching cache_key prefix "
f"'{cache_key_prefix}'", file=sys.stderr,
)
return 1
if len(rows) > 1:
print(
f" {len(rows)} rows match prefix '{cache_key_prefix}'; "
"rendering most recent. Pass a longer prefix to disambiguate.",
file=sys.stderr,
)
row = rows[0]
blob = row["run_dag_blob"]
if not blob:
print(
f" cache_key {row['cache_key'][:12]}: no run_dag_blob "
"(legacy row, predates #000009)",
file=sys.stderr,
)
return 1
try:
parsed = json.loads(blob)
except json.JSONDecodeError as exc:
print(f" run_dag_blob parse error: {exc}", file=sys.stderr)
return 1
nodes = parsed.get("nodes") or []
preflight_node = next(
(n for n in nodes if isinstance(n, dict)
and n.get("stage") == "preflight"),
None,
)
if preflight_node is None:
print(
f" cache_key {row['cache_key'][:12]}: run_dag has no "
"preflight stage (predates #000009 binding)",
file=sys.stderr,
)
return 1
# Pull the full preflight payload (Ticket #000009 §7.2 — payload
# now persisted alongside nodes via build_run_dag's
# preflight_payload kwarg). Fall back to hash-only render for
# legacy rows whose blob predates the payload-storage commit.
payload = parsed.get("preflight_payload")
out: dict = {
"cache_key": row["cache_key"][:12],
"question": row["question_text"],
"preflight_stage_hash": preflight_node.get("hash"),
"preflight_hash_12": (preflight_node.get("hash") or "")[:12],
"run_dag_root": parsed.get("root"),
"run_dag_stages": [n.get("stage") for n in nodes],
}
if payload is not None:
# Verify the persisted payload hashes to the persisted leaf.
# Mismatch would indicate post-write tampering or a serialization
# drift; surface it explicitly so an auditor can detect.
from arborist.qa.dag import _canonical_json, _sha256_hex
recomputed = _sha256_hex(_canonical_json(payload))
out["preflight_payload"] = payload
out["payload_hash_check"] = (
"ok" if recomputed == preflight_node.get("hash")
else f"MISMATCH (recomputed {recomputed[:12]} != stored {preflight_node.get('hash', '')[:12]})"
)
else:
out["preflight_payload"] = None
out["payload_hash_check"] = (
"unavailable: legacy row predates preflight_payload "
"persistence (Ticket #000009 §7.2)"
)
print(json.dumps(out, indent=2, ensure_ascii=False))
return 0
def _cmd_controller_events(args: argparse.Namespace) -> int:
"""List ``controller_events`` rows from one shard or every shard.
Read-only inspector for the Phase 2 advisory writes
(``arborist.qa.runner._emit_qa_controller_advisory``). Surfaces
decision / difficulty / budget_allocation rows so operators can
measure the Retrigger 1 signal (#000045 §3) without raw SQL.
"""
import sqlite3 as _sqlite3
import time as _time
shards_dir = args.global_shards_dir
db_path = args.db
if shards_dir:
shard_paths = sorted(Path(shards_dir).glob("*.db"))
elif db_path:
shard_paths = [Path(db_path)]
else:
shard_paths = [Path(DEFAULT_DB_PATH)]
where: list[str] = []
params: list = []
if args.kind:
where.append("event_kind = ?")
params.append(args.kind)
if args.organism_prefix:
where.append("organism_root LIKE ?")
params.append(args.organism_prefix + "%")
if args.since_seconds is not None:
where.append("recorded_at >= ?")
params.append(int(_time.time()) - args.since_seconds)
where_sql = (" WHERE " + " AND ".join(where)) if where else ""
sql = (
"SELECT event_id, organism_root, branch_id, event_kind, label,"
" entropy, difficulty, allocation, body_blob, recorded_at"
" FROM controller_events"
+ where_sql
+ " ORDER BY recorded_at DESC, event_id DESC LIMIT ?"
)
out: list[dict] = []
summary: dict[str, int] = {}
remaining = args.limit
for sp in shard_paths:
if remaining <= 0:
break
if sp.suffix != ".db" or "-shm" in sp.name or "-wal" in sp.name:
continue
try:
conn = _sqlite3.connect(f"file:{sp}?mode=ro", uri=True)
except _sqlite3.OperationalError:
continue
conn.row_factory = _sqlite3.Row
has_table = conn.execute(
"SELECT name FROM sqlite_master "
"WHERE type='table' AND name='controller_events'"
).fetchone()
if not has_table:
conn.close()
continue
try:
rows = conn.execute(sql, [*params, remaining]).fetchall()
except _sqlite3.OperationalError:
conn.close()
continue
conn.close()
for r in rows:
kind = r["event_kind"]
summary[kind] = summary.get(kind, 0) + 1
entry = {
"shard": sp.name,
"event_id": r["event_id"],
"kind": kind,
"organism_root": r["organism_root"],
"branch_id": r["branch_id"],
"label": r["label"],
"entropy": r["entropy"],
"difficulty": r["difficulty"],
"allocation": r["allocation"],
"recorded_at": r["recorded_at"],
"recorded_at_iso": _time.strftime(
"%Y-%m-%dT%H:%M:%SZ", _time.gmtime(r["recorded_at"])
),
}
if args.body:
try:
entry["body"] = json.loads(r["body_blob"])
except (TypeError, ValueError):
entry["body"] = r["body_blob"]
out.append(entry)
remaining = args.limit - len(out)
if args.json:
print(json.dumps(
{"summary": summary, "rows": out},
indent=2, ensure_ascii=False,
))
return 0
if not out:
print("(no controller_events rows matched)")
return 0
print(f"# controller_events — {len(out)} row(s) across "
f"{len({e['shard'] for e in out})} shard(s)")
for kind, n in sorted(summary.items(), key=lambda kv: -kv[1]):
print(f"# {kind}: {n}")
print()
print(
f"{'shard':<10} {'kind':<29} {'label':<10} "
f"{'diff':>6} {'alloc':>6} {'recorded_at_iso'} organism"
)
for e in out:
org = (e["organism_root"] or "")[:48]
diff = "-" if e["difficulty"] is None else f"{e['difficulty']:.2f}"
alloc = "-" if e["allocation"] is None else f"{e['allocation']:.2f}"
print(
f"{e['shard']:<10} {e['kind']:<29} "
f"{(e['label'] or '-'):<10} {diff:>6} {alloc:>6} "
f"{e['recorded_at_iso']} {org}"
)
return 0
def _load_record_context(row, shards_dir, qa_db):
"""Reassemble context for a providence record. Returns text or None
if any source doc has no hot chunks (cold)."""
from arborist.qa.query import _load_doc_text
proof = json.loads(row["merkle_proof"])
sources = proof.get("sources", [])
if not sources:
return None
parts: list[str] = []
for src in sources:
shard_name = src.get("shard")
if not shard_name:
return None
if shards_dir:
shard_path = shards_dir / shard_name
else:
shard_path = qa_db.parent / shard_name
if not shard_path.exists():
return None
text = _load_doc_text(str(shard_path), src["document_root"])
if not text:
return None
parts.append(text)
return "\n\n".join(parts)
def _cmd_reclassify(args: argparse.Namespace) -> int:
"""Re-run the layered verifier against existing live providence records.
Reads each record's answer + reassembles its context from
merkle_proof.sources, runs verify_quotes(), and updates the row only
if the verdict differs from what's stored. No LLM calls — this just
relabels existing answers under the current verifier.
Cold-source records (where any source doc has no hot chunks) are
skipped: we can't faithfully reclassify without the original context.
Run `arborist rehydrate` first if you want those covered too.
`--compare` runs all four entity policies side-by-side without
writing — use it to see what each policy would produce on real data
before committing to one. `--entity-policy X` writes under a single
policy.
Each changed record gets one 'providence_reclassify' audit event
with old & new state for chain-of-custody.
"""
import time
from collections import defaultdict
from arborist.qa.verify import (
DEFAULT_ENTITY_POLICY,
ENTITY_POLICIES,
verify_quotes,
)
qa_db = args.qa_db
if qa_db is None:
if args.global_shards_dir:
qa_db = Path(args.global_shards_dir) / "qa.db"
else:
qa_db = Path.home() / ".arborist" / "qa.db"
qa_db = Path(qa_db)
shards_dir = (
Path(args.global_shards_dir) if args.global_shards_dir else None
)
conn = connect(qa_db)
try:
sql = (
"SELECT cache_key, answer_text, merkle_proof, audit_mode, "
" verifier_method, n_quotes, n_verified, unverified_quotes, "
" question_text "
"FROM providence_cache "
"WHERE falsification_state = 'live' "
"ORDER BY created_at DESC"
)
if args.limit:
sql += f" LIMIT {int(args.limit)}"
rows = conn.execute(sql).fetchall()
if args.compare:
# Run all four policies side-by-side, no DB write. Output is a
# per-record grid + a per-policy distribution summary so fox can
# eyeball where the policies disagree.
grid = []
distribution: dict[str, dict[str, int]] = {
p: defaultdict(int) for p in ENTITY_POLICIES
}
skipped_cold = 0
for row in rows:
context = _load_record_context(row, shards_dir, qa_db)
if context is None:
skipped_cold += 1
continue
per_policy = {}
for p in ENTITY_POLICIES:
v = verify_quotes(row["answer_text"], context, entity_policy=p)
label = f"{v['audit_mode']}/{v['verifier_method']}"
per_policy[p] = label
distribution[p][label] += 1
grid.append({
"cache_key": row["cache_key"][:16] + "",
"question": row["question_text"][:55],
**per_policy,
})
print(json.dumps({
"examined": len(grid),
"skipped_cold": skipped_cold,
"distribution": {p: dict(d) for p, d in distribution.items()},
"records": grid,
}, indent=2, ensure_ascii=False))
return 0
# Single-policy reclassify. Default tracks DEFAULT_ENTITY_POLICY
# so the CLI always matches the verifier's current contract.
policy_name = args.entity_policy or DEFAULT_ENTITY_POLICY
if policy_name not in ENTITY_POLICIES:
print(
f"--entity-policy must be one of {ENTITY_POLICIES}, "
f"got {policy_name!r}",
file=sys.stderr,
)
return 2
summary = {
"examined": 0,
"changed": 0,
"skipped_cold": 0,
"unchanged": 0,
"entity_policy": policy_name,
"transitions": defaultdict(int),
}
for row in rows:
summary["examined"] += 1
context = _load_record_context(row, shards_dir, qa_db)
if context is None:
summary["skipped_cold"] += 1
continue
verdict = verify_quotes(
row["answer_text"], context, entity_policy=policy_name
)
old_unverified = row["unverified_quotes"] or "null"
new_unverified_blob = (
json.dumps(verdict["unverified_quotes"], separators=(",", ":"))
if verdict["unverified_quotes"]
else None
)
new_unverified_for_compare = new_unverified_blob or "null"
unchanged = (
verdict["audit_mode"] == row["audit_mode"]
and verdict["verifier_method"] == row["verifier_method"]
and verdict["n_quotes"] == row["n_quotes"]
and verdict["n_verified"] == row["n_verified"]
and old_unverified == new_unverified_for_compare
)
if unchanged:
summary["unchanged"] += 1
continue
summary["changed"] += 1
transition = (
f"{row['audit_mode']}/{row['verifier_method']} "
f"-> {verdict['audit_mode']}/{verdict['verifier_method']}"
)
summary["transitions"][transition] += 1
if args.dry_run:
continue
now = int(time.time())
with transaction(conn):
event_hash = append_audit(
conn,
event_type="providence_reclassify",
subject_root=row["cache_key"],
body={
"old_audit_mode": row["audit_mode"],
"new_audit_mode": verdict["audit_mode"],
"old_method": row["verifier_method"],
"new_method": verdict["verifier_method"],
"old_n_verified": row["n_verified"],
"new_n_verified": verdict["n_verified"],
"entity_policy": policy_name,
},
ts=now,
)
conn.execute(
"UPDATE providence_cache SET "
" audit_mode = ?, n_quotes = ?, n_verified = ?, "
" unverified_quotes = ?, verifier_method = ?, "
" audit_event_hash = ? "
"WHERE cache_key = ?",
(
verdict["audit_mode"],
verdict["n_quotes"],
verdict["n_verified"],
new_unverified_blob,
verdict["verifier_method"],
event_hash,
row["cache_key"],
),
)
finally:
conn.close()
summary["transitions"] = dict(summary["transitions"])
summary["dry_run"] = bool(args.dry_run)
print(json.dumps(summary, indent=2, ensure_ascii=False))
return 0
def _cmd_emergent(args: argparse.Namespace) -> int:
"""Surface emergent claims from UNGROUNDED/HYBRID providence records.
These are spans the model produced that don't appear verbatim in the
corpus — candidate ingest targets. Frequent unverified quotes signal
knowledge the model has from training that our corpus is missing.
"""
conn = (
connect_query(args.db, shards_dir=args.global_shards_dir)
if args.global_shards_dir
else connect(args.db)
)
try:
if args.aggregate:
rows = conn.execute(
"SELECT unverified_quotes FROM providence_cache "
"WHERE audit_mode IN ('UNGROUNDED','HYBRID') "
" AND falsification_state = 'live' "
" AND unverified_quotes IS NOT NULL"
).fetchall()
counts: dict[str, int] = {}
for r in rows:
for q in json.loads(r["unverified_quotes"]):
counts[q] = counts.get(q, 0) + 1
ranked = sorted(counts.items(), key=lambda kv: -kv[1])[: args.limit]
print(json.dumps(
[{"quote": q, "count": c} for q, c in ranked],
indent=2, ensure_ascii=False
))
else:
rows = conn.execute(
"SELECT cache_key, audit_mode, verifier_method, question_text, "
" n_quotes, n_verified, unverified_quotes, created_at "
"FROM providence_cache "
"WHERE audit_mode IN ('UNGROUNDED','HYBRID') "
" AND falsification_state = 'live' "
"ORDER BY created_at DESC LIMIT ?",
(args.limit,),
).fetchall()
out = [
{
"cache_key": r["cache_key"],
"audit_mode": r["audit_mode"],
"verifier_method": r["verifier_method"],
"question": r["question_text"],
"n_quotes": r["n_quotes"],
"n_verified": r["n_verified"],
"unverified_quotes": (
json.loads(r["unverified_quotes"])
if r["unverified_quotes"]
else []
),
"created_at": r["created_at"],
}
for r in rows
]
print(json.dumps(out, indent=2, ensure_ascii=False))
finally:
conn.close()
return 0
def _cmd_evict(args: argparse.Namespace) -> int:
"""Demote surface chunks from hot to cold (NULL content). Cores never evict."""
from arborist.evict import evict_to_cold
conn = (
connect_query(args.db, shards_dir=args.global_shards_dir)
if args.global_shards_dir
else connect(args.db)
)
try:
result = evict_to_cold(
conn,
source_type=args.source_type,
older_than_days=args.older_than_days,
document_roots=args.document_root or None,
)
finally:
conn.close()
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
def _cmd_rehydrate(args: argparse.Namespace) -> int:
"""Rehydrate cold chunks from source; non-zero exit if drift detected."""
from arborist.evict import rehydrate
conn = (
connect_query(args.db, shards_dir=args.global_shards_dir)
if args.global_shards_dir
else connect(args.db)
)
try:
if args.all_cold:
roots = [
r["document_root"]
for r in conn.execute(
"SELECT DISTINCT document_root FROM chunks WHERE tier = 'cold'"
).fetchall()
]
else:
roots = list(args.document_root or [])
if not roots:
print(
"rehydrate needs --document-root R or --all-cold",
file=sys.stderr,
)
return 2
results = []
for r in roots:
res = rehydrate(conn, r)
res["document_root"] = r
results.append(res)
finally:
conn.close()
print(json.dumps(results, indent=2, ensure_ascii=False))
drift = sum(1 for r in results if r.get("status") == "drift_detected")
return 1 if drift else 0
# --- cold-object-store CLI (#000061) ---------------------------------------
#
# Backend config comes from env vars; credentials use the standard boto3
# discovery chain (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY env, or
# ~/.aws/credentials). Per Operation Voyeur (CLAUDE.md): credentials NEVER
# print to stdout, NEVER pass as CLI argv. Only endpoint URL + bucket name
# appear in logs.
#
# Example for DigitalOcean Spaces:
# export AWS_ACCESS_KEY_ID=<key>
# export AWS_SECRET_ACCESS_KEY=<secret>
# export ARBORIST_COLD_ENDPOINT_URL=https://nyc3.digitaloceanspaces.com
# export ARBORIST_COLD_BUCKET=arborist-corpus
# arborist cold push --source-type html
def _make_cold_backend():
"""Build a backend from env vars; raise with a useful message if missing.
NEVER reads or prints the credential env vars — boto3 handles those
internally. We only touch the public-facing config (endpoint + bucket).
"""
import os
endpoint = os.environ.get("ARBORIST_COLD_ENDPOINT_URL")
bucket = os.environ.get("ARBORIST_COLD_BUCKET")
region = os.environ.get("ARBORIST_COLD_REGION")
if not endpoint or not bucket:
raise SystemExit(
"cold backend requires ARBORIST_COLD_ENDPOINT_URL and "
"ARBORIST_COLD_BUCKET env vars.\n"
"Credentials come from AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY "
"or ~/.aws/credentials (boto3 standard discovery)."
)
from arborist.cold_object import S3CompatibleBackend
return S3CompatibleBackend(
endpoint_url=endpoint, bucket=bucket, region=region
)
def _cmd_corpus_reshard(args: argparse.Namespace) -> int:
"""Content-hash reshard (#000065).
Planner picks A/B/C from disk; executor runs the picked strategy.
The plan + rationale always print first so the operator can review
before any data moves. --plan-only stops after that.
"""
from arborist.migrate import (
HydrationPlanner,
InsufficientDisk,
execute_plan,
)
source_dir = Path(args.source_dir).expanduser()
target_dir = Path(args.target_dir).expanduser()
audit_ndjson = Path(args.audit_ndjson).expanduser()
try:
plan = HydrationPlanner().plan(
source_dir=source_dir,
target_dir=target_dir,
target_M=args.target_M,
force_strategy=args.force_strategy,
allow_in_place=args.allow_in_place,
)
except InsufficientDisk as e:
print(f"insufficient disk: {e}", file=sys.stderr)
return 1
except (ValueError, FileNotFoundError) as e:
print(f"plan error: {e}", file=sys.stderr)
return 2
# Always show the plan readout. Goes to stdout (not stderr) so
# operators can pipe to a log.
print(json.dumps(plan.as_audit_body(), indent=2, ensure_ascii=False))
print(
f"\nstrategy: {plan.strategy}\n"
f"peak draw est: {plan.estimated_peak_draw_bytes / 1e9:.1f} GB\n"
f"free at peak: {plan.free_at_peak_bytes() / 1e9:.1f} GB\n"
f"rationale: {plan.rationale}",
file=sys.stderr,
)
if args.plan_only:
return 0
if not audit_ndjson.exists():
print(
f"audit ndjson missing at {audit_ndjson}. "
f"Run `python bench/extract_audit_events.py` first.",
file=sys.stderr,
)
return 3
expected_row_counts: dict[str, int] = {}
if args.expected_row_counts_json:
snap_path = Path(args.expected_row_counts_json).expanduser()
if not snap_path.exists():
print(f"snapshot not found at {snap_path}", file=sys.stderr)
return 5
snap = json.loads(snap_path.read_text())
totals = snap.get("totals", {})
for table in ("documents", "chunks", "edges"):
if table in totals:
expected_row_counts[table] = int(totals[table])
try:
result = execute_plan(
plan,
audit_events_ndjson=audit_ndjson,
expected_row_counts=expected_row_counts,
dry_run=args.dry_run,
)
except NotImplementedError as e:
print(f"strategy not implemented: {e}", file=sys.stderr)
return 4
except RuntimeError as e:
print(f"validation failed: {e}", file=sys.stderr)
return 6
print(
json.dumps(
{
"strategy": result.strategy,
"elapsed_seconds": round(result.elapsed_seconds, 1),
"rows_moved_total": sum(result.rows_moved.values()),
"rows_moved": result.rows_moved,
"audit_events_consolidated": result.audit_events_consolidated,
"final_event_hash": result.final_event_hash,
"target_row_counts": result.target_row_counts,
"notes": result.notes,
},
indent=2,
ensure_ascii=False,
)
)
return 0
def _cmd_cold_pack(args: argparse.Namespace) -> int:
from arborist.evict import push_pack
# When --no-push is set, --local-dir is required (otherwise we'd build
# packs and immediately discard them).
if not args.push_to_bucket and not args.local_dir:
print(
"--no-push requires --local-dir (otherwise the packs go nowhere)",
file=sys.stderr,
)
return 2
# Backend is only contacted when we're pushing — local-dir-only runs
# work without bucket credentials.
backend = (
_make_cold_backend() if args.push_to_bucket else _LocalOnlyBackend()
)
conn = (
connect_query(args.db, shards_dir=args.global_shards_dir)
if args.global_shards_dir
else connect(args.db)
)
try:
result = push_pack(
conn,
backend,
document_root=args.document_root,
max_chunks=args.max_chunks if args.max_chunks > 0 else None,
max_pack_bytes=args.max_pack_bytes,
local_dir=args.local_dir,
push_to_bucket=args.push_to_bucket,
allow_license_class=args.allow_license_class,
include_fts=args.include_fts,
jit_blobs=getattr(args, "jit_blobs", False),
jit_blobs_workers=getattr(args, "jit_blobs_workers", 16),
)
finally:
conn.close()
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
class _LocalOnlyBackend:
"""Identity-only stub for --no-push runs.
`push_pack` still asks the backend for its identity (folds into audit
body) even when uploads are disabled. This stub answers that one
question without requiring real bucket credentials, and raises if any
actual S3 method is called — fail-loud rather than silently misroute
bytes.
"""
@property
def identity(self):
from arborist.cold_object import BackendIdentity
return BackendIdentity(
endpoint_url="local-only://",
bucket="(no bucket)",
region=None,
)
def __getattr__(self, name):
raise RuntimeError(
f"--no-push run tried to call backend.{name}; that's a bug "
"in push_pack — local-only paths must skip every S3 call"
)
def _cmd_cold_unpack(args: argparse.Namespace) -> int:
"""Hydrate a shard from a metadata pack hash. SPV-style consumer:
- default mode "just-enough": pull only the metadata pack. Schema
restored, every chunks row has content=NULL. Queryable immediately
for metadata; chunk-body queries return null until a future JIT-
fetch path fills them on cache miss.
- "--full": after the metadata pack lands, iterate its
`_chunk_pack_hashes` and pull every chunk pack. Final state: full
corpus offline-queryable.
Either way, `args.pack_hash` must be a metadata pack hash. Chunk
packs are anonymous from the consumer's perspective — accessed via
the metadata pack's reference list, not by name.
"""
from arborist.evict import (
hydrate_from_metadata_pack,
hydrate_from_metadata_pack_routed,
)
from arborist.store import set_corpus_shard_count
backend = _make_cold_backend()
# M-aware genesis path (#000067): --hydrate-shards-dir is the target
# directory; --hydrate-M picks the routing modulus. Each incoming
# row lands on shard_for_document(root, M).
if args.hydrate_shards_dir:
target_dir = Path(args.hydrate_shards_dir).expanduser()
target_dir.mkdir(parents=True, exist_ok=True)
M = args.hydrate_M
target_paths = [target_dir / f"{i:03d}.db" for i in range(M)]
# NOTE: I tried PRAGMA page_size = 16384 on hydrate targets
# to reduce ext4-journal contention by emitting fewer
# page-write events. Measured WORSE on 2026-05-26 — narrow
# UPDATEs (~500 byte content per row) rewrite the whole
# page; 16 KB pages = 4× write amplification per chunk-
# content UPDATE. Stay at SQLite default 4 KB.
targets: list = []
for p in target_paths:
t = connect(p)
# busy_timeout FIRST so all subsequent statements wait
# instead of crashing with `database is locked` under
# parallel-hydrate (xargs -P N spawns N processes that
# each open connections to all M target shards). #54.
t.execute("PRAGMA busy_timeout = 30000")
t.execute("PRAGMA foreign_keys = OFF")
targets.append(t)
try:
result = hydrate_from_metadata_pack_routed(
targets, backend, args.pack_hash, M=M, mode=args.mode,
)
for t in targets:
with t:
set_corpus_shard_count(t, M)
finally:
for t in targets:
t.close()
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
# Single-shard legacy path (no --hydrate-shards-dir).
conn = (
connect_query(args.db, shards_dir=args.global_shards_dir)
if args.global_shards_dir
else connect(args.db)
)
try:
result = hydrate_from_metadata_pack(
conn, backend, args.pack_hash, mode=args.mode,
)
finally:
conn.close()
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
def _cold_rebuild_fts_one(path_str: str) -> tuple[str, float, int]:
"""ProcessPool worker: clear + rebuild FTS on one shard. Each shard is
its own DB file, so workers never contend. ``_rebuild_fts_on_target``
clears-first, overwriting the dead index a cold-pack FTS restore
leaves behind, then rebuilds from chunk content."""
import time as _t
from pathlib import Path as _P
from arborist.store import connect as _connect
from arborist.migrate import _rebuild_fts_on_target
t = _t.time()
conn = _connect(path_str)
try:
conn.execute("PRAGMA synchronous = OFF")
conn.execute("PRAGMA journal_mode = MEMORY")
conn.execute("PRAGMA temp_store = MEMORY")
conn.execute("PRAGMA cache_size = -524288")
_rebuild_fts_on_target(conn)
cf = conn.execute("SELECT count(*) FROM chunks_fts").fetchone()[0]
finally:
conn.close()
return (_P(path_str).name, _t.time() - t, int(cf))
def _cmd_cold_rebuild_fts(args: argparse.Namespace) -> int:
"""Rebuild chunks_fts + documents_fts on every shard, in PARALLEL
(one process per shard — separate DB files, so no lock contention).
FTS is derived from chunk content, so this is the canonical repair
for a cold-recovered peer: a cold-pack FTS restore leaves a
non-functional index (rows present, MATCH returns 0), and
``_rebuild_fts_on_target`` clears-first + rebuilds from content, so
this overwrites it cleanly. Run after `cold unpack --hydrate-shards-dir`.
"""
import concurrent.futures as _cf
import time
shards_dir = Path(args.shards_dir).expanduser()
db_files = sorted(shards_dir.glob("00[0-9].db"))
if not db_files:
print(f"no shards under {shards_dir}", file=sys.stderr)
return 1
req = int(getattr(args, "workers", 0) or 0)
workers = max(1, min(req if req > 0 else len(db_files), len(db_files)))
started = time.time()
with _cf.ProcessPoolExecutor(max_workers=workers) as ex:
for name, secs, cf in ex.map(_cold_rebuild_fts_one, [str(p) for p in db_files]):
print(f" {name}: FTS rebuilt in {secs:.1f}s (chunks_fts={cf})", file=sys.stderr)
print(
f"all {len(db_files)} shards FTS-rebuilt in {time.time() - started:.1f}s "
f"({workers}-way parallel)",
file=sys.stderr,
)
return 0
def _cmd_cold_verify(args: argparse.Namespace) -> int:
"""Self-check a hydrated shard set: chunk content materialized (not
zero-filled placeholders) AND FTS searchable. Exits non-zero with a
summary if any shard fails, so a bad recovery fails LOUDLY instead of
silently serving empty results (the 2026-05-29 genesis trap: 3/4
shards had zero-filled bodies + dead FTS and nobody noticed)."""
import re as _re
from arborist.compress import unpack_chunk
shards_dir = Path(args.shards_dir).expanduser()
db_files = sorted(shards_dir.glob("00[0-9].db"))
if not db_files:
print(f"no shards under {shards_dir}", file=sys.stderr)
return 1
n_sample = max(1, int(getattr(args, "sample", 20) or 20))
failures: list[str] = []
checked = 0
for path in db_files:
conn = connect(path)
try:
nchunks = conn.execute(
"SELECT count(*) FROM chunks WHERE content IS NOT NULL"
).fetchone()[0]
if not nchunks:
continue # legitimately empty target shard — nothing to verify
checked += 1
rows = conn.execute(
"SELECT content FROM chunks WHERE content IS NOT NULL LIMIT ?",
(n_sample,),
).fetchall()
real_words: set[str] = set()
zero = 0
for (packed,) in rows:
try:
txt = unpack_chunk(packed)
except Exception:
txt = None
if not (txt and str(txt).strip("\x00").strip()):
zero += 1
else:
for w in _re.findall(r"[a-z]{4,}", str(txt).lower())[:5]:
real_words.add(w)
if zero == len(rows):
failures.append(
f"{path.name}: content zero-filled ({zero}/{len(rows)} sampled "
f"of {nchunks}) — chunk bodies never materialized"
)
continue
probe = next(iter(real_words), None)
if probe is not None:
hits = conn.execute(
"SELECT count(*) FROM chunks_fts WHERE chunks_fts MATCH ?",
(probe,),
).fetchone()[0]
if not hits:
failures.append(
f"{path.name}: FTS dead (MATCH {probe!r} = 0 with {nchunks} "
f"chunks) — run `cold rebuild-fts`"
)
finally:
conn.close()
if failures:
print("COLD VERIFY FAILED:", file=sys.stderr)
for f in failures:
print(" - " + f, file=sys.stderr)
return 1
print(f"cold verify OK: {checked} non-empty shard(s) — content materialized + searchable")
return 0
def _cmd_cold_stream_snapshot(args: argparse.Namespace) -> int:
"""Producer: SQLite-Backup-API snapshot each shard to a raw `.db`,
multipart-upload to the bucket, swap CURRENT. Optionally Tier B
(`--just-enough`): strip chunks.content into per-chunk blobs."""
from arborist.cold_clone import stream_snapshot
backend = _make_cold_backend()
manifest = stream_snapshot(
Path(args.shards_dir).expanduser(),
backend,
just_enough=bool(getattr(args, "just_enough", False)),
snapshot_id=getattr(args, "snapshot_id", None),
workers=int(getattr(args, "workers", 0) or 0),
)
import json as _json
print(_json.dumps({
"snapshot_id": manifest.snapshot_id,
"shards": [s.__dict__ for s in manifest.shards],
"just_enough": manifest.just_enough,
}, indent=2))
return 0
def _cmd_cold_clone(args: argparse.Namespace) -> int:
"""Consumer: read CURRENT (or pinned snapshot) and pull raw `.db`
shards into --shards-dir in parallel. No pack/unpack, no FTS rebuild
— the index is already inside the file."""
from arborist.cold_clone import clone_from_bucket
backend = _make_cold_backend()
manifest = clone_from_bucket(
Path(args.shards_dir).expanduser(),
backend,
snapshot_id=getattr(args, "snapshot_id", None) or None,
workers=int(getattr(args, "workers", 4) or 4),
)
print(f"clone OK: {manifest.M} shards from snapshot {manifest.snapshot_id} "
f"into {args.shards_dir} (just_enough={manifest.just_enough})")
return 0
def _cmd_cold_stats(args: argparse.Namespace) -> int:
from arborist.cold_object import PACK_PREFIX
backend = _make_cold_backend()
pack_count = 0
total_bytes = 0
for key in backend.list_keys(PACK_PREFIX):
if key.endswith(".tar.zst"):
pack_count += 1
size = backend.object_size(key)
if size is not None:
total_bytes += size
out = {
"backend": backend.identity.to_audit_body(),
"packs": pack_count,
"packs_compressed_bytes": total_bytes,
}
print(json.dumps(out, indent=2, ensure_ascii=False))
return 0
def _cmd_cold_list(args: argparse.Namespace) -> int:
"""Enumerate packs in the bucket for new-peer hydration. SPV-aware.
For each `<hash>.{metadata,chunks}.tar.zst`, HEAD for size and
(unless --no-manifest) fetch the small manifest sidecar to surface
kind + chunk_count / table_count + chunk_pack_hashes. Output
enables a consumer to:
- find the metadata pack(s) — the entry points
- know what chunk packs each metadata pack references (full sync)
"""
from arborist.cold_object import PACK_PREFIX, pack_key, parse_manifest
backend = _make_cold_backend()
packs = []
for key in backend.list_keys(PACK_PREFIX):
# v3 keys: packs/<hash>.{metadata,chunks,fts}.tar.zst
if key.endswith(".metadata.tar.zst"):
kind = "metadata"
pack_hash = key[len(PACK_PREFIX):-len(".metadata.tar.zst")]
elif key.endswith(".chunks.tar.zst"):
kind = "chunks"
pack_hash = key[len(PACK_PREFIX):-len(".chunks.tar.zst")]
elif key.endswith(".fts.tar.zst"):
kind = "fts"
pack_hash = key[len(PACK_PREFIX):-len(".fts.tar.zst")]
else:
continue
entry = {
"pack_hash": pack_hash,
"kind": kind,
"compressed_bytes": backend.object_size(key),
}
if args.fetch_manifest:
try:
manifest_bytes = backend.get(
pack_key(pack_hash, kind=kind, manifest=True)
)
parsed = parse_manifest(manifest_bytes)
if kind == "metadata":
entry["table_count"] = len(parsed.tables)
entry["snapshot_root"] = parsed.snapshot_root
entry["chunk_pack_hashes"] = list(parsed.chunk_pack_hashes)
else:
entry["chunk_count"] = len(parsed.chunks)
except Exception as e: # noqa: BLE001 — surface but don't fail
entry["manifest_error"] = repr(e)
packs.append(entry)
out = {
"backend": backend.identity.to_audit_body(),
"pack_count": len(packs),
"packs": packs,
}
print(json.dumps(out, indent=2, ensure_ascii=False))
return 0
def _cmd_activity(args: argparse.Namespace) -> int:
"""Recent activity: Q&A records + freshly cached docs across all shards.
Designed for an agent to read before deciding the next action — what was
just asked, what was just integrated, what's the corpus state.
"""
import time as _time
from arborist.store import discover_shards
shard_paths: list[Path] = []
if args.global_shards_dir:
shard_paths = discover_shards(args.global_shards_dir)
else:
shard_paths = [Path(args.db)]
cutoff_ts = 0
if args.since_seconds:
cutoff_ts = int(_time.time()) - args.since_seconds
qa_records: list[dict] = []
ingest_events: list[dict] = []
derive_events: list[dict] = []
falsifications: list[dict] = []
corpus = {
"documents_total": 0,
"documents_surface": 0,
"documents_core": 0,
"providence_total": 0,
"providence_live": 0,
"providence_stale": 0,
"providence_failed": 0,
"audit_events_total": 0,
}
for sp in shard_paths:
c = connect(sp)
try:
corpus["documents_total"] += c.execute(
"SELECT COUNT(*) FROM documents"
).fetchone()[0]
corpus["documents_surface"] += c.execute(
"SELECT COUNT(*) FROM documents WHERE kind='surface'"
).fetchone()[0]
corpus["documents_core"] += c.execute(
"SELECT COUNT(*) FROM documents WHERE kind='core'"
).fetchone()[0]
corpus["providence_total"] += c.execute(
"SELECT COUNT(*) FROM providence_cache"
).fetchone()[0]
corpus["providence_live"] += c.execute(
"SELECT COUNT(*) FROM providence_cache WHERE falsification_state='live'"
).fetchone()[0]
corpus["providence_stale"] += c.execute(
"SELECT COUNT(*) FROM providence_cache WHERE falsification_state='stale'"
).fetchone()[0]
corpus["providence_failed"] += c.execute(
"SELECT COUNT(*) FROM providence_cache WHERE falsification_state='failed'"
).fetchone()[0]
corpus["audit_events_total"] += c.execute(
"SELECT COUNT(*) FROM audit_events"
).fetchone()[0]
# Q&A records
for r in c.execute(
"SELECT cache_key, question_text, answer_text, "
" falsification_state, hit_count, created_at, last_hit_at, "
" document_uri FROM providence_cache "
"WHERE created_at >= ? ORDER BY created_at DESC LIMIT ?",
(cutoff_ts, args.limit),
).fetchall():
ans = r["answer_text"] or ""
qa_records.append(
{
"ts": r["created_at"],
"shard": sp.name,
"cache_key": r["cache_key"],
"question": r["question_text"],
"answer_preview": (
ans if len(ans) <= args.preview_chars
else ans[: args.preview_chars] + ""
),
"sources_uri": r["document_uri"],
"state": r["falsification_state"],
"hit_count": r["hit_count"],
"last_hit_at": r["last_hit_at"],
}
)
# Recent ingest events
for r in c.execute(
"SELECT subject_root, body, ts FROM audit_events "
"WHERE event_type = 'ingest' AND ts >= ? "
"ORDER BY ts DESC LIMIT ?",
(cutoff_ts, args.limit),
).fetchall():
body = json.loads(r["body"]) if r["body"] else {}
ingest_events.append(
{
"ts": r["ts"],
"shard": sp.name,
"document_root": r["subject_root"],
"document_uri": body.get("document_uri"),
"source_type": body.get("source_type"),
"chunks": body.get("chunks"),
"supersedes": body.get("supersedes"),
}
)
# Recent derive events (distillations)
for r in c.execute(
"SELECT subject_root, body, ts FROM audit_events "
"WHERE event_type = 'derive' AND ts >= ? "
"ORDER BY ts DESC LIMIT ?",
(cutoff_ts, args.limit),
).fetchall():
body = json.loads(r["body"]) if r["body"] else {}
derive_events.append(
{
"ts": r["ts"],
"shard": sp.name,
"core_root": r["subject_root"],
"src_root": body.get("src_root"),
"process_id": body.get("process_id"),
"compression_ratio": body.get("compression_ratio"),
"compression_depth": body.get("compression_depth"),
}
)
# Recent falsifications
for r in c.execute(
"SELECT cache_key, state, reason, by_actor, at FROM falsifications "
"WHERE at >= ? ORDER BY at DESC LIMIT ?",
(cutoff_ts, args.limit),
).fetchall():
falsifications.append(
{
"ts": r["at"],
"shard": sp.name,
"cache_key": r["cache_key"],
"state": r["state"],
"reason": r["reason"],
"by_actor": r["by_actor"],
}
)
finally:
c.close()
qa_records.sort(key=lambda x: -x["ts"])
ingest_events.sort(key=lambda x: -x["ts"])
derive_events.sort(key=lambda x: -x["ts"])
falsifications.sort(key=lambda x: -x["ts"])
print(
json.dumps(
{
"as_of": int(_time.time()),
"shards": [str(p) for p in shard_paths],
"corpus": corpus,
"recent_qa": qa_records[: args.limit],
"recent_ingests": ingest_events[: args.limit],
"recent_derives": derive_events[: args.limit],
"recent_falsifications": falsifications[: args.limit],
},
indent=2, ensure_ascii=False
)
)
return 0
def _cmd_canon(args: argparse.Namespace) -> int:
"""Direct π* canonicalization — no shards, no LLM, no audit chain.
Two modes:
``arborist canon --list`` — print registry contents.
``arborist canon <key> "<input>"`` — canonicalize and print.
"""
import hashlib
from arborist.pi_star import PiStarError, get, list_keys
if args.list:
from arborist.pi_star import REGISTRY
for key in sorted(list_keys()):
ps = REGISTRY[key]
print(f"{key}\t{ps.domain}")
return 0
if not args.key or args.input is None:
print(
"error: KEY and INPUT required (or --list to see registry)",
file=sys.stderr,
)
return 2
try:
ps = get(args.key)
except KeyError:
print(f"error: unknown π*: {args.key!r}", file=sys.stderr)
print(
f"available: {', '.join(sorted(list_keys()))}",
file=sys.stderr,
)
return 2
try:
canonical = ps.canonicalize(args.input.encode("utf-8"))
except PiStarError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
except NotImplementedError as exc:
print(f"error: {args.key} not yet implemented ({exc})", file=sys.stderr)
return 1
if args.json:
digest = hashlib.sha256(canonical).hexdigest()
print(json.dumps({
"pi_star_ref": args.key,
"input": args.input,
"canonical": canonical.decode("utf-8", errors="replace"),
"canonical_sha256": digest,
}, ensure_ascii=False))
else:
sys.stdout.buffer.write(canonical)
sys.stdout.buffer.write(b"\n")
return 0
def _cmd_stats(args: argparse.Namespace) -> int:
"""Counts: documents, chunks (by tier), edges, providence, audit events."""
conn = (
connect_query(args.db, shards_dir=args.global_shards_dir)
if args.global_shards_dir
else connect(args.db)
)
try:
result = stats(conn)
finally:
conn.close()
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
def _check_audit_chain(conn: sqlite3.Connection) -> tuple[int, int]:
"""Return (events_checked, breaks) for one audit chain in `conn`."""
import hashlib
rows = conn.execute(
"SELECT seq, event_hash, prev_event_hash, body FROM audit_events ORDER BY seq"
).fetchall()
prev = None
breaks = 0
for r in rows:
h = hashlib.sha256()
if r["prev_event_hash"]:
h.update(bytes.fromhex(r["prev_event_hash"]))
h.update(r["body"].encode("utf-8"))
if h.hexdigest() != r["event_hash"]:
breaks += 1
if r["prev_event_hash"] != prev:
breaks += 1
prev = r["event_hash"]
return len(rows), breaks
def _cmd_analyze(args: argparse.Namespace) -> int:
"""Compression spectrum, depth distribution, audit chain integrity."""
from arborist.store import discover_shards
# In sharded mode, audit chains live per-shard (each shard has its own
# genesis -> latest). Check each independently and aggregate.
audit_summary: dict | None = None
if args.global_shards_dir:
per_shard_chain = []
total_events = 0
total_breaks = 0
for sp in discover_shards(args.global_shards_dir):
# Read-only: an audit-chain walk must not run migration DDL
# or take a write lock on the shard (see connect_readonly).
sc = connect_readonly(sp)
try:
ev, br = _check_audit_chain(sc)
finally:
sc.close()
per_shard_chain.append({"shard": sp.name, "events": ev, "breaks": br})
total_events += ev
total_breaks += br
audit_summary = {
"events": total_events,
"breaks": total_breaks,
"shards": per_shard_chain,
}
conn = (
connect_query(args.db, shards_dir=args.global_shards_dir)
if args.global_shards_dir
else connect(args.db)
)
try:
# Depth distribution.
depth = conn.execute(
"SELECT compression_depth, COUNT(*) AS n "
"FROM documents GROUP BY compression_depth ORDER BY 1"
).fetchall()
# Per-process compression ratios.
procs = conn.execute(
"SELECT json_extract(body, '$.process_id') AS process_id, "
" json_extract(body, '$.src_kind') AS src_kind, "
" AVG(CAST(json_extract(body, '$.compression_ratio') AS REAL)) AS mean_ratio, "
" MIN(CAST(json_extract(body, '$.compression_ratio') AS REAL)) AS min_ratio, "
" MAX(CAST(json_extract(body, '$.compression_ratio') AS REAL)) AS max_ratio, "
" COUNT(*) AS n_events "
"FROM audit_events WHERE event_type='derive' "
"GROUP BY process_id, src_kind"
).fetchall()
# Source/kind crosstab.
kinds = conn.execute(
"SELECT source_type, kind, COUNT(*) AS n "
"FROM documents GROUP BY source_type, kind ORDER BY 3 DESC"
).fetchall()
# Tier distribution.
tiers = conn.execute(
"SELECT tier, COUNT(*) AS n FROM chunks GROUP BY tier"
).fetchall()
# Top inbound link targets — the 'gravity wells' of the corpus,
# counted per resolved destination document (dst_root), which the
# idx_edges_dst_root partial index serves directly: an index-ordered
# scan -> streaming GROUP BY, bounded memory. (Grouping by the raw
# dst_uri link string instead has no index and hash-aggregates over
# every target including red links -> unbounded RSS at corpus scale.)
# Join documents only for the surviving top-N rows, for titles.
gravity = conn.execute(
"SELECT g.root AS root, g.inbound AS inbound, "
" d.document_uri AS uri, d.title AS title "
"FROM (SELECT dst_root AS root, COUNT(*) AS inbound "
" FROM edges "
" WHERE edge_type='wikilink' AND dst_root <> '' "
" GROUP BY dst_root ORDER BY inbound DESC LIMIT ?) g "
"LEFT JOIN documents d ON d.document_root = g.root "
"ORDER BY g.inbound DESC",
(args.gravity_top,),
).fetchall()
# Audit chain integrity (per-shard if sharded; single chain otherwise).
if audit_summary is None:
ev, br = _check_audit_chain(conn)
audit_summary = {"events": ev, "breaks": br}
report = {
"compression_depth_histogram": [
{"depth": r["compression_depth"], "count": r["n"]} for r in depth
],
"distillers": [
{
"process_id": r["process_id"],
"src_kind": r["src_kind"],
"n_events": r["n_events"],
"compression_ratio": {
"mean": (
round(r["mean_ratio"], 4)
if r["mean_ratio"] is not None
else None
),
"min": (
round(r["min_ratio"], 4)
if r["min_ratio"] is not None
else None
),
"max": (
round(r["max_ratio"], 4)
if r["max_ratio"] is not None
else None
),
},
}
for r in procs
],
"documents_by_source_kind": [
{"source_type": r["source_type"], "kind": r["kind"], "count": r["n"]}
for r in kinds
],
"chunks_by_tier": {r["tier"]: r["n"] for r in tiers},
"audit_chain": audit_summary,
"gravity_top_inbound": [
{
"root": r["root"],
"uri": r["uri"],
"title": r["title"],
"inbound": r["inbound"],
}
for r in gravity
],
}
finally:
conn.close()
print(json.dumps(report, indent=2, ensure_ascii=False))
return 0
def _cmd_snapshot_create(args: argparse.Namespace) -> int:
"""Compute snapshot_root over the read scope, persist into args.db.
Single-DB mode (--db only): read + write are the same connection;
delegate to the snapshot module's create_snapshot().
Cross-shard mode (--shards-dir + --db): read against the in-memory
UNION view to get the cluster-level Merkle root, then persist into
args.db (a dedicated snapshots store, conventionally
`~/.arborist/shards/snapshots.db`). The writer's own documents table
is irrelevant to the snapshot value — only the union scope counts.
"""
import time as _time
from arborist.snapshot import compute_snapshot_root, create_snapshot
if args.global_shards_dir is None:
conn = connect(args.db)
try:
result = create_snapshot(
conn, reason=args.reason, parent_snapshot=args.parent,
)
finally:
conn.close()
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
# Cross-shard: compute against UNION, write to args.db.
read_conn = connect_query(args.db, shards_dir=args.global_shards_dir)
try:
snapshot_root, doc_count = compute_snapshot_root(read_conn)
finally:
read_conn.close()
write_conn = connect(args.db)
try:
parent = args.parent
if parent is None:
row = write_conn.execute(
"SELECT snapshot_root FROM snapshots ORDER BY taken_at DESC LIMIT 1"
).fetchone()
if row is not None:
parent = row["snapshot_root"]
now = int(_time.time())
body = {
"snapshot_root": snapshot_root,
"doc_count": doc_count,
"parent_snapshot": parent,
"reason": args.reason,
"scope": "shards-union",
}
audit_event_hash = append_audit(
write_conn,
event_type="snapshot_create",
body=body,
subject_root=snapshot_root,
ts=now,
)
with transaction(write_conn):
write_conn.execute(
"INSERT OR IGNORE INTO snapshots "
"(snapshot_root, taken_at, audit_event_hash, doc_count, "
" parent_snapshot, reason) VALUES (?, ?, ?, ?, ?, ?)",
(
snapshot_root,
now,
audit_event_hash,
doc_count,
parent,
args.reason,
),
)
finally:
write_conn.close()
print(
json.dumps(
{
"snapshot_root": snapshot_root,
"doc_count": doc_count,
"parent_snapshot": parent,
"audit_event_hash": audit_event_hash,
"taken_at": now,
"reason": args.reason,
"scope": "shards-union",
},
indent=2, ensure_ascii=False
)
)
return 0
def _cmd_snapshot_list(args: argparse.Namespace) -> int:
"""List recent corpus snapshots (newest first, --limit N)."""
from arborist.snapshot import list_snapshots
conn = connect(args.db)
try:
rows = list_snapshots(conn, limit=args.limit)
finally:
conn.close()
print(json.dumps(rows, indent=2, ensure_ascii=False))
return 0
def _cmd_snapshot_verify(args: argparse.Namespace) -> int:
"""Re-derive snapshot root from current corpus; non-zero exit on drift."""
from arborist.snapshot import verify_snapshot
conn = (
connect_query(args.db, shards_dir=args.global_shards_dir)
if args.global_shards_dir
else connect(args.db)
)
try:
result = verify_snapshot(conn, args.snapshot_root)
finally:
conn.close()
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0 if result["matches"] else 1
def _cmd_snapshot_diff(args: argparse.Namespace) -> int:
from arborist.snapshot import diff_against_current
conn = (
connect_query(args.db, shards_dir=args.global_shards_dir)
if args.global_shards_dir
else connect(args.db)
)
try:
result = diff_against_current(conn, args.snapshot_root)
finally:
conn.close()
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
def _cmd_substrate_score(args: argparse.Namespace) -> int:
"""Compute the substrate ForkScore over (parent, child) bench-result JSON files."""
from arborist.substrate import (
bench_result_to_metrics,
fork_score,
)
from arborist.substrate.weights import DEFAULT_WEIGHTS, from_dict as weights_from_dict
with open(args.parent, "r", encoding="utf-8") as fh:
parent_payload = json.load(fh)
with open(args.child, "r", encoding="utf-8") as fh:
child_payload = json.load(fh)
if args.weights:
with open(args.weights, "r", encoding="utf-8") as fh:
weights = weights_from_dict(json.load(fh))
else:
weights = DEFAULT_WEIGHTS
parent_metrics = bench_result_to_metrics(parent_payload)
child_metrics = bench_result_to_metrics(child_payload)
scored = fork_score(
parent_metrics,
child_metrics,
weights=weights,
capital_delta=float(args.capital_delta),
selfmodel_calibration_gain=float(args.selfmodel_calibration_gain),
audit_completeness=float(args.audit_completeness),
validator_diversity=float(args.validator_diversity),
security_risk=float(args.security_risk),
complexity_delta=float(args.complexity_delta),
memory_invalidation_count=float(args.memory_invalidation_count),
)
artifact = scored.to_dict()
artifact_json = json.dumps(
artifact, indent=2, ensure_ascii=False, default=str
)
print(artifact_json)
# Phase 1b — also write to disk so CI / mesh peers / downstream
# graders can pick up the artifact without parsing stdout.
out_path = getattr(args, "out", None)
if out_path:
from pathlib import Path
p = Path(out_path)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(artifact_json + "\n", encoding="utf-8")
# Phase 1c — branch-set persistence (#000012). Default off:
# writes only when --branch-set is present. --branch-id falls
# back to --child-root so a single-flag CLI works for the common
# case (one branch per child-root identity).
branch_set_id = getattr(args, "branch_set", None)
if branch_set_id:
from arborist.substrate.fork_score import persist_branch_score
parent_root = getattr(args, "parent_root", None)
child_root = getattr(args, "child_root", None)
branch_id = getattr(args, "branch_id", None) or child_root
if not parent_root or not branch_id:
sys.stderr.write(
"--branch-set requires --parent-root + (--branch-id "
"or --child-root)\n"
)
return 2
persist_shard = getattr(args, "persist_shard", None) or args.db
weights_id = getattr(args, "weights_id", None) or (
"default" if not args.weights else Path(args.weights).stem
)
p_conn = connect(persist_shard)
try:
with transaction(p_conn):
persist_branch_score(
p_conn,
branch_set_id=branch_set_id,
branch_id=branch_id,
parent_root=parent_root,
child_root=child_root,
scored=scored,
weights_id=weights_id,
)
finally:
p_conn.close()
# Non-zero exit on REJECT so CI can gate on it.
return 0 if scored.verdict in ("ACCEPT", "MARGINAL") else 1
def _cmd_memory_snapshot(args: argparse.Namespace) -> int:
"""Build a memory snapshot from current store state and persist it."""
from arborist.memory import snapshot, store_snapshot
conn = connect(args.db)
try:
with transaction(conn):
ms = snapshot(conn)
root = store_snapshot(conn, ms)
finally:
conn.close()
print(json.dumps({"memory_root": root}, indent=2, ensure_ascii=False))
return 0
def _cmd_memory_show(args: argparse.Namespace) -> int:
"""Print a memory record by root, or the latest live one."""
from arborist.memory import branches_for, latest, load
conn = connect(args.db)
try:
if args.root:
row = load(conn, args.root)
else:
row = latest(conn)
if row is None:
print(json.dumps({"error": "no memory snapshot found"}, indent=2))
return 1
body = row["branch_summaries_blob"]
if isinstance(body, (bytes, bytearray)):
body = body.decode("utf-8", errors="replace")
out = {
"memory_root": row["memory_root"],
"state": row["state"],
"schema_version": row["schema_version"],
"parent_memory_root": row["parent_memory_root"],
"audit_events_high_water": row["audit_events_high_water"],
"audit_event_hash": row["audit_event_hash"],
"created_at": row["created_at"],
"falsified_at": row["falsified_at"],
"falsified_reason": row["falsified_reason"],
"body": body,
"branches": branches_for(conn, row["memory_root"]),
}
finally:
conn.close()
print(json.dumps(out, indent=2, ensure_ascii=False, default=str))
return 0
def _cmd_memory_branches(args: argparse.Namespace) -> int:
"""List branch summaries attached to a memory_root."""
from arborist.memory import branches_for, latest
conn = connect(args.db)
try:
if args.root:
root = args.root
else:
row = latest(conn)
if row is None:
print(json.dumps([], indent=2))
return 0
root = row["memory_root"]
out = branches_for(conn, root)
finally:
conn.close()
# Decode summary_blob to text for human inspection.
rendered = []
for b in out:
rec = dict(b)
if isinstance(rec["summary_blob"], (bytes, bytearray)):
rec["summary_blob"] = rec["summary_blob"].decode(
"utf-8", errors="replace"
)
rendered.append(rec)
print(json.dumps(rendered, indent=2, ensure_ascii=False, default=str))
return 0
def _cmd_memory_falsify(args: argparse.Namespace) -> int:
"""Mark a memory_root falsified."""
from arborist.memory import falsify
conn = connect(args.db)
try:
with transaction(conn):
event_hash = falsify(
conn,
args.root,
reason=args.reason,
triggering_branch_id=args.branch_id,
)
finally:
conn.close()
print(
json.dumps(
{
"memory_root": args.root,
"audit_event_hash": event_hash or None,
"noop": event_hash == "",
},
indent=2,
ensure_ascii=False,
)
)
return 0
def _cmd_capital_summary(args: argparse.Namespace) -> int:
"""Aggregate capital_ledger totals; per-form sums + row count."""
from arborist.capital import summary as capital_summary
conn = connect(args.db)
try:
out = capital_summary(conn, op_type=args.op_type, since=args.since)
finally:
conn.close()
print(json.dumps(out, indent=2, ensure_ascii=False))
return 0
def _cmd_capital_op_cost(args: argparse.Namespace) -> int:
"""Per-form totals for one op_type."""
from arborist.capital import op_cost
conn = connect(args.db)
try:
out = op_cost(conn, args.op_type)
finally:
conn.close()
print(json.dumps(out, indent=2, ensure_ascii=False))
return 0
def _cmd_capital_top(args: argparse.Namespace) -> int:
"""Top-N op_types by total contribution to a single capital form."""
from arborist.capital import top_by_form
conn = connect(args.db)
try:
out = top_by_form(conn, args.form, limit=args.limit)
finally:
conn.close()
print(json.dumps(out, indent=2, ensure_ascii=False))
return 0
def _cmd_selfmodel_snapshot(args: argparse.Namespace) -> int:
"""Build a SelfModel from current store state and persist it.
Reads the latest providence_cache row + audit_events to derive the
canonical fields (model_profile_hash, governance_policy_hash,
verifier_method_root, etc.) and writes one row to selfmodel_records
+ emits a selfmodel_snapshot_landed audit event.
"""
from arborist.selfmodel import snapshot, store_snapshot
conn = connect(args.db)
try:
with transaction(conn):
sm = snapshot(conn)
root = store_snapshot(conn, sm)
finally:
conn.close()
print(json.dumps({"selfmodel_root": root}, indent=2, ensure_ascii=False))
return 0
def _cmd_selfmodel_show(args: argparse.Namespace) -> int:
"""Print a SelfModel record by root, or the latest live one."""
from arborist.selfmodel import latest, load
from arborist.selfmodel.store import claims_for
conn = connect(args.db)
try:
if args.root:
row = load(conn, args.root)
else:
row = latest(conn)
if row is None:
print(json.dumps({"error": "no SelfModel found"}, indent=2))
return 1
body = row["body_blob"]
if isinstance(body, (bytes, bytearray)):
body = body.decode("utf-8", errors="replace")
out = {
"selfmodel_root": row["selfmodel_root"],
"state": row["state"],
"schema_version": row["schema_version"],
"parent_selfmodel_root": row["parent_selfmodel_root"],
"model_profile_hash": row["model_profile_hash"],
"verifier_method_root": row["verifier_method_root"],
"governance_policy_hash": row["governance_policy_hash"],
"canonicalization_version": row["canonicalization_version"],
"chunking_version": row["chunking_version"],
"memory_root": row["memory_root"],
"audit_event_hash": row["audit_event_hash"],
"created_at": row["created_at"],
"falsified_at": row["falsified_at"],
"falsified_reason": row["falsified_reason"],
"claims": claims_for(conn, row["selfmodel_root"]),
}
finally:
conn.close()
print(json.dumps(out, indent=2, ensure_ascii=False, default=str))
return 0
def _cmd_selfmodel_falsify(args: argparse.Namespace) -> int:
"""Mark a SelfModel falsified, citing a reason."""
from arborist.selfmodel import falsify
conn = connect(args.db)
try:
with transaction(conn):
event_hash = falsify(
conn,
args.root,
reason=args.reason,
triggering_claim_hash=args.claim_hash,
)
finally:
conn.close()
print(
json.dumps(
{
"selfmodel_root": args.root,
"audit_event_hash": event_hash or None,
"noop": event_hash == "",
},
indent=2,
ensure_ascii=False,
)
)
return 0
def _cmd_selfmodel_list(args: argparse.Namespace) -> int:
"""List recent SelfModel rows, newest first."""
conn = connect(args.db)
try:
rows = conn.execute(
"SELECT selfmodel_root, state, model_profile_hash,"
" governance_policy_hash, created_at,"
" falsified_at, falsified_reason"
" FROM selfmodel_records "
" ORDER BY created_at DESC LIMIT ?",
(args.limit,),
).fetchall()
finally:
conn.close()
print(
json.dumps(
[dict(r) for r in rows], indent=2, ensure_ascii=False, default=str
)
)
return 0
def _render_warrant_chain_tail(result: dict) -> str:
"""Compute a render-layer tail showing how many cited sources
have a `warrant-resolver-v1` derivations chain back to a primary-
source surface (ticket #000031 Phase 3).
Returns ``""`` when no shards_dir is on the result dict, no
chains exist, or the lookup fails — keeps the render path silent
in absence of data.
Returns ``" · warrant: 1 proven"`` (or higher count) when cited
sources have Merkle-bound bindings to textbook surface chunks.
The actual ladder upgrade — POINTER-LINKED → ANCHOR-WARRANTED →
EVIDENCE-WARRANTED based on warrant chains — is intentionally
NOT done here. This is a positive-signal render addition; the
underlying audit_mode + violations stay as the verifier
produced them.
Reads ``result["_shards_dir"]`` (stashed by ``_cmd_query`` before
render time). The render function takes only ``(result, question)``
so the shards_dir threads through the result dict.
"""
shards_dir = result.get("_shards_dir")
if not shards_dir:
return ""
sources = result.get("sources") or []
if not sources:
return ""
try:
from arborist.qa.warrant_resolver import warrant_chains_for_sources
chains = warrant_chains_for_sources(sources, shards_dir)
except Exception:
return ""
if not chains:
return ""
n = len(chains)
label = "warrant" if n == 1 else "warrants"
return f" · {label}: {n} proven"
def _cmd_warrant_status(args: argparse.Namespace) -> int:
"""Read-only — show what surface chunks the citation resolver
finds for each claim-pack record. JSON output. No DB writes.
See ``arborist.qa.warrant_resolver`` for ticket #000031 Phase 2.
"""
from arborist.qa.warrant_resolver import warrant_status
shards_dir = args.global_shards_dir or args.shards_dir
if not shards_dir:
print("--shards-dir is required for warrant-status", file=sys.stderr)
return 2
results = warrant_status(shards_dir, limit=args.limit)
out = []
for r in results:
out.append(
{
"record_root": r.record_root,
"record_title": r.record_title,
"source_reference": r.source_reference,
"citations": [
{
"title": c.title,
"authors": list(c.authors),
"year": c.year,
"section": c.section,
}
for c in r.citations
],
"matches": [
{
"shard": m.shard_path,
"document_root": m.document_root,
"document_title": m.document_title,
"chunk_id": m.chunk_id,
"score": m.score,
"snippet": m.snippet,
}
for m in r.matches
],
"has_derivation": r.has_derivation,
}
)
print(json.dumps(out, indent=2, ensure_ascii=False))
return 0
def _cmd_warrant_resolve(args: argparse.Namespace) -> int:
"""Run the warrant resolver across all claim-pack records.
Default dry-run (just summary counts); ``--write`` flag computes
Merkle inclusion proofs for top matches and writes
``derivations`` rows. Idempotent at the database layer.
"""
from arborist.qa.warrant_resolver import warrant_resolve
shards_dir = args.global_shards_dir or args.shards_dir
if not shards_dir:
print("--shards-dir is required for warrant-resolve", file=sys.stderr)
return 2
summary = warrant_resolve(
shards_dir,
write=args.write,
limit=args.limit,
use_aliases=getattr(args, "use_aliases", False),
)
print(json.dumps(summary, indent=2))
return 0
def _aliases_db_path(args: argparse.Namespace) -> str | None:
"""Resolve the aliases DB path. Default: shard 000 of the shards
cluster (alongside claim-pack records). Operators can override
via --aliases-db."""
explicit = getattr(args, "aliases_db", None)
if explicit:
return str(explicit)
shards_dir = args.global_shards_dir or getattr(args, "shards_dir", None)
if not shards_dir:
return None
return str(Path(shards_dir) / "000.db")
def _cmd_alias_citation_add(args: argparse.Namespace) -> int:
"""Add a citation alias. Refuses without --by; audit-discipline
fail-closed at the API surface."""
from arborist.qa.aliases import add_citation_alias
db = _aliases_db_path(args)
if not db:
print("--shards-dir or --aliases-db is required", file=sys.stderr)
return 2
try:
inserted = add_citation_alias(
db,
original_ref=args.original,
substitute_ref=args.substitute,
substitute_authors=args.author or [],
substitute_title=args.title or "",
decision_by=args.by,
decision_rationale=args.rationale or "",
)
except ValueError as exc:
print(f"alias add failed: {exc}", file=sys.stderr)
return 2
print(json.dumps({"inserted": inserted, "db": db}, indent=2))
return 0
def _cmd_alias_citation_list(args: argparse.Namespace) -> int:
from arborist.qa.aliases import list_citation_aliases
db = _aliases_db_path(args)
if not db:
print("--shards-dir or --aliases-db is required", file=sys.stderr)
return 2
aliases = list_citation_aliases(db, original_filter=args.filter)
print(
json.dumps(
[
{
"original_ref": a.original_ref,
"substitute_ref": a.substitute_ref,
"substitute_authors": list(a.substitute_authors),
"substitute_title": a.substitute_title,
"decision_at": a.decision_at,
"decision_by": a.decision_by,
"decision_rationale": a.decision_rationale,
}
for a in aliases
],
indent=2,
ensure_ascii=False,
)
)
return 0
def _cmd_alias_citation_remove(args: argparse.Namespace) -> int:
from arborist.qa.aliases import remove_citation_alias
db = _aliases_db_path(args)
if not db:
print("--shards-dir or --aliases-db is required", file=sys.stderr)
return 2
removed = remove_citation_alias(db, args.original, args.substitute)
print(json.dumps({"removed": removed, "db": db}, indent=2))
return 0
def _cmd_alias_term_add(args: argparse.Namespace) -> int:
"""Add a term alias. Refuses without --by; audit-discipline
fail-closed at the API surface."""
from arborist.qa.aliases import add_term_alias
db = _aliases_db_path(args)
if not db:
print("--shards-dir or --aliases-db is required", file=sys.stderr)
return 2
try:
inserted = add_term_alias(
db,
term=args.term,
alternate_term=args.alternate,
domain=args.domain,
decision_by=args.by,
decision_rationale=args.rationale or "",
)
except ValueError as exc:
print(f"alias add failed: {exc}", file=sys.stderr)
return 2
print(json.dumps({"inserted": inserted, "db": db}, indent=2))
return 0
def _cmd_alias_term_list(args: argparse.Namespace) -> int:
from arborist.qa.aliases import list_term_aliases
db = _aliases_db_path(args)
if not db:
print("--shards-dir or --aliases-db is required", file=sys.stderr)
return 2
aliases = list_term_aliases(db, domain=args.domain, term_filter=args.filter)
print(
json.dumps(
[
{
"term": a.term,
"alternate_term": a.alternate_term,
"domain": a.domain,
"decision_at": a.decision_at,
"decision_by": a.decision_by,
"decision_rationale": a.decision_rationale,
}
for a in aliases
],
indent=2,
ensure_ascii=False,
)
)
return 0
def _cmd_alias_term_remove(args: argparse.Namespace) -> int:
from arborist.qa.aliases import remove_term_alias
db = _aliases_db_path(args)
if not db:
print("--shards-dir or --aliases-db is required", file=sys.stderr)
return 2
removed = remove_term_alias(db, args.term, args.alternate, args.domain)
print(json.dumps({"removed": removed, "db": db}, indent=2))
return 0
def _cmd_sweep(args: argparse.Namespace) -> int:
"""Unconscious sweep — drain the meta-cognition backlog.
Implements the warrant-resolver-only fragment of #000037 §3.1
Target B (documents that bypassed meta-cognition at ingest
time). The full bicameral sweep (canonical-projection probe
/ freshness probe / document-content witness fan-out) is
gated on the §12 phase trigger and a small schema migration
(`documents.last_swept_at`); `--target warrants` here is
the no-schema-change increment available today.
Idempotent: re-running on the same shards is a no-op at the
database layer (PK collision on
`(core_root, src_root, process_id)` per the existing
`derivations` table). Operators can run on a cron / systemd
timer without worrying about row proliferation.
"""
from arborist.qa.warrant_resolver import warrant_resolve
shards_dir = args.global_shards_dir or args.shards_dir
if not shards_dir:
print("--shards-dir is required for sweep", file=sys.stderr)
return 2
if args.target == "warrants":
summary = warrant_resolve(
shards_dir,
write=args.write,
limit=args.limit,
use_aliases=getattr(args, "use_aliases", False),
)
summary["target"] = "warrants"
summary["mode"] = "write" if args.write else "dry-run"
print(json.dumps(summary, indent=2))
return 0
if args.target == "all":
# Full bicameral sweep awaits #000037 §12 phase trigger
# (canonical-projection probe + freshness probe + document-
# content witness fan-out + documents.last_swept_at schema
# migration). Today this is a no-op with an honest message.
print(
json.dumps(
{
"target": "all",
"status": "deferred",
"reason": (
"Full bicameral sweep requires #000037 §12 "
"phase trigger + documents.last_swept_at "
"migration. Use --target warrants for the "
"available increment today."
),
},
indent=2,
)
)
return 0
print(f"unknown sweep target: {args.target}", file=sys.stderr)
return 2
def _cmd_mesh_status(args: argparse.Namespace) -> int:
"""Show mesh state: enabled flag, identity, current epoch, roster."""
from arborist.mesh import current_epoch, is_enabled, load_identity
from arborist.mesh.state import roster_at
conn = connect(args.db)
try:
ident = load_identity(conn)
epoch = current_epoch(conn)
roster = roster_at(conn, epoch) if epoch is not None else []
out = {
"enabled": is_enabled(conn),
"identity": (
{
"member_id": ident.member_id,
"group_name": ident.group_name,
"sign_pub_hex": ident.sign_pub.hex(),
"dh_pub_hex": ident.dh_pub.hex(),
"created_at": ident.created_at,
}
if ident
else None
),
"current_epoch": epoch,
"roster": [
{
"member_id": m.member_id,
"role": m.role,
"sign_pub_hex": m.sign_pub.hex(),
"dh_pub_hex": m.dh_pub.hex(),
}
for m in roster
],
}
finally:
conn.close()
print(json.dumps(out, indent=2, ensure_ascii=False))
return 0
def _cmd_mesh_init(args: argparse.Namespace) -> int:
from arborist.mesh import init_identity
conn = connect(args.db)
try:
ident = init_identity(conn, group_name=args.group, member_id=args.member_id)
except RuntimeError as e:
print(f"error: {e}", file=sys.stderr)
conn.close()
return 2
finally:
conn.close()
print(
json.dumps(
{
"member_id": ident.member_id,
"group_name": ident.group_name,
"sign_pub_hex": ident.sign_pub.hex(),
"dh_pub_hex": ident.dh_pub.hex(),
"note": "share sign_pub_hex + dh_pub_hex with the founder of any group "
"you want to join. Run 'mesh enable' to flip the gating flag on.",
},
indent=2, ensure_ascii=False
)
)
return 0
def _cmd_mesh_enable(args: argparse.Namespace) -> int:
from arborist.mesh import set_enabled
conn = connect(args.db)
try:
set_enabled(conn, True)
finally:
conn.close()
print(json.dumps({"enabled": True}, indent=2, ensure_ascii=False))
return 0
def _cmd_mesh_disable(args: argparse.Namespace) -> int:
from arborist.mesh import set_enabled
conn = connect(args.db)
try:
set_enabled(conn, False)
finally:
conn.close()
print(json.dumps({"enabled": False}, indent=2, ensure_ascii=False))
return 0
def _cmd_mesh_members(args: argparse.Namespace) -> int:
from arborist.mesh import current_epoch
from arborist.mesh.state import roster_at
conn = connect(args.db)
try:
epoch = current_epoch(conn)
if epoch is None:
print(json.dumps({"error": "mesh not initialized"}, indent=2, ensure_ascii=False))
return 2
roster = roster_at(conn, epoch)
finally:
conn.close()
print(
json.dumps(
{
"epoch": epoch,
"members": [
{
"member_id": m.member_id,
"role": m.role,
"sign_pub_hex": m.sign_pub.hex(),
"dh_pub_hex": m.dh_pub.hex(),
}
for m in roster
],
},
indent=2, ensure_ascii=False
)
)
return 0
def _cmd_mesh_add(args: argparse.Namespace) -> int:
from arborist.mesh.members import add_member
try:
sign_pub = bytes.fromhex(args.sign_pub)
dh_pub = bytes.fromhex(args.dh_pub)
except ValueError:
print("error: --sign-pub and --dh-pub must be hex-encoded 32-byte keys", file=sys.stderr)
return 2
if len(sign_pub) != 32 or len(dh_pub) != 32:
print("error: keys must decode to exactly 32 bytes", file=sys.stderr)
return 2
conn = connect(args.db)
try:
epoch = add_member(
conn,
member_id=args.member_id,
sign_pub=sign_pub,
dh_pub=dh_pub,
role=args.role,
)
except (PermissionError, RuntimeError, ValueError) as e:
print(f"error: {e}", file=sys.stderr)
return 2
finally:
conn.close()
print(json.dumps({"new_epoch": epoch, "added": args.member_id}, indent=2, ensure_ascii=False))
return 0
def _cmd_mesh_kick(args: argparse.Namespace) -> int:
from arborist.mesh.members import kick_member
conn = connect(args.db)
try:
epoch = kick_member(conn, member_id=args.member_id, reason=args.reason)
except (PermissionError, RuntimeError, ValueError) as e:
print(f"error: {e}", file=sys.stderr)
return 2
finally:
conn.close()
print(
json.dumps(
{"new_epoch": epoch, "kicked": args.member_id, "reason": args.reason},
indent=2, ensure_ascii=False
)
)
return 0
def _cmd_mesh_rotate(args: argparse.Namespace) -> int:
from arborist.mesh.members import scheduled_rotate
conn = connect(args.db)
try:
epoch = scheduled_rotate(conn, reason=args.reason)
except (PermissionError, RuntimeError) as e:
print(f"error: {e}", file=sys.stderr)
return 2
finally:
conn.close()
print(json.dumps({"new_epoch": epoch, "reason": args.reason}, indent=2, ensure_ascii=False))
return 0
def _cmd_mesh_serve(args: argparse.Namespace) -> int:
"""Run the HTTP gossip server until SIGINT."""
from arborist.mesh import is_enabled, load_identity
from arborist.mesh.wire import MeshWireServer
conn = connect(args.db)
try:
if load_identity(conn) is None:
print("error: mesh not initialized; run 'arborist mesh init' first", file=sys.stderr)
return 2
if not is_enabled(conn):
print("error: mesh.enabled is off; run 'arborist mesh enable' first", file=sys.stderr)
return 2
finally:
conn.close()
srv = MeshWireServer(args.db, host=args.host, port=args.port)
print(json.dumps({"status": "serving", "url": srv.url, "db": str(args.db)}, ensure_ascii=False))
sys.stdout.flush()
try:
srv.serve()
except KeyboardInterrupt:
print(json.dumps({"status": "stopped", "reason": "SIGINT"}, ensure_ascii=False))
finally:
srv.stop()
return 0
def _cmd_mesh_sync(args: argparse.Namespace) -> int:
"""Push local roots + falsifications to a peer.
Two pushes happen by default (unless ``--no-roots`` /
``--no-falsifications`` opts one out):
1. **ANNOUNCE_ROOT** for the most-recent ``--limit`` documents
older than the kindergarten window. Receivers dedup by
``documents.document_root``.
2. **ANNOUNCE_FALSIFICATION** for the most-recent ``--limit``
falsifications older than the kindergarten window. Burns
deliberately NOT propagated — local kindergarten cleanup.
**Kindergarten window.** Records younger than
``--kindergarten-seconds`` (default 3600 = 1 hour) are NOT
broadcast. Gives the operator time to inspect a fresh ingest or
falsification & burn it before the network sees it. Override per
invocation; ``--kindergarten-seconds 0`` broadcasts everything
(cron-friendly opt-out for operators who prefer immediate
propagation). The window is sender-side discipline; receivers
don't enforce it because they have no view into when the sender
created the record.
Receivers verify the Ed25519 signature, run per-peer chain-of-
claims fork detection, then write one ``mesh_received`` audit
event per accepted envelope. Duplicate broadcasts produce
duplicate audit-log entries on the receiver but no state
corruption.
"""
import time as _time
from arborist.mesh import is_enabled, load_identity
from arborist.mesh.wire import MeshWireClient
now_ts = int(_time.time())
cutoff_ts = now_ts - max(0, args.kindergarten_seconds)
conn = connect(args.db)
try:
if load_identity(conn) is None:
print("error: mesh not initialized", file=sys.stderr)
return 2
if not is_enabled(conn):
print("error: mesh.enabled is off", file=sys.stderr)
return 2
# Total counts inform skipped-by-kindergarten reporting.
total_roots = 0
total_falsifications = 0
root_rows: list = []
falsification_rows: list = []
if not args.no_roots:
total_roots = conn.execute(
"SELECT COUNT(*) FROM documents"
).fetchone()[0]
root_rows = conn.execute(
"SELECT document_root, document_uri, chunking_version, "
" canonicalization_version, schema_version "
"FROM documents WHERE ingest_ts <= ? "
"ORDER BY rowid DESC LIMIT ?",
(cutoff_ts, args.limit),
).fetchall()
if not args.no_falsifications:
total_falsifications = conn.execute(
"SELECT COUNT(*) FROM falsifications"
).fetchone()[0]
falsification_rows = conn.execute(
"SELECT cache_key, reason FROM falsifications "
"WHERE at <= ? "
"ORDER BY at DESC LIMIT ?",
(cutoff_ts, args.limit),
).fetchall()
finally:
conn.close()
# Skipped-by-kindergarten = (rows younger than cutoff that would have
# been in the most-recent --limit) — approximated by total minus what
# we pulled, capped at limit.
fresh_roots_held = max(
0,
min(total_roots, args.limit) - len(root_rows),
) if not args.no_roots else 0
fresh_falsifications_held = max(
0,
min(total_falsifications, args.limit) - len(falsification_rows),
) if not args.no_falsifications else 0
sent_roots: list[dict] = []
sent_falsifications: list[dict] = []
errors: list[dict] = []
with MeshWireClient(args.db, args.peer) as client:
try:
peer_info = client.info()
except Exception as e:
print(json.dumps({"status": "peer_unreachable", "peer": args.peer, "error": str(e)}, indent=2, ensure_ascii=False))
return 2
for r in root_rows:
try:
resp = client.announce_root(
document_root=r["document_root"],
source_uri=r["document_uri"],
chunking_version=r["chunking_version"],
canonicalization_version=r["canonicalization_version"],
schema_version=r["schema_version"],
)
sent_roots.append({"document_root": r["document_root"], "ack": resp})
except Exception as e:
errors.append({"document_root": r["document_root"], "error": str(e)})
for f in falsification_rows:
try:
resp = client.announce_falsification(
cache_key=f["cache_key"],
reason=f["reason"] or "",
)
sent_falsifications.append({"cache_key": f["cache_key"], "ack": resp})
except Exception as e:
errors.append({"cache_key": f["cache_key"], "error": str(e)})
print(json.dumps({
"status": "synced",
"peer": args.peer,
"peer_member_id": peer_info.get("member_id"),
"peer_epoch": peer_info.get("current_epoch"),
"kindergarten_seconds": args.kindergarten_seconds,
"announced_roots": len(sent_roots),
"announced_falsifications": len(sent_falsifications),
"kindergarten_held_roots": fresh_roots_held,
"kindergarten_held_falsifications": fresh_falsifications_held,
"errors": len(errors),
"sent_roots": sent_roots[: args.verbose],
"sent_falsifications": sent_falsifications[: args.verbose],
"error_samples": errors[:5],
}, indent=2, ensure_ascii=False))
return 0 if not errors else 1
def _cmd_mesh_pull(args: argparse.Namespace) -> int:
"""Pull a single document body from a peer by document_root.
Closes the request half of the gossip loop. The wire client already
verifies the peer's signature and re-derives the Merkle root from the
delivered leaves before returning. This verb then re-ingests the
delivered text through the standard ingest path so chunking_version /
canonicalization_version stay consistent — and rejects with rc=2 if
the local re-ingest produces a different document_root than requested.
"""
from arborist.document import Document
from arborist.ingest import ingest_source
from arborist.mesh import is_enabled, load_identity
from arborist.mesh.wire import MeshWireClient
conn = connect(args.db)
try:
if load_identity(conn) is None:
print("error: mesh not initialized", file=sys.stderr)
return 2
if not is_enabled(conn):
print("error: mesh.enabled is off", file=sys.stderr)
return 2
already = conn.execute(
"SELECT document_root, document_uri FROM documents WHERE document_root=?",
(args.root,),
).fetchone()
finally:
conn.close()
if already is not None:
print(json.dumps({
"status": "already_present",
"document_root": already["document_root"],
"document_uri": already["document_uri"],
"shard": str(args.db),
}, indent=2, ensure_ascii=False))
return 0
try:
with MeshWireClient(args.db, args.peer) as client:
body = client.request_body(root=args.root)
except Exception as e:
print(f"error: pull failed: {e}", file=sys.stderr)
return 2
delivered_uri = body.get("document_uri") or ""
delivered_text = body.get("text") or ""
class _PulledSource:
source_type = "mesh_pull"
def iter_documents(self):
yield Document(
uri=delivered_uri,
content=delivered_text,
source_type="mesh_pull",
title=None,
)
conn = connect(args.db)
try:
ingest_source(conn, _PulledSource())
row = conn.execute(
"SELECT document_root FROM documents WHERE document_root=?",
(args.root,),
).fetchone()
if row is None:
# Re-ingest produced a different root than the peer claimed.
# The pulled text doesn't reproduce the requested root under
# this peer's chunker/canonicalization. Fail closed.
actual = conn.execute(
"SELECT document_root FROM documents WHERE document_uri=? "
"ORDER BY ingest_ts DESC LIMIT 1",
(delivered_uri,),
).fetchone()
actual_root = actual["document_root"] if actual else None
print(
"error: local re-ingest produced "
f"{actual_root!r}, expected {args.root!r}",
file=sys.stderr,
)
return 2
with transaction(conn):
event_hash = append_audit(
conn,
event_type="mesh_pulled",
body={
"document_root": args.root,
"document_uri": delivered_uri,
"peer": args.peer,
},
subject_root=args.root,
)
finally:
conn.close()
print(json.dumps({
"status": "pulled",
"document_root": args.root,
"document_uri": delivered_uri,
"shard": str(args.db),
"audit_event_hash": event_hash,
}, indent=2, ensure_ascii=False))
return 0
def _cmd_crawl(args: argparse.Namespace) -> int:
"""BFS-discover same-domain URLs from a seed and optionally ingest.
Two modes:
- default: print discovered URLs to stdout (one per line). Compose
with `arborist ingest --source html` if you want to feed them
through the standard ingest path manually.
- ``--ingest``: run the discovery + ingest path in a single shot,
capturing ETag + Last-Modified per page so a future
``crawler recrawl-check`` can do conditional HEADs.
"""
try:
from arborist.sources.crawler.bridge import crawl_seed, ingest_crawled
except ImportError as e:
print(f"error: {e}", file=sys.stderr)
return 2
from arborist.progress import Progress
cap = "no cap" if args.max_pages == 0 else f"max {args.max_pages}"
speed = "fast" if args.fast else "polite"
print(
f" crawl: seed={args.seed_url} depth={args.depth} {cap} ({speed})",
file=sys.stderr,
flush=True,
)
crawl_progress = Progress(prefix="crawl ")
urls = crawl_seed(
args.seed_url,
max_depth=args.depth,
max_pages=args.max_pages,
progress=crawl_progress,
fast=args.fast,
)
print(
f" crawl: discovery done — {len(urls)} URLs",
file=sys.stderr,
flush=True,
)
if not args.ingest:
for u in urls:
print(u)
return 0
print(
f" ingest: starting on {len(urls)} URLs",
file=sys.stderr,
flush=True,
)
ingest_progress = Progress(prefix="ingest ", total_estimate=len(urls))
conn = connect(args.db)
try:
result = ingest_crawled(
conn,
urls,
progress=ingest_progress,
default_author=getattr(args, "author", None),
)
finally:
conn.close()
print(json.dumps(
{
"status": "crawled_and_ingested",
"seed": args.seed_url,
"depth": args.depth,
"max_pages": args.max_pages,
"discovered": len(urls),
**result,
},
indent=2, ensure_ascii=False
))
return 0
def _cmd_crawler_recrawl_check(args: argparse.Namespace) -> int:
"""Send conditional HEAD requests for ingested documents.
Reports each as fresh (304), stale (200, body changed), gone
(404/410), or unreachable. Updates `document_http_meta.last_status`
and `last_checked_at` so consecutive runs target the oldest checks
first.
"""
try:
from arborist.sources.crawler.bridge import recrawl_check
except ImportError as e:
print(f"error: {e}", file=sys.stderr)
return 2
conn = connect(args.db)
try:
result = recrawl_check(
conn,
domain=args.domain,
limit=args.limit,
)
finally:
conn.close()
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="arborist",
description="An arborist for trees and forests of cross-linked information.",
)
p.add_argument("--version", action="version", version=f"arborist {__version__}")
p.add_argument(
"--db",
type=Path,
default=DEFAULT_DB_PATH,
help=f"path to arborist SQLite db (default: {DEFAULT_DB_PATH})",
)
p.add_argument(
"--shards-dir",
dest="global_shards_dir",
default=None,
help=(
"for read commands: attach all shards in this directory and "
"expose them as UNION views over the standard tables"
),
)
sub = p.add_subparsers(dest="cmd", required=True)
# Optional sqlite-vec backend (#000039): only surface --embed /
# `arborist embed` / `search --backend vec` when the [vec] extra
# is installed.
try:
from arborist.search import VEC_AVAILABLE as _vec_ok
except Exception: # pragma: no cover
_vec_ok = False
ingest = sub.add_parser("ingest", help="ingest documents from a source")
ingest.add_argument(
"--source",
required=True,
choices=[
"wikipedia_cur",
"wikipedia_old",
"wikipedia_xml",
"wikipedia_xml_history",
"wikipedia_abstract",
"html",
"grok_export",
"grok_media",
"git_repo",
"hg_repo",
"providence",
"claim_pack",
"textbook_tex",
],
help="source type",
)
ingest.add_argument(
"--kindergarten-seconds",
type=int,
default=None,
help=(
"(providence source only) records younger than this many "
"seconds stay opaque to ingestion — fresh thoughts cool "
"before they become substrate. Default 3600s (1h)."
),
)
ingest.add_argument(
"--path",
help=(
"path to dump file (wikipedia) or to xAI export root / "
"prod-grok-backend.json (grok_export, grok_media)"
),
)
ingest.add_argument(
"--url", action="append", help="URL to ingest (html source; repeatable)"
)
ingest.add_argument(
"--bundle",
action="append",
help=(
"JSON bundle path (claim_pack source; repeatable). Provide axiom "
"and theorem bundles together so cross-bundle pillar_reference "
"edges resolve to specific record URIs."
),
)
ingest.add_argument(
"--urls-from",
dest="urls_from",
help="file with one URL per line (html source)",
)
ingest.add_argument(
"--no-robots",
dest="no_robots",
action="store_true",
help="do not consult robots.txt (use only for explicitly authorized sites)",
)
ingest.add_argument(
"--author",
default=None,
help=(
"default author surname for the html / textbook_tex sources "
"(#000031 Phase 1 follow-up). Appended to document title as "
"'<title>, by <author>' so the warrant resolver's "
"_shard_matches_citation heuristic finds the surname in "
"the title haystack. Wikisource and PG HTML pages rarely "
"include author in <title>; the manifest carries it instead."
),
)
ingest.add_argument(
"--chunker", default=None, help="chunker name (default: tok-512-v1)"
)
ingest.add_argument(
"--limit", type=int, default=None, help="cap number of documents"
)
ingest.add_argument(
"--batch-size",
dest="batch_size",
type=int,
default=200,
help="documents per SQLite transaction (default 200)",
)
ingest.add_argument(
"--shard",
default=None,
help=(
"rank/total — yield only every N-th doc for parallel ingest. "
"spawn N processes, each with --shard 0/N, 1/N, ... they "
"parallelize parser CPU and serialize writes via WAL"
),
)
ingest.add_argument(
"--shards-dir",
dest="shards_dir",
default=None,
help=(
"directory for attach-forever sharding. With --shard rank/total, "
"writes to shards-dir/<rank>.db instead of --db, removing the "
"WAL writer-lock contention entirely. Reads via arborist --shards-dir"
),
)
ingest.add_argument(
"--resume",
action="store_true",
help=(
"rsync-style: read each source's last high-water mark from this "
"DB's meta table and skip rows whose id is <= it. Safe to kill "
"and restart at any time"
),
)
ingest.add_argument(
"--quiet",
action="store_true",
help="suppress periodic stderr progress output",
)
ingest.add_argument(
"--progress-interval",
dest="progress_interval",
type=float,
default=2.0,
help="seconds between stderr progress lines (default 2.0)",
)
ingest.add_argument(
"--total-estimate",
dest="total_estimate",
type=int,
default=None,
help=(
"estimated total docs the source will yield. enables percent "
"+ ETA in progress output"
),
)
ingest.add_argument(
"--no-loss-report",
dest="no_loss_report",
action="store_true",
help=(
"disable adapter LossReport sidecar (ticket #000022). "
"Default: enabled. Sidecar — toggling does NOT invalidate "
"QA cache_keys"
),
)
ingest.add_argument(
"--no-loss-excerpts",
dest="no_loss_excerpts",
action="store_true",
help=(
"drop sample_excerpt content from LossReport rows "
"(PII-paranoid mode). sample_hash stays populated"
),
)
ingest.add_argument(
"--loss-excerpt-bytes",
dest="loss_excerpt_bytes",
type=int,
default=200,
help="max excerpt byte length (default 200)",
)
if _vec_ok:
ingest.add_argument(
"--embed", action="store_true",
help=(
"after ingest, embed the chunks this run added (incremental "
"chunk_vecs population — #000039; default ingest does NOT "
"embed, the lazy `arborist embed` pass / cron / Prometheus-Σ "
"sweep is the usual path)"
),
)
ingest.set_defaults(func=_cmd_ingest)
search = sub.add_parser("search", help="keyword/semantic search (UNGROUNDED audit mode)")
search.add_argument("query", help="query string")
search.add_argument("--limit", type=int, default=20)
search.add_argument("--json", action="store_true", help="output JSON")
_search_backends = ["fts5"] + (["vec"] if _vec_ok else [])
search.add_argument(
"--backend", choices=_search_backends, default="fts5",
help=(
"retrieval backend: 'fts5' (BM25 lexical, default) or 'vec' "
"(semantic ANN over chunk_vecs — requires [vec] extra + a populated "
"shard via `arborist embed`)" if "vec" in _search_backends
else "retrieval backend: 'fts5' (BM25 lexical; 'vec' needs the [vec] extra)"
),
)
search.set_defaults(func=_cmd_search)
if "vec" in _search_backends:
embed_cmd = sub.add_parser(
"embed",
help="populate chunk_vecs (semantic embeddings) for --db [vec extra]",
)
embed_cmd.add_argument(
"--limit", type=int, default=None,
help="cap the number of chunks embedded (smoke-test on a real shard)",
)
embed_cmd.add_argument("--batch-size", type=int, default=256)
embed_cmd.add_argument(
"--quant", choices=["float32", "int8"], default="float32",
help=(
"vector quantization: float32 (default — +25%% corpus tax, max "
"fidelity) or int8 (~4x smaller, +6%% tax, ~1-3%% recall hit — "
"the recommended production default; switching quant on an "
"existing chunk_vecs requires --rebuild)"
),
)
embed_cmd.add_argument(
"--rebuild", action="store_true",
help=(
"DROP + recreate chunk_vecs (at --quant), then full re-embed — "
"the clean version-bump path and the only way to switch quant "
"(default is incremental: embed only chunks not already in "
"chunk_vecs)"
),
)
embed_cmd.set_defaults(func=_cmd_embed)
verify = sub.add_parser(
"verify", help="round-trip Merkle proofs for N random documents"
)
verify.add_argument("-n", type=int, default=10)
verify.set_defaults(func=_cmd_verify)
distill = sub.add_parser(
"distill",
help="compress docs into Merkle-signed cores (surface->core or core->core)",
)
distill.add_argument(
"--process", default="first-sentence-v1", help="distiller name"
)
distill.add_argument(
"--kind",
choices=["surface", "core"],
default="surface",
help="source kind to scan; 'core' runs recursive distillation",
)
distill.add_argument(
"--source-type",
dest="source_type",
default=None,
help="restrict to one source_type",
)
distill.add_argument(
"--chunker", default=None, help="chunker for the core doc"
)
distill.add_argument(
"--limit", type=int, default=None, help="cap number of docs scanned"
)
distill.add_argument(
"--batch-size",
dest="batch_size",
type=int,
default=200,
help="cores written per SQLite transaction (default 200)",
)
distill.set_defaults(func=_cmd_distill)
ask_cmd = sub.add_parser(
"ask",
help="answer a question about a document (cache-first, STRICT)",
)
ask_cmd.add_argument(
"--document-root",
dest="document_root",
required=True,
help="document_root to ask about",
)
ask_cmd.add_argument(
"--question", required=True, help="question text"
)
ask_cmd.add_argument(
"--model",
default=None,
help="model_id (default $ARBORIST_LLM_MODEL or hermes-3)",
)
ask_cmd.add_argument(
"--endpoint",
default=None,
help="OpenAI-compatible base URL (default $ARBORIST_LLM_ENDPOINT)",
)
ask_cmd.add_argument(
"--dry-run",
dest="dry_run",
action="store_true",
help="use StubClient — no network call",
)
ask_cmd.add_argument(
"--answer-mode", dest="answer_mode", default=None,
choices=["quote", "claim_lattice_pointer", "claim_lattice"],
help=(
"answer schema. See `query --answer-mode` for full semantics. "
"Default 'quote'; 'claim_lattice_pointer' enables quote-by-pointer; "
"'claim_lattice' is the JSON variant (vLLM guided_json + lenient "
"pre-parser; pairs with grammar-constrained inference)."
),
)
ask_cmd.add_argument(
"--user-payload-layout", dest="user_payload_layout", default=None,
choices=["tail", "bookend", "per_chunk"],
help=(
"where the question text appears in the final user message. "
"See `query --user-payload-layout` for full semantics."
),
)
ask_cmd.add_argument(
"--demote-on-missed-answer",
dest="demote_on_missed_answer",
action="store_true",
default=False,
help=(
"Ticket #000068 Phase 3 — opt-in missed-answer demote. "
"See `query --demote-on-missed-answer` for semantics."
),
)
ask_cmd.set_defaults(func=_cmd_ask)
query_cmd = sub.add_parser(
"query",
help="multi-source RAG: question -> top-K corpus docs -> Hermes -> cache",
)
query_cmd.add_argument("question", help="the question to ask")
query_cmd.add_argument(
"--top-k", dest="top_k", type=int, default=8,
help="max distinct source documents in context (default 8)",
)
query_cmd.add_argument(
"--over-fetch", dest="over_fetch", type=int, default=32,
help="FTS5 hits to fetch per shard before dedup (default 32)",
)
query_cmd.add_argument(
"--include-shard", dest="include_shard", action="append", default=None,
metavar="PATH",
help=(
"extra shard db file(s) to search alongside the main corpus "
"(repeatable). Per-shard fan-out, so no 10-attach limit here. "
"The local crawl db (ARBORIST_CRAWL_DB, default "
"~/.arborist/crawl/web.db) is auto-included when present."
),
)
query_cmd.add_argument(
"--no-crawl-db", dest="no_crawl_db", action="store_true",
help=(
"do not auto-include the local crawl db; query only the main "
"corpus (--shards-dir / --db)."
),
)
query_cmd.add_argument(
"--max-context-chars", dest="max_context_chars", type=int, default=None,
help=(
"cap on assembled context bytes. When omitted, falls back to "
"the per-mode default in DEFAULT_QUERY_POLICY['max_context_chars_by_mode'] "
"(quote=24000, claim_lattice_pointer=24000, claim_lattice=48000). "
"Sprint 1b 2026-05-02 — peaks measured per mode."
),
)
query_cmd.add_argument(
"--question-dedup", dest="question_dedup", default=None,
choices=["strict", "equivalence_class"],
help=(
"write-time question canonicalization. 'equivalence_class' "
"(default) collapses articles + trailing-punct + case so "
"variants share cache_keys. 'strict' keeps every variant "
"distinct (audit-grade)."
),
)
query_cmd.add_argument(
"--fidelity", dest="fidelity", default=None,
choices=["strict", "equivalence_class"],
help=(
"lookup tolerance. 'equivalence_class' (default) tries the "
"primary cache_key then falls back to the alternate dedup "
"mode's cache_key. 'strict' refuses fallback."
),
)
query_cmd.add_argument(
"--qa-db", dest="qa_db", default=None,
help=(
"providence_cache target DB. default: <shards-dir>/qa.db, or "
"~/.arborist/qa.db when no shards-dir"
),
)
query_cmd.add_argument(
"--model", default=None,
help="model_id (default $ARBORIST_LLM_MODEL or hermes-3)",
)
query_cmd.add_argument(
"--endpoint", default=None,
help="OpenAI-compatible base URL (default $ARBORIST_LLM_ENDPOINT)",
)
query_cmd.add_argument(
"--dry-run", dest="dry_run", action="store_true",
help="use StubClient — assembles context but skips the LLM call",
)
query_cmd.add_argument(
"--json", action="store_true",
help="emit the raw record as indented JSON (default: human render)",
)
query_cmd.add_argument(
"--burn", action="store_true",
help=(
"delete the matching live providence_cache row BEFORE lookup, "
"forcing a fresh inference. Test-ergonomic — see new behavior "
"without finding cache_keys by hand. Writes a providence_burn "
"audit event."
),
)
query_cmd.add_argument(
"--repair", action="store_true",
help=(
"enable mechanical repair after first verify (off by "
"default). When the verdict is HYBRID/UNGROUNDED, applies "
"synthetic_elision split, trailing_artifact trim, and "
"no_overlap drop deterministically; persists the repaired "
"answer with a providence_repair audit event."
),
)
query_cmd.add_argument(
"--repair-reprompts", dest="repair_reprompts", type=int, default=0,
help=(
"max LLM re-prompt iterations after mechanical repair "
"(default 0 = no re-prompt). Each iteration sends a feedback "
"turn naming failed quotes; the model is asked to rewrite "
"using only verbatim citations. Requires --repair."
),
)
query_cmd.add_argument(
"--answer-mode", dest="answer_mode", default=None,
choices=["quote", "claim_lattice_pointer", "claim_lattice"],
help=(
"answer schema. 'quote' (default): model writes prose with "
"verbatim quote spans inline. 'claim_lattice_pointer' (G0 "
"/ CTI quote-by-pointer): runtime builds a labeled evidence "
"map (E1, E2, …); model writes pointer-line prose ('Claim. "
"[E12]'); renderer interpolates literal spans. Synthetic-"
"elision-by-construction-impossible. No repair loop "
"(one-shot discipline). 'claim_lattice' is the JSON variant "
"(vLLM guided_json + lenient pre-parser; pairs with grammar-"
"constrained inference like Qwen 3.6 reasoner / Claude / GPT-4)."
),
)
query_cmd.add_argument(
"--user-payload-layout", dest="user_payload_layout", default=None,
choices=["tail", "bookend", "per_chunk"],
help=(
"where the question text appears in the final user message. "
"'tail' (default, preserves prior cache): question after "
"evidence only. 'bookend': question repeated before AND "
"after evidence — counters lost-in-the-middle on small "
"models (Hermes-3-8B) with long contexts. 'per_chunk': "
"bookend + a one-line `[for: <question>]` reminder before "
"each evidence block. Folds into governance_policy_hash."
),
)
query_cmd.add_argument(
"--demote-on-missed-answer",
dest="demote_on_missed_answer",
action="store_true",
default=False,
help=(
"Ticket #000068 Phase 3 — opt-in demote of EVIDENCE-WARRANTED "
"→ EVIDENCE-MISSED-PARTIAL when the missed-answer sidecar "
"fires at strong/medium confidence. Default off. Folds into "
"verifier_policy_hash (changes the rendered audit_mode); "
"flipping it invalidates prior cached records on lookup. "
"Use after reviewing Phase 2 bench evidence — see "
"docs/tickets/ticket-000068-*.md."
),
)
query_cmd.add_argument(
"--retrieval-keywords", dest="retrieval_keywords", default=None,
help=(
"operator-supplied keywords appended to the question for "
"FTS5 retrieval ONLY — never sent to the LLM, never enters "
"cache_key, never reaches the verifier. Use to narrow "
"OR-mode retrieval on long discursive questions whose "
"content tokens get diluted by template phrasing. Example: "
"make query Q='what tech may enable one person to "
"reconstruct another person's thoughts...' "
"K='transcranial knowledge acquisition'. Pair with --burn "
"to force fresh inference (keywords are session-only and "
"cache-hits ignore them)."
),
)
# Ticket #000008 Phase 4 — quantifier-guard CLI flags. Each
# corresponds to a level of the §10.11.2 disable hierarchy.
query_cmd.add_argument(
"--no-quantifier-guard",
dest="no_quantifier_guard", action="store_true",
help=(
"Disable the broad-quantifier preflight guard for this "
"call. Overrides quantifier_guard_enabled in policy. "
"Bench-side telemetry (quantifier_intensity, etc.) goes "
"to None for the row. Use when the guard misclassifies."
),
)
query_cmd.add_argument(
"--allow-broad",
dest="allow_broad", action="store_true",
help=(
"Emergent-search mode: keep the classifier on (telemetry "
"stays useful) but don't apply caps. For broad questions "
"where the operator wants exploratory enumeration, not "
"grounded completeness."
),
)
query_cmd.add_argument(
"--reject-broad",
dest="reject_broad", action="store_true",
help=(
"Strict mode: when intensity is ALL/COMPREHENSIVE/"
"OPEN_REQUEST AND scope_bound_hint is unbounded, return "
"UNGROUNDED before the LLM call with a "
"BROAD_QUANTIFIER_REJECTED violation. Saves ~10-15s on "
"rejected runs. Bounded universals (e.g. all members of "
"the Beatles) are NOT rejected."
),
)
query_cmd.add_argument(
"--apply-quantifier-caps",
dest="apply_quantifier_caps", action="store_true",
help=(
"Flip the dry-run gate per-call. By default Phase 2 "
"lands with quantifier_guard_apply_caps=False so the "
"cap is reported on the result but not applied to the "
"verifier. This flag enables actual cap enforcement "
"for one call. Use after dry-run bench review confirms "
"the classifier output across the question set."
),
)
# Ticket #000001 §7 Phase 0 — cross-language guard (default OFF;
# dry-run rollout discipline). Enable per-call to experiment.
query_cmd.add_argument(
"--crosslang-guard",
dest="crosslang_guard", action="store_true",
help=(
"Enable the deterministic cross-language guard for this "
"call (ticket #000001 §7 Phase 0, default OFF). A query "
"with a non-English signal (¿/¡/non-ASCII letter) has "
"es-v1 function words stripped from the retrieval set so "
"they can't drive an OR-mode full-corpus FTS5 scan; if no "
"corpus-language content token survives, returns "
"UNGROUNDED before retrieval/LLM with a "
"CROSS_LANGUAGE_UNSUPPORTED violation. Retrieval-side "
"only — never touches the verifier / cache_key / "
"question_hash. Provably inert on English."
),
)
query_cmd.add_argument(
"--crosslang-translate",
dest="crosslang_translate", action="store_true",
help=(
"Operation Sandwich (ticket #000056, default OFF; implies "
"--crosslang-guard). When the non-English signal fires, "
"translate the query es→en (local [mt] opus-mt; the "
"English article ranks primary AND the LLM is prompted in "
"English), ground the English answer with the UNTOUCHED "
"verifier, then render the verified English answer back "
"es as DISPLAY-ONLY (banner-labelled, zero grounding). "
"Needs the [mt] extra; degrades to the Phase-0 guard if "
"absent. Never touches the verifier / cache_key / "
"question_hash / governance_policy_hash."
),
)
# Ticket #000010 — meta-cognition CLI flags.
query_cmd.add_argument(
"--no-preflight",
dest="no_preflight", action="store_true",
help=(
"Disable the meta-cognition preflight guard for this "
"call. Skips temporal / contradiction / false-premise / "
"out-of-corpus detectors. The QuestionState surfaces a "
"stub with empty logical_statuses so bench rows stay "
"column-aligned."
),
)
query_cmd.add_argument(
"--block-on-contradiction",
dest="block_on_contradiction", action="store_true",
help=(
"Hard-block on lexical contradictions (default: label-"
"only). Strict mode: questions like 'which unmarried "
"spouse is X married to' return PREFLIGHT_BLOCKED."
),
)
query_cmd.add_argument(
"--soft-preflight",
dest="soft_preflight", action="store_true",
help=(
"Ticket #000011 — opt-in to the model-assisted soft "
"preflight sidecar. Adds one short LLM round-trip "
"(~200ms median) before the main answer call; the model "
"classifies the question shape and returns a SOFT_* "
"advisory hint that surfaces as `· soft: <label>` on "
"the audit-line tail. NEVER enters the verifier proof "
"path; cannot create PREFLIGHT_OK or PREFLIGHT_BLOCKED."
),
)
query_cmd.add_argument(
"--no-canonical-preflight",
dest="no_canonical_preflight", action="store_true",
help=(
"Disable the math/logic π* short-circuit. Pure-arithmetic "
"(e.g. '0.1 + 0.2') and pure-propositional ('A IMPL B') "
"questions normally bypass RAG and answer via "
"arithmetic@v1 / logic-kernel@v1 directly. Pass this "
"flag to force RAG against the LLM (useful when bench-"
"comparing the canonical answer with the model's reply)."
),
)
witness_group = query_cmd.add_mutually_exclusive_group()
witness_group.add_argument(
"--witness", dest="witness_override", action="store_const",
const="on",
help=(
"Ticket #000028 — multi-modality witness. On canonical-shape "
"questions (arithmetic@v1, logic-kernel@v1, future kernels), "
"fan out kernel + cache + LLM in parallel and verify cross-"
"modality agreement via kernel-canonicalization. Adds one LLM "
"call (~2-5s) on top of the canonical fast path; default OFF "
"to keep arithmetic queries at ~10ms."
),
)
witness_group.add_argument(
"--no-witness", dest="witness_override", action="store_const",
const="off",
help="Disable the witness path even when policy enables it.",
)
progress_group = query_cmd.add_mutually_exclusive_group()
progress_group.add_argument(
"--progress", dest="progress_override", action="store_const",
const="on",
help=(
"Force per-stage state-machine emission to stderr (per-shard "
"× per-route search lines, llm.start/done, verify, persist, "
"etc.). Default: on at TTY, off when stderr is piped. "
"Format: [arborist NN.NNs] stage.name key=value …"
),
)
progress_group.add_argument(
"--no-progress", dest="progress_override", action="store_const",
const="off",
help=(
"Suppress state-machine emission even when stderr is a TTY. "
"Use when running interactively but piping output through "
"tools that misbehave on stderr noise."
),
)
query_cmd.set_defaults(
func=_cmd_query, progress_override=None, witness_override=None,
)
inspect_cmd = sub.add_parser(
"inspect",
help=(
"sidecar diagnostic for a providence_cache record — pulls "
"source chunks and classifies each unverified span "
"(paraphrase / trailing_artifact / interior_elision / "
"synthetic_elision_inside_quote / "
"no_overlap). Read-only, "
"no audit events, no v9.8 field changes."
),
)
inspect_cmd.add_argument(
"--cache-key", dest="cache_key", required=True,
help="64-char hex cache_key of the providence record to inspect",
)
inspect_cmd.add_argument(
"--qa-db", dest="qa_db", default=None,
help="path to qa.db (default: <shards>/qa.db or ~/.arborist/qa.db)",
)
inspect_cmd.add_argument(
"--json", action="store_true",
help="emit raw diagnosis as JSON (default: human render)",
)
inspect_cmd.set_defaults(func=_cmd_inspect)
losses_cmd = sub.add_parser(
"losses",
help=(
"list adapter LossReport sidecar rows (ticket #000022). "
"Read-only; never enters proof path"
),
)
losses_cmd.add_argument(
"--document-root", dest="document_root", default=None,
help="filter to one document_root (hex)",
)
losses_cmd.add_argument(
"--chunk-id", dest="chunk_id", type=int, default=None,
help="filter to one chunk_id",
)
losses_cmd.add_argument(
"--kind", default=None,
help="filter to one loss_kind (ref_tag, file_link, html_chrome, ...)",
)
losses_cmd.add_argument(
"--stage", default=None,
help="filter to one stage (wikitext_base, html_normalize, ingest)",
)
losses_cmd.add_argument(
"--summary", action="store_true",
help="aggregate by (stage, loss_kind, loss_mode)",
)
losses_cmd.add_argument(
"--limit", type=int, default=200,
help="row cap for non-summary mode (default 200)",
)
losses_cmd.add_argument(
"--json", action="store_true",
help="emit raw rows as JSON (default: human render)",
)
losses_cmd.set_defaults(func=_cmd_losses)
prov_cmd = sub.add_parser(
"providence",
help="list or falsify providence_cache records",
)
prov_cmd.add_argument("--document-uri", dest="document_uri", default=None)
prov_cmd.add_argument("--source-root", dest="source_root", default=None)
prov_cmd.add_argument("--limit", type=int, default=20)
prov_cmd.add_argument(
"--falsify",
default=None,
metavar="CACHE_KEY",
help=(
"mark a providence_cache record as failed/stale/quarantined. "
"Lookups will skip it. Audit chain records the act"
),
)
prov_cmd.add_argument(
"--state",
default="failed",
choices=["failed", "stale", "quarantined"],
help="falsification state to set (default: failed)",
)
prov_cmd.add_argument(
"--reason",
default=None,
help="reason text stored in falsifications log",
)
prov_cmd.add_argument(
"--by-actor",
dest="by_actor",
default=None,
help="who is falsifying (default: $USER)",
)
prov_cmd.add_argument(
"--show-preflight",
dest="show_preflight",
default=None,
metavar="CACHE_KEY_PREFIX",
help=(
"Pull the preflight stage payload from a row's "
"run_dag_blob. Match by 12-char prefix. Renders the "
"preflight stage hash + run-DAG stage list. Operator "
"tool for inspecting the policy state that governed a "
"cached row (#000009 §7.2)."
),
)
prov_cmd.set_defaults(func=_cmd_providence)
ce_cmd = sub.add_parser(
"controller-events",
help=(
"list controller_events advisory rows from #000037 Phase 2 "
"(QA-runner → controller decision/difficulty/budget rows)"
),
)
ce_cmd.add_argument("--limit", type=int, default=20)
ce_cmd.add_argument(
"--kind",
choices=[
"controller_decision",
"controller_difficulty",
"controller_budget_allocation",
"controller_falsification_proposal",
],
default=None,
help="filter to one event_kind",
)
ce_cmd.add_argument(
"--organism-prefix",
dest="organism_prefix",
default=None,
help='match organism_root LIKE prefix (e.g. "qa:" for QA-runner advisories)',
)
ce_cmd.add_argument(
"--since-seconds",
dest="since_seconds",
type=int,
default=None,
help="only rows recorded within the last N seconds",
)
ce_cmd.add_argument(
"--body",
action="store_true",
help="include the JSON body_blob in JSON output (off by default — bodies are bulky)",
)
ce_cmd.add_argument(
"--json",
action="store_true",
help="emit JSON instead of a terminal table",
)
ce_cmd.set_defaults(func=_cmd_controller_events)
burn_cmd = sub.add_parser(
"burn",
help=(
"delete a leaf with no children — providence_cache, document, "
"or core (kindergarten use; falsify/evict are audit-preserving)"
),
)
burn_cmd.add_argument(
"--kind",
choices=("providence", "document", "core"),
default="providence",
help="leaf kind to burn (default: providence — backwards-compatible)",
)
burn_cmd.add_argument(
"--cache-key",
dest="cache_key",
default=None,
help="cache_key (hex) of the providence record to burn (kind=providence)",
)
burn_cmd.add_argument(
"--root",
dest="root",
default=None,
help="document_root (hex) of the document/core to burn (kind=document|core)",
)
burn_cmd.add_argument(
"--reason",
default=None,
help="reason text recorded in the burn audit event",
)
burn_cmd.add_argument(
"--by-actor",
dest="by_actor",
default=None,
help="who is burning (default: $USER)",
)
burn_cmd.add_argument(
"--force",
action="store_true",
help="burn even if children exist; not recommended",
)
burn_cmd.set_defaults(func=_cmd_burn)
burn_kg_cmd = sub.add_parser(
"burn-kindergarten",
help=(
"burn every providence_cache record younger than the "
"kindergarten window — test-ergonomic mass cleanup that "
"matches the mesh-sync kindergarten window so only "
"un-broadcast records get busted"
),
)
burn_kg_cmd.add_argument(
"--kindergarten-seconds",
dest="kindergarten_seconds",
type=int,
default=3600,
help=(
"burn rows younger than this many seconds (default: 3600 = 1 "
"hour, mirrors mesh sync default). 0 = burn everything live."
),
)
burn_kg_cmd.add_argument(
"--reason", default=None,
help="reason text recorded in each providence_burn audit event",
)
burn_kg_cmd.add_argument(
"--by-actor", dest="by_actor", default=None,
help="who is burning (default: $USER)",
)
burn_kg_cmd.add_argument(
"--force", action="store_true",
help="burn even if rows have falsification children",
)
burn_kg_cmd.add_argument(
"--dry-run", dest="dry_run", action="store_true",
help="report what would burn without writing",
)
burn_kg_cmd.add_argument(
"--verbose", type=int, default=10,
help="include this many items in the result JSON (default: 10)",
)
burn_kg_cmd.set_defaults(func=_cmd_burn_kindergarten)
reclassify_cmd = sub.add_parser(
"reclassify",
help="re-run the verifier against existing live providence records "
"(no LLM calls; relabels stale classifications)",
)
reclassify_cmd.add_argument(
"--qa-db", dest="qa_db", default=None,
help="path to qa.db (default: <shards>/qa.db or ~/.arborist/qa.db)",
)
reclassify_cmd.add_argument(
"--limit", type=int, default=0,
help="reclassify at most N records (0 = unlimited)",
)
reclassify_cmd.add_argument(
"--dry-run", dest="dry_run", action="store_true",
help="report what would change without writing",
)
reclassify_cmd.add_argument(
"--entity-policy", dest="entity_policy", default=None,
choices=["strict", "hybrid", "drop", "proximity"],
help=(
"how the entity path classifies: 'strict' (legacy, overclaims), "
"'hybrid' (default — caps at HYBRID), 'drop' (skip entity path → "
"UNGROUNDED), 'proximity' (STRICT only if N entities cluster within "
"W chars in source)"
),
)
reclassify_cmd.add_argument(
"--compare", dest="compare", action="store_true",
help="run all four entity policies side-by-side without writing",
)
reclassify_cmd.set_defaults(func=_cmd_reclassify)
emergent_cmd = sub.add_parser(
"emergent",
help="surface UNGROUNDED/HYBRID claims — corpus-growth signal",
)
emergent_cmd.add_argument(
"--aggregate",
action="store_true",
help="rank unverified quotes by frequency (vs per-record list)",
)
emergent_cmd.add_argument("--limit", type=int, default=20)
emergent_cmd.set_defaults(func=_cmd_emergent)
evict_cmd = sub.add_parser(
"evict",
help="demote surface chunks hot→cold (NULL content, retain leaf_hash)",
)
evict_cmd.add_argument(
"--source-type",
dest="source_type",
default=None,
help="restrict to one source_type",
)
evict_cmd.add_argument(
"--older-than-days",
dest="older_than_days",
type=int,
default=None,
help="only evict docs older than N days",
)
evict_cmd.add_argument(
"--document-root",
action="append",
default=None,
help="explicit document_root(s) to evict; repeatable",
)
evict_cmd.set_defaults(func=_cmd_evict)
rehydrate_cmd = sub.add_parser(
"rehydrate",
help="refetch URI, verify leaves, restore cold content if root matches",
)
rehydrate_cmd.add_argument(
"--document-root",
action="append",
default=None,
help="explicit document_root(s) to rehydrate; repeatable",
)
rehydrate_cmd.add_argument(
"--all-cold",
dest="all_cold",
action="store_true",
help="rehydrate every document with cold chunks",
)
rehydrate_cmd.set_defaults(func=_cmd_rehydrate)
# --- cold-pack distribution tier (#000061): tarballs in S3, hydrate
# new peers from empty via a small number of HTTPS GETs.
cold_cmd = sub.add_parser(
"cold",
help="build/distribute tar.zst corpus packs via an S3-compatible bucket (DO Spaces, AWS S3, R2, B2, GCS, MinIO) or local disk for DVD burning",
)
cold_sub = cold_cmd.add_subparsers(dest="cold_subcommand", required=True)
cold_pack = cold_sub.add_parser(
"pack",
help=(
"bundle hot chunks into tar.zst pack(s), ≤4.4 GB DVD-R safe-fit "
"per pack by default — push to bucket and/or write locally for burning"
),
)
cold_pack.add_argument("--document-root", default=None)
cold_pack.add_argument(
"--max-chunks", type=int, default=0,
help="optional chunk-count cap per pack (0 = unlimited; the byte cap "
"is the primary control)",
)
cold_pack.add_argument(
"--max-pack-bytes", dest="max_pack_bytes", type=int,
default=4_400_000_000,
help="compressed-bytes cap per pack — streaming zstd peeks the "
"compressed buffer after each chunk and finalizes when ≥ cap, so "
"each pack fills the disc. Default 4_400_000_000 = DVD-R safe-fit "
"(4.4 GB, ~6.5%% buffer below the 4.7 GB media spec to absorb "
"ISO9660 overhead, media variance, and drive-edge refusal). "
"BD-R = 24_000_000_000; BD-R DL = 48_000_000_000.",
)
cold_pack.add_argument(
"--local-dir", dest="local_dir", default=None,
help="write each pack to this directory as arborist-pack-<hash>.tar.zst "
"(+ manifest sidecar) for burning to physical media. Independent of "
"--no-push.",
)
cold_pack.add_argument(
"--no-push", dest="push_to_bucket", action="store_false",
help="skip the bucket upload — only write locally via --local-dir",
)
cold_pack.add_argument(
"--allow-license-class",
dest="allow_license_class",
default="public_redistributable",
choices=["public_redistributable", "unknown", "private"],
help="strictest license_class arborist will push to the bucket. "
"Default refuses unknown/private shards on a public bucket "
"(Gap 2 per #000061 review). Override only after confirming the "
"bucket ACL + source licensing permit redistribution.",
)
cold_pack.add_argument(
"--no-fts", dest="include_fts", action="store_false",
help="(default) skip the FTS5 shadow-table pack. The cold-pack FTS "
"restore produces a dead index (rows present, MATCH=0; 2026-05-29) "
"and FTS is derivable from content, so consumers rebuild it with "
"`cold rebuild-fts` after unpack.",
)
cold_pack.add_argument(
"--with-fts", dest="include_fts", action="store_true",
help="ship the FTS5 shadow-table pack anyway (bigger bucket; the "
"restored index is currently non-functional — not recommended).",
)
cold_pack.add_argument(
"--jit-blobs", dest="jit_blobs", action="store_true",
help="upload each chunk as a content-addressed blob at "
"`blobs/<hash[:2]>/<hash[2:]>` and skip the batched chunk-pack "
"phase. Targets the online JIT consumer flow: consumer pulls only "
"the small metadata pack (`cold unpack --mode just-enough`), then "
"queries with `ARBORIST_JIT_CHUNKS=1` fetch single chunks on "
"demand. Mutually exclusive with the DVD/burn workflow.",
)
cold_pack.add_argument(
"--jit-blobs-workers", dest="jit_blobs_workers", type=int, default=16,
help="concurrent blob-upload workers when --jit-blobs is set "
"(default 16; PUT throughput is the bottleneck).",
)
cold_pack.set_defaults(
func=_cmd_cold_pack, push_to_bucket=True, include_fts=False,
jit_blobs=False,
)
cold_unpack = cold_sub.add_parser(
"unpack",
help=(
"hydrate a shard from a metadata pack hash. default is "
"--just-enough (metadata only, chunks NULL); --full also "
"pulls every referenced chunk pack."
),
)
cold_unpack.add_argument(
"pack_hash",
help="metadata pack hash (NOT a chunk pack hash). Find via `cold list`.",
)
cold_unpack.add_argument(
"--full",
dest="mode",
action="store_const",
const="full",
default="just-enough",
help="after metadata, pull every chunk pack the metadata references",
)
cold_unpack.add_argument(
"--just-enough",
dest="mode",
action="store_const",
const="just-enough",
help="(default) metadata only — chunks remain NULL for JIT fetch",
)
cold_unpack.add_argument(
"--hydrate-shards-dir", dest="hydrate_shards_dir", default=None,
help="(M-aware genesis, #000067) hydrate into M target shards "
"under this directory; each incoming row routes by "
"shard_for_document(root, M). Mutually exclusive with --db / "
"--global-shards-dir.",
)
cold_unpack.add_argument(
"--hydrate-M", dest="hydrate_M", type=int, default=4,
help="number of target shards for --hydrate-shards-dir routing "
"(default 4 matches #000065 canonical M).",
)
cold_unpack.set_defaults(func=_cmd_cold_unpack)
cold_stats = cold_sub.add_parser(
"stats",
help="bucket summary: pack count, total bytes, backend identity (no credentials)",
)
cold_stats.set_defaults(func=_cmd_cold_stats)
cold_rebuild_fts = cold_sub.add_parser(
"rebuild-fts",
help=(
"rebuild chunks_fts + documents_fts from content on every shard "
"in SHARDS_DIR, in parallel (one process per shard). Clears-first, "
"so it also repairs the dead index a cold-pack FTS restore leaves."
),
)
cold_rebuild_fts.add_argument(
"--shards-dir", dest="shards_dir", required=True,
help="directory of shards to FTS-rebuild",
)
cold_rebuild_fts.add_argument(
"--workers", dest="workers", type=int, default=0,
help="parallel workers (default 0 = one per shard)",
)
cold_rebuild_fts.set_defaults(func=_cmd_cold_rebuild_fts)
cold_verify = cold_sub.add_parser(
"verify",
help=(
"self-check a hydrated shard set: chunk content materialized "
"(not zero-filled) AND FTS searchable. Non-zero exit if any "
"shard fails — so a bad recovery fails loudly."
),
)
cold_verify.add_argument(
"--shards-dir", dest="shards_dir", required=True,
help="directory of shards to verify",
)
cold_verify.add_argument(
"--sample", dest="sample", type=int, default=20,
help="chunks sampled per shard for the content check (default 20)",
)
cold_verify.set_defaults(func=_cmd_cold_verify)
cold_stream_snapshot = cold_sub.add_parser(
"stream-snapshot",
help=(
"live-snapshot producer: SQLite Backup API each shard to a "
"raw .db, multipart-upload to bucket, swap CURRENT. "
"--just-enough strips chunks.content into per-chunk blobs "
"(Tier B, for SPV / mobile peers)."
),
)
cold_stream_snapshot.add_argument(
"--shards-dir", dest="shards_dir", required=True,
help="directory of shards to snapshot",
)
cold_stream_snapshot.add_argument(
"--just-enough", dest="just_enough", action="store_true",
help="Tier B: strip chunks.content into blobs/<hash>; ship metadata-only shards",
)
cold_stream_snapshot.add_argument(
"--snapshot-id", dest="snapshot_id", default=None,
help="pin a specific snapshot id (default: snap-<unix_ts>)",
)
cold_stream_snapshot.add_argument(
"--workers", dest="workers", type=int, default=0,
help="parallel shards (default 0 = one per shard; each worker "
"still has 10-way multipart concurrency internally)",
)
cold_stream_snapshot.set_defaults(func=_cmd_cold_stream_snapshot)
cold_clone = cold_sub.add_parser(
"clone",
help=(
"live-snapshot consumer: read CURRENT (or a pinned --snapshot-id), "
"multipart-download raw .db shards into --shards-dir in parallel. "
"No pack/unpack, no FTS rebuild — the index travels inside the file."
),
)
cold_clone.add_argument(
"--shards-dir", dest="shards_dir", required=True,
help="target dir to clone into",
)
cold_clone.add_argument(
"--snapshot-id", dest="snapshot_id", default=None,
help="pin a snapshot id (default: read CURRENT pointer)",
)
cold_clone.add_argument(
"--workers", dest="workers", type=int, default=4,
help="parallel multipart downloads (default 4 = one per shard)",
)
cold_clone.set_defaults(func=_cmd_cold_clone)
cold_list = cold_sub.add_parser(
"list",
help="enumerate packs in the bucket with metadata (pack_hash, size, chunk_count) for new-peer hydration",
)
cold_list.add_argument(
"--no-manifest",
dest="fetch_manifest",
action="store_false",
help="skip per-pack manifest fetch (faster on huge buckets; "
"chunk_count column shows null)",
)
cold_list.set_defaults(func=_cmd_cold_list, fetch_manifest=True)
# --- #000065 content-hash reshard ----------------------------------------
corpus_cmd = sub.add_parser(
"corpus",
help="corpus-wide topology operations (reshard, …)",
)
corpus_sub = corpus_cmd.add_subparsers(dest="corpus_op", required=True)
reshard = corpus_sub.add_parser(
"reshard",
help=(
"migrate shards to a content-hash-routed M-shard layout (#000065). "
"Picks a strategy from available disk; use --plan-only first."
),
)
reshard.add_argument(
"--to", dest="target_M", type=int, required=True,
help="target shard count M (e.g., 4)",
)
reshard.add_argument(
"--source-dir", dest="source_dir", required=True,
help="directory of source shards (00[0-9].db files)",
)
reshard.add_argument(
"--target-dir", dest="target_dir", required=True,
help="directory to write target shards (will be created)",
)
reshard.add_argument(
"--audit-events-ndjson", dest="audit_ndjson",
default="/tmp/audit-events.ndjson",
help="ndjson of pre-migration audit events from "
"bench/extract_audit_events.py (default /tmp/audit-events.ndjson)",
)
reshard.add_argument(
"--plan-only", dest="plan_only", action="store_true",
help="print the chosen strategy + peak draw estimate + rationale, "
"then exit. No DBs opened, no rows moved.",
)
reshard.add_argument(
"--force-strategy", dest="force_strategy",
choices=["all_at_once", "per_source_shard", "streaming_row"],
default=None,
help="override the planner's pick. Use only when you know the "
"planner's draw estimate is wrong (e.g., for testing strategy B).",
)
reshard.add_argument(
"--allow-in-place", dest="allow_in_place", action="store_true",
help="allow strategy C (streaming-row, in-place mutation of source "
"shards). Last-resort; disabled by default since it mutates "
"source data with no recoverable intermediate.",
)
reshard.add_argument(
"--dry-run", dest="dry_run", action="store_true",
help="do everything except actually open the target DBs and move "
"rows. Plan still emits, target dir still created.",
)
reshard.add_argument(
"--expected-row-counts", dest="expected_row_counts_json",
default=None,
help="path to a pre-migration snapshot JSON "
"(e.g., bench/results/pre-migration-snapshot.json). If provided, "
"the executor compares post-migration row totals against the "
"snapshot's `totals` block within ±1%% tolerance, and refuses to "
"promote .db.new files into final names on mismatch.",
)
reshard.set_defaults(func=_cmd_corpus_reshard)
activity_cmd = sub.add_parser(
"activity",
help="recent Q&A + freshly cached docs (agent-readable timeline)",
)
activity_cmd.add_argument(
"--limit", type=int, default=10,
help="max items per category (default 10)",
)
activity_cmd.add_argument(
"--since-seconds",
dest="since_seconds",
type=int,
default=0,
help="only events newer than this many seconds (0 = all time, default)",
)
activity_cmd.add_argument(
"--preview-chars",
dest="preview_chars",
type=int,
default=240,
help="answer preview length (default 240 chars)",
)
activity_cmd.set_defaults(func=_cmd_activity)
stats_cmd = sub.add_parser("stats", help="counts: docs, chunks, edges, audit")
stats_cmd.set_defaults(func=_cmd_stats)
canon_cmd = sub.add_parser(
"canon",
help=(
"canonicalize input via a registered π* (direct projection, "
"no RAG, no LLM)"
),
)
canon_cmd.add_argument(
"key", nargs="?",
help="π* key, e.g. arithmetic@v1, logic-kernel@v1, code-py-ast@v1",
)
canon_cmd.add_argument(
"input", nargs="?",
help="raw input string to canonicalize (UTF-8)",
)
canon_cmd.add_argument(
"--list", action="store_true",
help="list registered π* keys + their domains and exit",
)
canon_cmd.add_argument(
"--json", action="store_true",
help="emit {pi_star_ref, input, canonical, canonical_sha256}",
)
canon_cmd.set_defaults(func=_cmd_canon)
analyze_cmd = sub.add_parser(
"analyze",
help="compression spectrum, depth distribution, audit chain integrity",
)
analyze_cmd.add_argument(
"--gravity-top",
dest="gravity_top",
type=int,
default=10,
help="N top inbound-linked documents to report (default 10)",
)
analyze_cmd.set_defaults(func=_cmd_analyze)
# ----- snapshot subcommands ----------------------------------------------
snap_cmd = sub.add_parser(
"snapshot",
help="corpus-level Merkle snapshots: pin a forest state by single root",
)
snap_sub = snap_cmd.add_subparsers(dest="snap_op", required=True)
snap_create = snap_sub.add_parser(
"create", help="compute snapshot_root from current corpus, persist + audit"
)
snap_create.add_argument("--reason", default="manual")
snap_create.add_argument(
"--parent",
default=None,
help="explicit parent_snapshot hex (default: auto-link to latest prior snapshot)",
)
snap_create.set_defaults(func=_cmd_snapshot_create)
snap_list = snap_sub.add_parser("list", help="recent snapshots, newest first")
snap_list.add_argument("--limit", type=int, default=20)
snap_list.set_defaults(func=_cmd_snapshot_list)
snap_verify = snap_sub.add_parser(
"verify",
help="recompute root from current corpus; matches=True iff nothing has changed",
)
snap_verify.add_argument("snapshot_root", help="hex snapshot_root to verify")
snap_verify.set_defaults(func=_cmd_snapshot_verify)
snap_diff = snap_sub.add_parser(
"diff",
help="coarse drift signal between a snapshot and the current corpus",
)
snap_diff.add_argument("snapshot_root", help="hex snapshot_root to diff against current")
snap_diff.set_defaults(func=_cmd_snapshot_diff)
# ----- substrate subcommands (ticket #000012 Phase 1a + future) ----------
# Was `arborist v8 score` until 2026-05-10; renamed for naming-consistency
# with the arborist/substrate/ dir, which is itself the post-rename home
# of what used to live under arborist/v8/. The ``v`` in ``v8`` referred
# to the substrate-paper version, which collided with the v9.8 SQLite
# schema version and confused readers.
substrate_cmd = sub.add_parser(
"substrate",
help="Merkle-AGI substrate primitives (ForkScore + future paper specs)",
)
substrate_sub = substrate_cmd.add_subparsers(dest="substrate_op", required=True)
substrate_score = substrate_sub.add_parser(
"score",
help="ForkScore over (parent, child) bench-result JSON files (#000012)",
)
substrate_score.add_argument(
"--parent", required=True,
help="path to parent bench-result JSON (from `bench.batteries.runner --all`)",
)
substrate_score.add_argument(
"--child", required=True,
help="path to child bench-result JSON",
)
substrate_score.add_argument(
"--weights", default=None,
help="optional path to a weights JSON file; falls through to DEFAULT_WEIGHTS",
)
substrate_score.add_argument(
"--capital-delta", dest="capital_delta", type=float, default=0.0,
help="capital cost delta from #000020 ledger; positive = child costs more",
)
substrate_score.add_argument(
"--selfmodel-calibration-gain",
dest="selfmodel_calibration_gain", type=float, default=0.0,
help="SelfModel calibration improvement (parent→child); 0 if unmeasured",
)
substrate_score.add_argument(
"--audit-completeness", dest="audit_completeness", type=float, default=0.0,
help="fraction of state-changes with audit-event in 0..1",
)
substrate_score.add_argument(
"--validator-diversity", dest="validator_diversity", type=float, default=0.0,
help="multi-validator diversity score; 0 in single-validator mode",
)
substrate_score.add_argument(
"--security-risk", dest="security_risk", type=float, default=0.0,
help="reserved; 0 in Phase 1a (no security-bench yet)",
)
substrate_score.add_argument(
"--complexity-delta", dest="complexity_delta", type=float, default=0.0,
help="reserved; 0 in Phase 1a",
)
substrate_score.add_argument(
"--memory-invalidation-count",
dest="memory_invalidation_count", type=float, default=0.0,
help="count of memory_records the fork would falsify",
)
substrate_score.add_argument(
"--out", default=None,
help=(
"optional output file path; ScoredFork JSON is also written "
"here in addition to stdout. Default: stdout only. The CI "
"/ make bench-fork-score targets pin this to "
"bench/results/fork_score_report.json so downstream graders "
"/ ForkScore-aware mesh peers can ingest the artifact."
),
)
# ----- Phase 1c (#000012 §7) — branch-set persistence ---------------
# Default off. When --branch-set is present a row is written to
# ``fork_score_branches``; absent ⇒ pure-function semantics
# (Phase 1a behavior preserved).
substrate_score.add_argument(
"--branch-set", dest="branch_set", default=None,
help=(
"checkpoint identity (e.g. parent_root + ts); when present, "
"writes one row to fork_score_branches sibling table"
),
)
substrate_score.add_argument(
"--branch-id", dest="branch_id", default=None,
help="fork identifier; defaults to --child-root when omitted",
)
substrate_score.add_argument(
"--parent-root", dest="parent_root", default=None,
help="shared parent root (required when --branch-set is given)",
)
substrate_score.add_argument(
"--child-root", dest="child_root", default=None,
help="child root (nullable for in-flight branches)",
)
substrate_score.add_argument(
"--persist-shard", dest="persist_shard", default=None,
help=(
"SQLite path to write fork_score_branches row to; defaults "
"to --db (the current arborist target shard)"
),
)
substrate_score.add_argument(
"--weights-id", dest="weights_id", default=None,
help=(
"opaque label for the WeightSet used; defaults to "
"'default' or the basename of --weights"
),
)
substrate_score.set_defaults(func=_cmd_substrate_score)
# ----- memory subcommands (ticket #000017) --------------------------------
memory_cmd = sub.add_parser(
"memory",
help="lifelong-learning audit summary (ticket #000017)",
)
memory_sub = memory_cmd.add_subparsers(
dest="memory_op", required=True
)
mem_snap = memory_sub.add_parser(
"snapshot",
help="build a memory snapshot from current store state",
)
mem_snap.set_defaults(func=_cmd_memory_snapshot)
mem_show = memory_sub.add_parser(
"show",
help="print a memory record by root, or the latest live one",
)
mem_show.add_argument(
"--root",
default=None,
help="hex memory_root (default: latest live)",
)
mem_show.set_defaults(func=_cmd_memory_show)
mem_branches = memory_sub.add_parser(
"branches",
help="list branch summaries attached to a memory_root",
)
mem_branches.add_argument(
"--root",
default=None,
help="hex memory_root (default: latest live)",
)
mem_branches.set_defaults(func=_cmd_memory_branches)
mem_fals = memory_sub.add_parser(
"falsify", help="mark a memory_root falsified"
)
mem_fals.add_argument("root", help="hex memory_root to falsify")
mem_fals.add_argument(
"--reason", required=True, help="why this memory is falsified"
)
mem_fals.add_argument(
"--branch-id",
dest="branch_id",
default=None,
help="optional triggering branch_id",
)
mem_fals.set_defaults(func=_cmd_memory_falsify)
# ----- capital subcommands (ticket #000020) -------------------------------
capital_cmd = sub.add_parser(
"capital",
help="8-capital-form cost ledger (ticket #000020)",
)
capital_sub = capital_cmd.add_subparsers(
dest="capital_op", required=True
)
cap_summary = capital_sub.add_parser(
"summary",
help="aggregate per-form sums across the ledger",
)
cap_summary.add_argument(
"--op-type",
dest="op_type",
default=None,
help="filter to one op_type",
)
cap_summary.add_argument(
"--since",
type=int,
default=None,
help="only rows with recorded_at >= this Unix timestamp",
)
cap_summary.set_defaults(func=_cmd_capital_summary)
cap_op = capital_sub.add_parser(
"op-cost", help="per-form totals for one op_type"
)
cap_op.add_argument("op_type", help="e.g. ingest|qa|distill")
cap_op.set_defaults(func=_cmd_capital_op_cost)
cap_top = capital_sub.add_parser(
"top", help="top-N op_types by total in one capital form"
)
cap_top.add_argument(
"--form",
required=True,
choices=[
"living",
"material",
"financial",
"intellectual",
"experiential",
"social",
"cultural",
"spiritual",
],
)
cap_top.add_argument("--limit", type=int, default=10)
cap_top.set_defaults(func=_cmd_capital_top)
# ----- selfmodel subcommands (ticket #000014) -----------------------------
selfmodel_cmd = sub.add_parser(
"selfmodel",
help="agent identity record: capability claims, falsification (ticket #000014)",
)
selfmodel_sub = selfmodel_cmd.add_subparsers(
dest="selfmodel_op", required=True
)
sm_snap = selfmodel_sub.add_parser(
"snapshot",
help="build a SelfModel from current store state and persist it",
)
sm_snap.set_defaults(func=_cmd_selfmodel_snapshot)
sm_show = selfmodel_sub.add_parser(
"show",
help="print a SelfModel by root, or the latest live one",
)
sm_show.add_argument(
"--root",
default=None,
help="hex selfmodel_root (default: latest live)",
)
sm_show.set_defaults(func=_cmd_selfmodel_show)
sm_fals = selfmodel_sub.add_parser(
"falsify",
help="mark a SelfModel falsified with a reason",
)
sm_fals.add_argument("root", help="hex selfmodel_root to falsify")
sm_fals.add_argument(
"--reason", required=True, help="why this SelfModel is falsified"
)
sm_fals.add_argument(
"--claim-hash",
dest="claim_hash",
default=None,
help="optional triggering capability-claim hash",
)
sm_fals.set_defaults(func=_cmd_selfmodel_falsify)
sm_list = selfmodel_sub.add_parser(
"list", help="list recent SelfModel rows, newest first"
)
sm_list.add_argument("--limit", type=int, default=20)
sm_list.set_defaults(func=_cmd_selfmodel_list)
# ----- warrant resolver (#000031 Phase 2) --------------------------------
warrant_status_cmd = sub.add_parser(
"warrant-status",
help="show citation-resolver matches per claim-pack record (read-only)",
)
warrant_status_cmd.add_argument(
"--shards-dir",
dest="shards_dir",
default=None,
help="shards directory (overrides --shards-dir from global)",
)
warrant_status_cmd.add_argument(
"--limit", type=int, default=3, help="max candidate matches per record"
)
warrant_status_cmd.set_defaults(func=_cmd_warrant_status)
warrant_resolve_cmd = sub.add_parser(
"warrant-resolve",
help="run citation resolver; with --write, write derivations rows binding "
"claim-pack records to surface chunks (Merkle proof)",
)
warrant_resolve_cmd.add_argument(
"--shards-dir",
dest="shards_dir",
default=None,
help="shards directory (overrides --shards-dir from global)",
)
warrant_resolve_cmd.add_argument(
"--write",
action="store_true",
help="actually write derivations rows (default: dry-run summary)",
)
warrant_resolve_cmd.add_argument(
"--limit", type=int, default=1, help="how many top matches per record (default 1)"
)
warrant_resolve_cmd.add_argument(
"--use-aliases",
dest="use_aliases",
action="store_true",
help=(
"look up registered citation + term aliases at resolve "
"time (#000041 + #000042); alias-resolved derivations "
"carry process_id 'warrant-resolver-v1+alias'"
),
)
warrant_resolve_cmd.set_defaults(func=_cmd_warrant_resolve)
# ----- unconscious sweep (#000037 §3.1 partial) --------------------------
sweep_cmd = sub.add_parser(
"sweep",
help="unconscious sweep: re-run cross-checks on data that bypassed "
"meta-cognition at ingest time (#000037 §3.1)",
)
sweep_cmd.add_argument(
"--shards-dir",
dest="shards_dir",
default=None,
help="shards directory (overrides --shards-dir from global)",
)
sweep_cmd.add_argument(
"--target",
choices=["warrants", "all"],
default="warrants",
help=(
"sweep target: 'warrants' = re-run warrant resolver against every "
"claim-pack record (#000031), idempotent at PK level. 'all' "
"reserved for the full bicameral sweep landing later "
"(canonical-projection probe / freshness probe / document-content "
"witness — schema-bumped per #000037 §12 trigger)."
),
)
sweep_cmd.add_argument(
"--write",
action="store_true",
help="materialize derivations rows for newly-resolved records "
"(default: dry-run; report what would land)",
)
sweep_cmd.add_argument(
"--limit", type=int, default=1,
help="how many top matches per record to consider (default 1)",
)
sweep_cmd.add_argument(
"--use-aliases",
dest="use_aliases",
action="store_true",
help="apply citation + term aliases (#000041 + #000042) at resolve time",
)
sweep_cmd.set_defaults(func=_cmd_sweep)
# ----- alias subcommands (#000041 + #000042) -----------------------------
alias_cmd = sub.add_parser(
"alias",
help="curated citation + term aliases (audit-disciplined; opt-in)",
)
alias_sub = alias_cmd.add_subparsers(dest="alias_op", required=True)
# alias citation {add,list,remove}
alias_cit = alias_sub.add_parser(
"citation",
help="citation aliases (#000041): substitute textbook for proprietary cite",
)
alias_cit_sub = alias_cit.add_subparsers(dest="alias_cit_op", required=True)
cit_add = alias_cit_sub.add_parser(
"add", help="add a citation alias (refuses without --by)"
)
cit_add.add_argument("original", help="original source_reference string")
cit_add.add_argument(
"--substitute", required=True, help="replacement citation string"
)
cit_add.add_argument(
"--author", action="append", default=None,
help="author of the substitute work (repeatable)",
)
cit_add.add_argument(
"--title", default="", help="title of the substitute work"
)
cit_add.add_argument(
"--by", required=True,
help="who decided this alias (audit field; refuses if empty)",
)
cit_add.add_argument(
"--rationale", default="", help="why this alias is appropriate"
)
cit_add.add_argument(
"--aliases-db", default=None,
help="aliases DB path (default: <shards-dir>/000.db)",
)
cit_add.set_defaults(func=_cmd_alias_citation_add)
cit_list = alias_cit_sub.add_parser(
"list", help="list registered citation aliases"
)
cit_list.add_argument(
"--filter", default=None, help="substring filter on original_ref"
)
cit_list.add_argument("--aliases-db", default=None)
cit_list.set_defaults(func=_cmd_alias_citation_list)
cit_rm = alias_cit_sub.add_parser(
"remove", help="remove a citation alias by (original, substitute)"
)
cit_rm.add_argument("original")
cit_rm.add_argument("--substitute", required=True)
cit_rm.add_argument("--aliases-db", default=None)
cit_rm.set_defaults(func=_cmd_alias_citation_remove)
# alias term {add,list,remove}
alias_term = alias_sub.add_parser(
"term",
help="term aliases (#000042): vocabulary bridge for old vs modern words",
)
alias_term_sub = alias_term.add_subparsers(dest="alias_term_op", required=True)
term_add = alias_term_sub.add_parser(
"add", help="add a term alias (refuses without --by)"
)
term_add.add_argument("term", help="modern term used in claim-pack records")
term_add.add_argument(
"alternate", help="historical / foreign / alternate term used in textbook prose"
)
term_add.add_argument(
"--domain", required=True,
help="domain string (e.g., 'geometry', 'logic')",
)
term_add.add_argument(
"--by", required=True,
help="who decided this alias (audit field; refuses if empty)",
)
term_add.add_argument(
"--rationale", default="", help="why this alias is appropriate"
)
term_add.add_argument("--aliases-db", default=None)
term_add.set_defaults(func=_cmd_alias_term_add)
term_list = alias_term_sub.add_parser(
"list", help="list registered term aliases"
)
term_list.add_argument("--domain", default=None)
term_list.add_argument(
"--filter", default=None, help="substring filter on term"
)
term_list.add_argument("--aliases-db", default=None)
term_list.set_defaults(func=_cmd_alias_term_list)
term_rm = alias_term_sub.add_parser(
"remove", help="remove a term alias by (term, alternate, domain)"
)
term_rm.add_argument("term")
term_rm.add_argument("alternate")
term_rm.add_argument("--domain", required=True)
term_rm.add_argument("--aliases-db", default=None)
term_rm.set_defaults(func=_cmd_alias_term_remove)
# ----- mesh subcommands (off by default) ---------------------------------
mesh_cmd = sub.add_parser(
"mesh",
help="federation/gossip layer (off by default; opt-in via 'mesh enable')",
)
mesh_sub = mesh_cmd.add_subparsers(dest="mesh_op", required=True)
mesh_status = mesh_sub.add_parser("status", help="show enabled flag, identity, current epoch + roster")
mesh_status.set_defaults(func=_cmd_mesh_status)
mesh_init = mesh_sub.add_parser("init", help="generate this peer's keys; create epoch 0")
mesh_init.add_argument("--group", required=True, help="group name")
mesh_init.add_argument("--member-id", dest="member_id", default=None, help="optional fixed member id (default: random 8-hex)")
mesh_init.set_defaults(func=_cmd_mesh_init)
mesh_enable = mesh_sub.add_parser("enable", help="flip the mesh.enabled flag on")
mesh_enable.set_defaults(func=_cmd_mesh_enable)
mesh_disable = mesh_sub.add_parser("disable", help="flip the mesh.enabled flag off")
mesh_disable.set_defaults(func=_cmd_mesh_disable)
mesh_members = mesh_sub.add_parser("members", help="list current epoch's roster")
mesh_members.set_defaults(func=_cmd_mesh_members)
mesh_add = mesh_sub.add_parser("add", help="admin-only: add a peer to the roster (bumps epoch)")
mesh_add.add_argument("--member-id", dest="member_id", required=True)
mesh_add.add_argument("--sign-pub", dest="sign_pub", required=True, help="hex Ed25519 pubkey (32 bytes / 64 hex chars)")
mesh_add.add_argument("--dh-pub", dest="dh_pub", required=True, help="hex X25519 pubkey")
mesh_add.add_argument("--role", choices=["admin", "member"], default="member")
mesh_add.set_defaults(func=_cmd_mesh_add)
mesh_kick = mesh_sub.add_parser("kick", help="admin-only: evict a peer (bumps epoch; old signatures stay valid, new gossip is opaque to them)")
mesh_kick.add_argument("--member-id", dest="member_id", required=True)
mesh_kick.add_argument("--reason", required=True)
mesh_kick.set_defaults(func=_cmd_mesh_kick)
mesh_rotate = mesh_sub.add_parser("rotate", help="refresh epoch secret without changing roster")
mesh_rotate.add_argument("--reason", default="scheduled")
mesh_rotate.set_defaults(func=_cmd_mesh_rotate)
mesh_serve = mesh_sub.add_parser(
"serve",
help="run the HTTP gossip server (blocks until SIGINT)",
)
mesh_serve.add_argument("--host", default="127.0.0.1", help="bind host (default: 127.0.0.1)")
mesh_serve.add_argument("--port", type=int, default=8400, help="bind port (default: 8400)")
mesh_serve.set_defaults(func=_cmd_mesh_serve)
mesh_sync = mesh_sub.add_parser(
"sync",
help="announce local document_roots to a peer's gossip server",
)
mesh_sync.add_argument("--peer", required=True, help="peer URL, e.g. http://other.example.com:8400")
mesh_sync.add_argument("--limit", type=int, default=100, help="announce at most N most-recent items per category (default: 100)")
mesh_sync.add_argument("--verbose", type=int, default=10, help="include this many ack details in output (default: 10)")
mesh_sync.add_argument(
"--no-roots",
dest="no_roots",
action="store_true",
help="skip ANNOUNCE_ROOT broadcast (only push falsifications)",
)
mesh_sync.add_argument(
"--no-falsifications",
dest="no_falsifications",
action="store_true",
help="skip ANNOUNCE_FALSIFICATION broadcast (only push roots)",
)
mesh_sync.add_argument(
"--kindergarten-seconds",
dest="kindergarten_seconds",
type=int,
default=3600,
help=(
"hold records younger than this many seconds back from the "
"broadcast (default: 3600 = 1 hour). Gives operators time to "
"burn or falsify before peers see it. 0 = broadcast everything."
),
)
mesh_sync.set_defaults(func=_cmd_mesh_sync)
mesh_pull = mesh_sub.add_parser(
"pull",
help="pull one document body from a peer by document_root",
)
mesh_pull.add_argument("--root", required=True, help="64-char hex document_root to pull")
mesh_pull.add_argument("--peer", required=True, help="peer URL, e.g. http://other.example.com:8400")
mesh_pull.set_defaults(func=_cmd_mesh_pull)
crawl_cmd = sub.add_parser(
"crawl",
help=(
"BFS-discover same-domain URLs from a seed; optionally ingest "
"and store ETag/Last-Modified for cheap recrawl-checks "
"(requires arborist[crawler] extras)"
),
)
crawl_cmd.add_argument("--seed-url", dest="seed_url", required=True)
crawl_cmd.add_argument("--depth", type=int, default=2, help="max BFS depth (default: 2)")
crawl_cmd.add_argument(
"--max-pages",
dest="max_pages",
type=int,
default=0,
help="cap discovery at N URLs (0 = no cap, depth is the only bound; default: 0)",
)
crawl_cmd.add_argument(
"--ingest",
action="store_true",
help="ingest the discovered pages into --db (default: print URL list only)",
)
crawl_cmd.add_argument(
"--fast",
action="store_true",
help=(
"fast_mode: 5s timeouts, CPU*3 parallel page workers, ignore "
"robots.txt crawl-delay (Disallow is still honored). Use only "
"against domains where aggressive fetching is acceptable."
),
)
crawl_cmd.add_argument(
"--author",
default=None,
help=(
"default author surname (#000031 Phase 1 follow-up). "
"Appended to ingested document titles when the HTML's "
"<title> doesn't already carry the surname. Threads into "
"_shard_matches_citation's title-haystack for warrant "
"resolution. Only relevant with --ingest."
),
)
crawl_cmd.set_defaults(func=_cmd_crawl)
crawler_cmd = sub.add_parser(
"crawler",
help="crawler maintenance verbs (recrawl-check, ...)",
)
crawler_sub = crawler_cmd.add_subparsers(dest="crawler_op", required=True)
recrawl_check_cmd = crawler_sub.add_parser(
"recrawl-check",
help=(
"send conditional HEAD requests for ingested documents and "
"classify each as fresh/stale/gone/unreachable"
),
)
recrawl_check_cmd.add_argument(
"--domain",
default=None,
help="restrict to documents whose URI contains this domain",
)
recrawl_check_cmd.add_argument(
"--limit",
type=int,
default=100,
help="check at most N documents (oldest checks first; default: 100)",
)
recrawl_check_cmd.set_defaults(func=_cmd_crawler_recrawl_check)
# --- wallet-in-cloud (SPV) -------------------------------------------
serve_cmd = sub.add_parser(
"serve",
help=(
"start an HTTP wallet server: GET /snapshot_root, POST /ask. "
"Wraps query() and returns Merkle-verifiable AnswerBundles for "
"thin clients (`arborist wallet ask`) to verify against a "
"pinned trust anchor — same SPV pattern as Bitcoin/Electrum."
),
)
serve_cmd.add_argument("--host", default="127.0.0.1")
serve_cmd.add_argument("--port", type=int, default=8765)
serve_cmd.add_argument(
"--qa-db", default="~/.arborist/qa.db",
help="QA cache DB path (created on first call).",
)
serve_cmd.add_argument(
"--top-k", dest="serve_top_k", type=int, default=4,
help="default top_k for /ask requests that don't specify one.",
)
serve_cmd.set_defaults(func=_cmd_serve)
wallet_cmd = sub.add_parser(
"wallet",
help="SPV wallet client — query a remote arborist server and verify.",
)
wallet_sub = wallet_cmd.add_subparsers(dest="wallet_cmd", required=True)
wallet_ask = wallet_sub.add_parser(
"ask",
help=(
"submit a question to the server, verify the returned "
"AnswerBundle against the wallet's trust anchor"
),
)
wallet_ask.add_argument("question", type=str)
wallet_ask.add_argument(
"--server-url", required=True,
help="base URL of the arborist wallet server (e.g. http://host:8765)",
)
wallet_ask.add_argument(
"--trust-anchor", required=True,
help="hex snapshot_root the wallet trusts; out-of-band-set",
)
wallet_ask.add_argument(
"--top-k", type=int, default=None,
help="optional top_k override on the server.",
)
wallet_ask.set_defaults(func=_cmd_wallet_ask)
wallet_anchor = wallet_sub.add_parser(
"anchor",
help=(
"fetch the server's current snapshot_root (advisory — does NOT "
"update the wallet's trust anchor)"
),
)
wallet_anchor.add_argument("--server-url", required=True)
wallet_anchor.set_defaults(func=_cmd_wallet_anchor)
cloud_cmd = sub.add_parser(
"cloud",
help=(
"bucket-direct queries — open the corpus .db file on a "
"bucket via SQLite HTTP-range VFS, no server or local DB"
),
)
cloud_sub = cloud_cmd.add_subparsers(dest="cloud_cmd", required=True)
cloud_search = cloud_sub.add_parser(
"search",
help="FTS5 search against a bucket-resident shard .db file",
)
cloud_search.add_argument("query", type=str)
cloud_search.add_argument(
"--shard-url", required=True,
help="full URL to one .db file on the bucket (e.g. "
"https://bucket.example.com/clones/snap-1/000.db)",
)
cloud_search.add_argument(
"--blob-base", default=None,
help="base URL for chunk blobs (e.g. "
"https://bucket.example.com/blobs). Defaults to siblings/of/shard-url.",
)
cloud_search.add_argument("--limit", type=int, default=8)
cloud_search.add_argument(
"--cache-mb", type=int, default=32,
help="LRU page cache size in MB (default 32).",
)
cloud_search.add_argument(
"--raw", action="store_true",
help="pass query through unmodified (FTS5 syntax). Default: "
"natural-language → safe FTS5 (strip punctuation + stopwords, OR tokens).",
)
cloud_search.set_defaults(func=_cmd_cloud_search)
cloud_snapshot = cloud_sub.add_parser(
"snapshot-root",
help="compute snapshot_root of the bucket-resident shard",
)
cloud_snapshot.add_argument("--shard-url", required=True)
cloud_snapshot.add_argument("--cache-mb", type=int, default=32)
cloud_snapshot.set_defaults(func=_cmd_cloud_snapshot_root)
cloud_fetch = cloud_sub.add_parser(
"fetch-chunk",
help="fetch one chunk body from blobs/<hash> + verify its hash",
)
cloud_fetch.add_argument("leaf_hash", type=str)
cloud_fetch.add_argument("--blob-base", required=True)
cloud_fetch.set_defaults(func=_cmd_cloud_fetch_chunk)
cloud_ask = cloud_sub.add_parser(
"ask",
help=(
"bucket-direct end-to-end ask: FTS → pull chunks via SQL → "
"LLM call → verify quotes locally. Same audited-answer shape "
"as `arborist query` but no local DB."
),
)
cloud_ask.add_argument("question", type=str)
cloud_ask.add_argument(
"--bucket-url", default=None,
help="bucket root URL (e.g. https://nyc3.digitaloceanspaces.com/arborist). "
"The client fetches `clones/manifest.json` from this URL and queries "
"every listed shard. Use this for multi-shard corpus access.",
)
cloud_ask.add_argument(
"--shard-url", default=None,
help="single shard URL (mutually exclusive with --bucket-url). "
"Use when you want to point at exactly one .db file on a bucket.",
)
cloud_ask.add_argument("--top-k", type=int, default=4)
cloud_ask.add_argument("--max-context-chars", type=int, default=24_000)
cloud_ask.add_argument(
"--cache-mb", type=int, default=64,
help="LRU page cache size in MB (default 64 — bigger than search "
"default; chunk-content reads dominate).",
)
cloud_ask.add_argument(
"--endpoint", default=None,
help="LLM endpoint (default: $ARBORIST_LLM_ENDPOINT or "
"https://hermes.ai.unturf.com/v1).",
)
cloud_ask.add_argument(
"--model", default=None,
help="LLM model id (default: $ARBORIST_LLM_MODEL or Hermes-3-8B).",
)
cloud_ask.add_argument(
"--json", action="store_true",
help="emit the full result as JSON. Default: human-rendered "
"(matches `arborist query` output).",
)
cloud_ask.set_defaults(func=_cmd_cloud_ask)
return p
def _cmd_serve(args: argparse.Namespace) -> int:
"""Start the HTTP wallet server. Blocks until SIGINT/SIGTERM."""
from arborist.wallet.server import WalletServer, serve
shards_dir = getattr(args, "shards_dir", None)
single_db = getattr(args, "db", None)
if not shards_dir and not single_db:
print(
"arborist serve needs --shards-dir or --db pointing at the corpus",
file=sys.stderr,
)
return 2
qa_db = Path(args.qa_db).expanduser()
qa_db.parent.mkdir(parents=True, exist_ok=True)
chat_client_factory = None
if os.environ.get("ARBORIST_WALLET_STUB", "0") == "1":
from arborist.qa.client import StubClient
def chat_client_factory():
return StubClient(answer="[stub] no LLM in --stub mode")
else:
endpoint = os.environ.get("ARBORIST_LLM_ENDPOINT", "https://hermes.ai.unturf.com/v1")
model_id = os.environ.get("ARBORIST_LLM_MODEL", "NousResearch/Hermes-3-Llama-3.1-8B-FP8-Dynamic")
from arborist.qa.client import OpenAICompatibleClient
def chat_client_factory():
return OpenAICompatibleClient(endpoint=endpoint)
wsrv = WalletServer(
qa_db=qa_db,
shards_dir=Path(shards_dir) if shards_dir else None,
single_db=Path(single_db) if single_db else None,
chat_client_factory=chat_client_factory,
model_id=os.environ.get("ARBORIST_LLM_MODEL", "stub"),
default_top_k=args.serve_top_k,
)
httpd = serve(server=wsrv, host=args.host, port=args.port)
print(
f"arborist wallet server listening on http://{args.host}:{args.port}",
file=sys.stderr,
)
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nshutting down", file=sys.stderr)
finally:
httpd.shutdown()
return 0
def _cmd_wallet_ask(args: argparse.Namespace) -> int:
from arborist.wallet.client import WalletClient
from arborist.wallet.proof import VerificationError
client = WalletClient(
server_url=args.server_url,
trust_anchor=args.trust_anchor,
)
try:
verified = client.ask(args.question, top_k=args.top_k)
except VerificationError as e:
print(f"VERIFICATION FAILED: {e}", file=sys.stderr)
return 3
print(json.dumps({
"audit_mode": verified.audit_mode,
"local_audit_mode": verified.local_audit_mode,
"local_n_verified": verified.local_n_verified,
"local_verifier_method": verified.local_verifier_method,
"cache_key": verified.cache_key,
"snapshot_root": verified.snapshot_root,
"n_chunks_verified": verified.n_chunks_verified,
"sources": verified.sources,
"answer_text": verified.answer_text,
}, indent=2, ensure_ascii=False))
return 0
def _cmd_wallet_anchor(args: argparse.Namespace) -> int:
from arborist.wallet.client import WalletClient
# trust_anchor is required by the constructor but unused for
# /snapshot_root (it's an advisory fetch).
client = WalletClient(server_url=args.server_url, trust_anchor="00" * 32)
print(json.dumps(client.snapshot_root(), indent=2))
return 0
def _make_bucket_client(args):
"""Build a BucketClient from CLI args. Imports apsw lazily so the
rest of the CLI keeps working when apsw isn't installed."""
try:
from arborist.wallet.bucket import BucketClient, BucketEndpoint
except ImportError as e:
print(
f"bucket-direct commands need the optional `apsw` package: {e}",
file=sys.stderr,
)
sys.exit(2)
blob_base = getattr(args, "blob_base", None)
shard_url = getattr(args, "shard_url", None)
if blob_base is None and shard_url:
# Default: blobs live one level up + "/blobs" beside clones/.
# e.g. .../clones/snap-1/000.db → .../blobs
from urllib.parse import urlparse, urlunparse
u = urlparse(shard_url)
parts = u.path.rstrip("/").split("/")
# Walk up until we find a "clones" segment; sibling is blobs.
for i in range(len(parts) - 1, -1, -1):
if parts[i] == "clones":
base = "/".join(parts[:i]) + "/blobs"
blob_base = urlunparse(u._replace(path=base))
break
if blob_base is None:
blob_base = urlunparse(u._replace(path="/blobs"))
endpoint = BucketEndpoint(shard_url=shard_url or "", blob_base=blob_base or "")
cache_bytes = getattr(args, "cache_mb", 32) * 1024 * 1024
return BucketClient(endpoint, cache_bytes=cache_bytes)
def _cmd_cloud_search(args: argparse.Namespace) -> int:
client = _make_bucket_client(args)
try:
hits = client.fts_search(
args.query, limit=args.limit, raw=getattr(args, "raw", False),
)
finally:
client.close()
print(json.dumps({"hits": hits, "stats": client.stats()}, indent=2))
return 0
def _cmd_cloud_snapshot_root(args: argparse.Namespace) -> int:
client = _make_bucket_client(args)
try:
root = client.snapshot_root()
finally:
client.close()
print(json.dumps({"snapshot_root": root, "stats": client.stats()}, indent=2))
return 0
def _cmd_cloud_ask(args: argparse.Namespace) -> int:
"""Bucket-direct end-to-end ask: FTS → fetch chunks → LLM (claim-
lattice mode) → verify. Same audited-answer shape as local
`query()` — EVIDENCE blocks with E1/E2 pointer tags, pointer-line
answer format, four-rung audit ladder (POINTER-LINKED →
ANCHOR-WARRANTED → EVIDENCE-WARRANTED) — but every byte goes
through HttpRangeVFS from the bucket.
"""
import os as _os
import re as _re
import time as _time
from arborist.compress import unpack_chunk
from arborist.qa.client import OpenAICompatibleClient
from arborist.qa.evidence import (
build_evidence_map,
evidence_map_by_pointer_id,
render_claim_lattice,
render_evidence_map,
)
from arborist.qa.prompts import (
CLAIM_LATTICE_GROUNDING_REMINDER,
CLAIM_LATTICE_SYSTEM_PROMPT,
)
from arborist.qa.verify import verify_claim_lattice
# Multi-shard via manifest, or single-shard if --shard-url given.
if getattr(args, "bucket_url", None):
from arborist.wallet.bucket import (
MultiShardBucketCorpus,
load_bucket_manifest,
)
manifest = load_bucket_manifest(args.bucket_url)
client = MultiShardBucketCorpus(
manifest, cache_bytes_per_shard=args.cache_mb * 1024 * 1024,
)
multi_shard = True
else:
if not getattr(args, "shard_url", None):
print(
"cloud ask needs --bucket-url (multi-shard manifest) "
"or --shard-url (single .db file).",
file=sys.stderr,
)
return 2
client = _make_bucket_client(args)
multi_shard = False
t0 = _time.time()
try:
hits = client.fts_search(args.question, limit=args.top_k)
if not hits:
print(json.dumps({
"answer_text": "",
"audit_mode": "UNGROUNDED",
"verifier_method": "none",
"n_quotes": 0,
"n_verified": 0,
"sources": [],
"stats": client.stats(),
"timing_s": round(_time.time() - t0, 3),
"note": "FTS5 returned no hits — sanitized query may "
"have lost too many tokens; try `cloud search` first.",
}, indent=2, ensure_ascii=False))
return 0
# Pull the first body chunk of each hit via SQL through the
# same VFS; reuses the already-warm page cache. Per-doc budget
# keeps one huge article from monopolizing the context.
chunks_for_evidence: list[dict] = []
total_chars = 0
per_doc_budget = max(
1000, args.max_context_chars // max(1, len(hits))
)
for rank, h in enumerate(hits, 1):
# Multi-shard: route the chunk read back to the shard that
# owned the FTS hit (so we hit a warm page cache and don't
# re-fetch the .db header for every chunk).
if multi_shard:
conn_for_read = client.conn_for_shard(h["_shard_url"])
else:
conn_for_read = client.conn
rows = list(conn_for_read.execute(
"SELECT idx, leaf_hash, content FROM chunks "
"WHERE document_root = ? AND content IS NOT NULL "
"ORDER BY idx ASC LIMIT 1",
(h["document_root"],),
))
if not rows:
continue
idx, leaf_hash, content_blob = rows[0]
span = unpack_chunk(content_blob) or ""
if not span:
continue
if len(span) > per_doc_budget:
span = span[:per_doc_budget]
chunks_for_evidence.append({
"source_root": h["document_root"],
"document_uri": h["document_uri"],
"title": h["title"],
"chunk_idx": idx,
"chunk_root": leaf_hash,
"span": span,
"source_role": (
"primary_answer_source" if rank == 1
else "background_source"
),
})
total_chars += len(span)
if total_chars >= args.max_context_chars:
break
if not chunks_for_evidence:
print(json.dumps({
"answer_text": "",
"audit_mode": "UNGROUNDED",
"sources": hits,
"note": "FTS hit found, but no readable chunk content",
}, indent=2, ensure_ascii=False))
return 0
# Build the evidence map + format the LLM prompt with E1/E2/…
# pointer tags — same shape the local query.py pipeline uses.
evidence_map = build_evidence_map(chunks_for_evidence)
evidence_text = render_evidence_map(evidence_map)
endpoint = args.endpoint or _os.environ.get(
"ARBORIST_LLM_ENDPOINT", "https://hermes.ai.unturf.com/v1",
)
model = args.model or _os.environ.get(
"ARBORIST_LLM_MODEL",
"adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
)
chat = OpenAICompatibleClient(base_url=endpoint)
messages = [
{"role": "system", "content": CLAIM_LATTICE_SYSTEM_PROMPT},
{
"role": "user",
"content": (
f"EVIDENCE:\n\n{evidence_text}\n\n"
f"QUESTION: {args.question}\n\n"
f"{CLAIM_LATTICE_GROUNDING_REMINDER}"
),
},
]
try:
answer = chat.chat_completion(
messages=messages,
model=model,
max_tokens=512,
temperature=0.1,
)
except Exception as e:
print(json.dumps({
"answer_text": "",
"audit_mode": "UNGROUNDED",
"sources": [
{"document_root": h["document_root"],
"document_uri": h["document_uri"],
"title": h["title"], "score": h["score"]}
for h in hits
],
"error": f"LLM call failed: {type(e).__name__}: {e}",
"endpoint": endpoint,
"model": model,
}, indent=2, ensure_ascii=False))
return 4
# Claim-lattice verifier: parses the model's pointer-line
# output, resolves each [E#] back through the evidence map,
# runs the textual-coverage check per claim. Warrant chain
# disabled in cloud mode (no derivations table available).
verdict = verify_claim_lattice(
answer, evidence_map,
question=args.question,
warrant_check_enabled=False,
)
finally:
client.close()
elapsed = _time.time() - t0
# Annotate sources with used / used_pointer_ids per the model's
# cited pointers (parsed from the answer text). Same render-time
# signal `arborist query` surfaces for lattice modes.
by_pid = evidence_map_by_pointer_id(evidence_map)
pointer_re = _re.compile(r"\[E\d+(?:,\s*E\d+)*\]")
used_doc_roots: set[str] = set()
doc_root_to_pointers: dict[str, list[str]] = {}
for m in pointer_re.finditer(answer):
for pid in (p.strip() for p in m.group(0).strip("[]").split(",")):
ev = by_pid.get(pid)
if ev:
used_doc_roots.add(ev.source_root)
doc_root_to_pointers.setdefault(ev.source_root, []).append(pid)
sources_for_render: list[dict] = []
seen_roots: set[str] = set()
for ev in evidence_map:
if ev.source_root in seen_roots:
continue
seen_roots.add(ev.source_root)
sources_for_render.append({
"document_root": ev.source_root,
"document_uri": ev.document_uri,
"title": ev.title,
"source_role": ev.source_role,
"used": ev.source_root in used_doc_roots,
"used_pointer_ids": sorted(set(
doc_root_to_pointers.get(ev.source_root, [])
)),
})
# Render the answer in lattice form (claim line + spotlight excerpts).
claim_statuses = verdict.get("claim_statuses") or []
rendered_answer = render_claim_lattice(
[
{
"text": cs.get("text", ""),
"pointer_ids": cs.get("pointer_ids") or [],
}
for cs in claim_statuses
],
by_pid,
window=200,
) or answer # fall back to raw output if parser dropped everything
result = {
"answer_text": rendered_answer,
"raw_answer": answer,
"audit_mode": verdict["audit_mode"],
"verifier_method": verdict["verifier_method"],
"n_quotes": verdict["n_quotes"],
"n_verified": verdict["n_verified"],
"violations": verdict.get("violations") or [],
"sources": sources_for_render,
"stats": client.stats(),
"timing_s": round(elapsed, 3),
"endpoint": endpoint,
"model": model,
}
if getattr(args, "json", False):
print(json.dumps(result, indent=2, ensure_ascii=False))
else:
print(_render_cloud_ask_human(result, args.question))
return 0
def _render_cloud_ask_human(result: dict, question: str) -> str:
"""Pretty-print a cloud-ask result. Mirrors `_render_query_human`'s
layout — same audit-label ladder, same source roles + used / pointer
annotations — but trims sections the bucket-direct path doesn't
produce (warrant tails, run-DAG, retrieval-purity)."""
audit = result.get("audit_mode") or "UNGROUNDED"
method = result.get("verifier_method") or "?"
n_quotes = result.get("n_quotes") or 0
n_verified = result.get("n_verified") or 0
elapsed_s = result.get("timing_s") or 0.0
violations = result.get("violations") or []
label = _render_audit_label(audit, method, violations)
stats = result.get("stats") or {}
# Stats shape differs single-shard vs multi-shard; coalesce both.
cache_stats = stats.get("cache") or {}
http_reqs = (
stats.get("http_requests")
or stats.get("total_http_requests")
or 0
)
bytes_fetched = (
cache_stats.get("bytes_fetched")
or stats.get("total_bytes_fetched")
or 0
)
lines: list[str] = []
lines.append(question)
lines.append(
f" {label} {n_verified}/{n_quotes} {elapsed_s:.2f}s (bucket-direct)"
)
lines.append("")
lines.append(result.get("answer_text") or "")
lines.append("")
sources = result.get("sources") or []
if sources:
lines.append(f"sources ({len(sources)}):")
for i, s in enumerate(sources, start=1):
uri = s.get("document_uri", "")
title = (s.get("title") or "").strip() or _short_path(uri)
role = s.get("source_role")
used = s.get("used")
pointer_ids = s.get("used_pointer_ids") or []
annotations: list[str] = []
if role:
annotations.append(role)
if used is True:
if pointer_ids:
annotations.append(f"used ({','.join(pointer_ids)})")
else:
annotations.append("used")
elif used is False:
annotations.append("unused")
annotation_part = (
"" + "".join(annotations) if annotations else ""
)
lines.append(
f" [{i}] {title}{annotation_part}{_strip_scheme(uri)}"
)
lines.append("")
bytes_kb = bytes_fetched / 1024.0
lines.append(
f"bucket: {http_reqs} HTTP requests · {bytes_kb:.1f} KB · "
f"{result.get('endpoint','')} / {result.get('model','')}"
)
lines.append(" <run with --json for full record>")
return "\n".join(lines)
def _cmd_cloud_fetch_chunk(args: argparse.Namespace) -> int:
from arborist.merkle import hash_leaf
client = _make_bucket_client(args)
try:
body = client.fetch_chunk_body(args.leaf_hash)
finally:
client.close()
actual = hash_leaf(body).hex()
ok = actual == args.leaf_hash
print(json.dumps({
"leaf_hash_expected": args.leaf_hash,
"leaf_hash_actual": actual,
"verified": ok,
"body_bytes": len(body),
"body_preview": body[:200].decode("utf-8", errors="replace"),
}, indent=2))
return 0 if ok else 3
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())