Phase 1 of the federation/gossip layer fox sketched as the natural
extension of v9.8 admissibility's content-addressed identity. Two
peers ingesting the same dump already compute identical document_roots
and identical 8-dim cache_keys; the mesh layer is the wire-and-trust
plumbing that lets them dedup answers, exchange Merkle proofs, and
cleanly distrust an evicted member without a hard fork.
Cryptography (cryptography lib, audited):
Ed25519 — every membership mutation + (future) gossip envelope
is signed by the actor's pubkey.
X25519 ECDH — wraps each epoch's symmetric mesh secret to every
current member's DH pubkey via HKDF-derived AEAD key.
ChaCha20-P1305— AEAD for envelope payloads + per-member secret wrap.
State machine:
mesh_identity — singleton; this peer's keys + group name
mesh_roster — per-epoch (member_id, sign_pub, dh_pub, role)
mesh_epochs — epoch_id -> {started_at, started_event_hash,
secret_envelope JSON, reason}
meta:mesh.enabled flag — off by default; gates everything
Eviction works by rotating to a new epoch whose envelope omits the
kicked member. Their prior signatures stay verifiable (the older
roster row is retained), but any gossip from epoch+1 onward is
opaque to them — the secret was never shared with their pubkey.
Authority gate: only roster members with role='admin' can add or
kick. Self-kick is rejected explicitly. The last admin can't be
kicked. Schedule-rotate (refresh secret, no roster change) is open
to any current member as a session-hygiene op.
Audit-chain integration: every mesh state mutation writes an audit
event (mesh_init, mesh_enable/disable, mesh_epoch_rotate). The
epoch's started_event_hash backfills into mesh_epochs after the
audit row commits, giving each epoch a tamper-evident pin into the
ledger.
CLI subcommands: mesh init, mesh status, mesh enable, mesh disable,
mesh members, mesh add, mesh kick, mesh rotate. All read-only or
local-state-only — no network code paths in this commit.
The HTTP gossip wire (`mesh sync`, `mesh serve`) is the next phase.
Schema, cryptography, and roster state machine are all in place to
support it without further migration.
1479 lines
49 KiB
Python
1479 lines
49 KiB
Python
"""Aborist CLI: ingest / search / verify / stats."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from aborist import __version__
|
|
from aborist.ingest import ingest_source, verify_random_sample
|
|
from aborist.progress import Progress
|
|
from aborist.search import FTS5Backend
|
|
from aborist.sources import WikipediaCurDump
|
|
from aborist.store import DEFAULT_DB_PATH, connect, connect_query, stats
|
|
|
|
|
|
def _cmd_ingest(args: argparse.Namespace) -> int:
|
|
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 aborist.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 aborist.sources import HtmlPageSource
|
|
except ImportError:
|
|
print(
|
|
"html source requires extras: pip install 'aborist[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)
|
|
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 aborist.sources import GrokExportSource, GrokMediaPostsSource
|
|
|
|
cls = GrokExportSource if args.source == "grok_export" else GrokMediaPostsSource
|
|
src = cls(path=args.path)
|
|
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 aborist.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 aborist.sources import GitRepoSource, MercurialRepoSource
|
|
|
|
cls = GitRepoSource if args.source == "git_repo" else MercurialRepoSource
|
|
src = cls(repo_path=args.path)
|
|
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)
|
|
try:
|
|
result = ingest_source(
|
|
conn,
|
|
src,
|
|
chunker_name=args.chunker,
|
|
limit=args.limit,
|
|
batch_size=args.batch_size,
|
|
resume=args.resume,
|
|
progress=progress,
|
|
)
|
|
finally:
|
|
conn.close()
|
|
print(json.dumps(result.__dict__, indent=2))
|
|
return 0
|
|
|
|
|
|
def _cmd_search(args: argparse.Namespace) -> int:
|
|
conn = (
|
|
connect_query(args.db, shards_dir=args.global_shards_dir)
|
|
if args.global_shards_dir
|
|
else connect(args.db)
|
|
)
|
|
try:
|
|
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,
|
|
)
|
|
)
|
|
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_verify(args: argparse.Namespace) -> int:
|
|
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))
|
|
return 0 if result["failed"] == 0 else 1
|
|
|
|
|
|
def _cmd_distill(args: argparse.Namespace) -> int:
|
|
from aborist.distill import get_distiller
|
|
from aborist.distill.runner import distill_existing
|
|
from aborist.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))
|
|
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))
|
|
return 0
|
|
|
|
|
|
def _cmd_ask(args: argparse.Namespace) -> int:
|
|
import os
|
|
|
|
from aborist.qa import ask
|
|
from aborist.qa.client import OpenAICompatibleClient, StubClient
|
|
|
|
base_url = args.endpoint or os.environ.get(
|
|
"ABORIST_LLM_ENDPOINT", "https://hermes.ai.unturf.com/v1"
|
|
)
|
|
model = args.model or os.environ.get(
|
|
"ABORIST_LLM_MODEL",
|
|
"adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
|
|
)
|
|
revision = os.environ.get("ABORIST_LLM_REVISION", "")
|
|
quantization = os.environ.get("ABORIST_LLM_QUANTIZATION", "fp8-dynamic")
|
|
api_key = os.environ.get("ABORIST_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)
|
|
)
|
|
try:
|
|
result = ask(
|
|
conn,
|
|
document_root=args.document_root,
|
|
question=args.question,
|
|
client=client,
|
|
model_id=model,
|
|
revision=revision,
|
|
quantization=quantization,
|
|
)
|
|
finally:
|
|
conn.close()
|
|
print(json.dumps(result, indent=2))
|
|
return 0 if result.get("status") in ("cache_hit", "cache_miss_then_written") else 1
|
|
|
|
|
|
def _cmd_query(args: argparse.Namespace) -> int:
|
|
"""Multi-source RAG: question -> top-K corpus docs -> Hermes -> cache."""
|
|
import os
|
|
|
|
from aborist.qa.client import OpenAICompatibleClient, StubClient
|
|
from aborist.qa.query import query
|
|
|
|
base_url = args.endpoint or os.environ.get(
|
|
"ABORIST_LLM_ENDPOINT", "https://hermes.ai.unturf.com/v1"
|
|
)
|
|
model = args.model or os.environ.get(
|
|
"ABORIST_LLM_MODEL",
|
|
"adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
|
|
)
|
|
revision = os.environ.get("ABORIST_LLM_REVISION", "")
|
|
quantization = os.environ.get("ABORIST_LLM_QUANTIZATION", "fp8-dynamic")
|
|
api_key = os.environ.get("ABORIST_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() / ".aborist" / "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
|
|
|
|
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,
|
|
top_k=args.top_k,
|
|
over_fetch=args.over_fetch,
|
|
max_context_chars=args.max_context_chars,
|
|
)
|
|
|
|
print(json.dumps(result, indent=2))
|
|
return (
|
|
0
|
|
if result.get("status") in ("cache_hit", "cache_miss_then_written")
|
|
else 1
|
|
)
|
|
|
|
|
|
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 aborist.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 _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))
|
|
return 0 if result.get("status") == "falsified" else 1
|
|
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))
|
|
return 0
|
|
|
|
|
|
def _cmd_evict(args: argparse.Namespace) -> int:
|
|
from aborist.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))
|
|
return 0
|
|
|
|
|
|
def _cmd_rehydrate(args: argparse.Namespace) -> int:
|
|
from aborist.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))
|
|
drift = sum(1 for r in results if r.get("status") == "drift_detected")
|
|
return 1 if drift else 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 aborist.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,
|
|
)
|
|
)
|
|
return 0
|
|
|
|
|
|
def _cmd_stats(args: argparse.Namespace) -> int:
|
|
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))
|
|
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 aborist.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):
|
|
sc = connect(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).
|
|
gravity = conn.execute(
|
|
"SELECT dst_uri, COUNT(*) AS inbound FROM edges "
|
|
"WHERE edge_type='wikilink' AND dst_uri != '' "
|
|
"GROUP BY dst_uri ORDER BY inbound DESC LIMIT ?",
|
|
(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": [
|
|
{"uri": r["dst_uri"], "inbound": r["inbound"]} for r in gravity
|
|
],
|
|
}
|
|
finally:
|
|
conn.close()
|
|
print(json.dumps(report, indent=2))
|
|
return 0
|
|
|
|
|
|
def _cmd_mesh_status(args: argparse.Namespace) -> int:
|
|
from aborist.mesh import current_epoch, is_enabled, load_identity
|
|
from aborist.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))
|
|
return 0
|
|
|
|
|
|
def _cmd_mesh_init(args: argparse.Namespace) -> int:
|
|
from aborist.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,
|
|
)
|
|
)
|
|
return 0
|
|
|
|
|
|
def _cmd_mesh_enable(args: argparse.Namespace) -> int:
|
|
from aborist.mesh import set_enabled
|
|
|
|
conn = connect(args.db)
|
|
try:
|
|
set_enabled(conn, True)
|
|
finally:
|
|
conn.close()
|
|
print(json.dumps({"enabled": True}, indent=2))
|
|
return 0
|
|
|
|
|
|
def _cmd_mesh_disable(args: argparse.Namespace) -> int:
|
|
from aborist.mesh import set_enabled
|
|
|
|
conn = connect(args.db)
|
|
try:
|
|
set_enabled(conn, False)
|
|
finally:
|
|
conn.close()
|
|
print(json.dumps({"enabled": False}, indent=2))
|
|
return 0
|
|
|
|
|
|
def _cmd_mesh_members(args: argparse.Namespace) -> int:
|
|
from aborist.mesh import current_epoch
|
|
from aborist.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))
|
|
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,
|
|
)
|
|
)
|
|
return 0
|
|
|
|
|
|
def _cmd_mesh_add(args: argparse.Namespace) -> int:
|
|
from aborist.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))
|
|
return 0
|
|
|
|
|
|
def _cmd_mesh_kick(args: argparse.Namespace) -> int:
|
|
from aborist.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,
|
|
)
|
|
)
|
|
return 0
|
|
|
|
|
|
def _cmd_mesh_rotate(args: argparse.Namespace) -> int:
|
|
from aborist.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))
|
|
return 0
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
p = argparse.ArgumentParser(
|
|
prog="aborist",
|
|
description="An arborist for trees and forests of cross-linked information.",
|
|
)
|
|
p.add_argument("--version", action="version", version=f"aborist {__version__}")
|
|
p.add_argument(
|
|
"--db",
|
|
type=Path,
|
|
default=DEFAULT_DB_PATH,
|
|
help=f"path to aborist 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)
|
|
|
|
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",
|
|
],
|
|
help="source type",
|
|
)
|
|
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(
|
|
"--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(
|
|
"--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 aborist --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.set_defaults(func=_cmd_ingest)
|
|
|
|
search = sub.add_parser("search", help="keyword search (VISUAL 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.set_defaults(func=_cmd_search)
|
|
|
|
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 $ABORIST_LLM_MODEL or hermes-3)",
|
|
)
|
|
ask_cmd.add_argument(
|
|
"--endpoint",
|
|
default=None,
|
|
help="OpenAI-compatible base URL (default $ABORIST_LLM_ENDPOINT)",
|
|
)
|
|
ask_cmd.add_argument(
|
|
"--dry-run",
|
|
dest="dry_run",
|
|
action="store_true",
|
|
help="use StubClient — no network call",
|
|
)
|
|
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(
|
|
"--max-context-chars", dest="max_context_chars", type=int, default=60000,
|
|
help="cap on assembled context bytes (default 60000)",
|
|
)
|
|
query_cmd.add_argument(
|
|
"--qa-db", dest="qa_db", default=None,
|
|
help=(
|
|
"providence_cache target DB. default: <shards-dir>/qa.db, or "
|
|
"~/.aborist/qa.db when no shards-dir"
|
|
),
|
|
)
|
|
query_cmd.add_argument(
|
|
"--model", default=None,
|
|
help="model_id (default $ABORIST_LLM_MODEL or hermes-3)",
|
|
)
|
|
query_cmd.add_argument(
|
|
"--endpoint", default=None,
|
|
help="OpenAI-compatible base URL (default $ABORIST_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.set_defaults(func=_cmd_query)
|
|
|
|
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.set_defaults(func=_cmd_providence)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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 URIs to report (default 10)",
|
|
)
|
|
analyze_cmd.set_defaults(func=_cmd_analyze)
|
|
|
|
# ----- 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)
|
|
|
|
return p
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = build_parser().parse_args(argv)
|
|
return args.func(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|