diff --git a/Makefile b/Makefile index edf3c45..e70af52 100644 --- a/Makefile +++ b/Makefile @@ -114,6 +114,10 @@ ACTIVITY_LIMIT ?= 10 activity: bootstrap ## recent Q&A + freshly cached docs (agent timeline) $(ABORIST) --shards-dir $(SHARDS_DIR) activity --limit $(ACTIVITY_LIMIT) +falsify: bootstrap ## mark a cached answer wrong: make falsify KEY=hex REASON='why' + @if [ -z "$(KEY)" ]; then echo "usage: make falsify KEY= REASON='why'" >&2; exit 2; fi + $(ABORIST) --shards-dir $(SHARDS_DIR) providence --falsify $(KEY) --reason "$(REASON)" + # Multi-source RAG query against the shard cluster. # Usage: make query Q="What is anarcho-capitalism?" QUERY_TOP_K ?= 8 diff --git a/aborist/cli.py b/aborist/cli.py index cb0fbb0..5ae44fa 100644 --- a/aborist/cli.py +++ b/aborist/cli.py @@ -4,6 +4,7 @@ from __future__ import annotations import argparse import json +import os import sys from pathlib import Path @@ -323,8 +324,99 @@ def _cmd_query(args: argparse.Namespace) -> int: ) +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 for a document URI or source_root.""" + """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 @@ -978,11 +1070,37 @@ def build_parser() -> argparse.ArgumentParser: prov_cmd = sub.add_parser( "providence", - help="list providence_cache records", + 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(