From 4573dbcf4e28ecb3e121e721f850a9a1766281b7 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Wed, 29 Apr 2026 16:56:59 -0400 Subject: [PATCH] =?UTF-8?q?cli:=20'aborist=20burn-kindergarten'=20?= =?UTF-8?q?=E2=80=94=20mass-burn=20fresh=20providence=20rows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per fox: useful while iterating on retrieval/verifier knobs to wipe recent test runs without finding each cache_key. Mirrors the mesh-sync kindergarten window so what's still un-broadcast (private to this peer) is exactly what's safe to bust without confusing peers. Surface: aborist burn-kindergarten [--kindergarten-seconds N] [--reason '...'] [--by-actor X] [--force] [--dry-run] [--verbose N] make burn-kindergarten [SECONDS=3600] [FORCE=1] [DRY_RUN=1] [REASON='why'] Behavior: - Walks every shard, finds providence_cache rows with created_at >= now - SECONDS and falsification_state='live'. - Each row goes through the existing _burn_cache_key — children gate honored unless --force, audit event written per successful burn, chain integrity preserved. - Result JSON reports examined / burned / refused_has_children / not_found counts plus a verbose tail of per-row results. - --dry-run reports without writing or auditing. - --kindergarten-seconds 0 = burn every live row (test reset). Tests (4): selective burn (old kept, fresh burned), dry-run writes nothing, 0-second window burns everything, children-gate refusal without --force. 353 passed, 1 skipped. Operational note: this command does NOT propagate to peers (burn is local kindergarten cleanup by design, see docs/mesh.md). If you want the remote effect, falsify each record individually & let mesh sync broadcast the falsifications instead. --- Makefile | 14 ++++- aborist/cli.py | 126 ++++++++++++++++++++++++++++++++++++++ tests/test_burn.py | 147 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 286 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index be4c39a..63339d3 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ SEARCH_Q ?= computer ingest-grok ingest-grok-media \ ingest-self ingest-git ingest-hg \ verify search stats test docs chain-check chain-check-shards \ - falsify burn inspect bootstrap-crawler test-crawler crawl-ingest \ + falsify burn burn-kindergarten inspect bootstrap-crawler test-crawler crawl-ingest \ recrawl-check clean clean-db clean-data help all: bootstrap fetch-cur ingest-cur verify stats ## bootstrap → fetch cur → ingest cur → verify → stats @@ -140,6 +140,18 @@ burn: bootstrap ## delete a leaf with no children. providence: KEY=; echo "unknown KIND: $$kind (expected: providence|document|core)" >&2; exit 2; \ fi +# Mass-burn providence_cache rows younger than the kindergarten window. +# Mirrors mesh sync's kindergarten so what's still un-broadcast is what's +# safe to bust without confusing peers. Useful while iterating on +# retrieval/verifier tunings — wipe recent test runs in one shot. +KG_SECONDS ?= 3600 +burn-kindergarten: bootstrap ## bust providence rows < SECONDS old [SECONDS=3600 FORCE=1 DRY_RUN=1 REASON='why'] + $(ABORIST) --shards-dir $(SHARDS_DIR) burn-kindergarten \ + --kindergarten-seconds $(KG_SECONDS) \ + $(if $(REASON),--reason "$(REASON)",) \ + $(if $(FORCE),--force,) \ + $(if $(DRY_RUN),--dry-run,) + # Multi-source RAG query against the shard cluster. # Usage: make query Q="What is anarcho-capitalism?" QUERY_TOP_K ?= 8 diff --git a/aborist/cli.py b/aborist/cli.py index e5afcc3..9874790 100644 --- a/aborist/cli.py +++ b/aborist/cli.py @@ -1022,6 +1022,91 @@ def _burn_core_root( 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 aborist.store import discover_shards + + now = int(_time.time()) + cutoff = now - max(0, args.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: + 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. @@ -2849,6 +2934,47 @@ def build_parser() -> argparse.ArgumentParser: ) 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 " diff --git a/tests/test_burn.py b/tests/test_burn.py index 0383c50..49142bf 100644 --- a/tests/test_burn.py +++ b/tests/test_burn.py @@ -274,6 +274,153 @@ def test_burn_cli_invocation(tmp_path, capsys): assert payload["reason"] == "via cli" +def test_burn_kindergarten_burns_only_recent_rows(tmp_path, capsys): + """Mass-burn rows younger than the window. Older rows are kept; the + test seeds two rows, backdates one to 2h ago, and asserts the + fresh one burns and the old one stays.""" + import time as _time + from aborist.cli import build_parser + from aborist.store import transaction + + db = tmp_path / "qa.db" + OLD = "ab" * 32 + FRESH = "cd" * 32 + _seed_providence_record(db, cache_key=OLD, question="old", audit_mode="STRICT") + _seed_providence_record(db, cache_key=FRESH, question="fresh", audit_mode="STRICT") + + # Backdate OLD to 2h ago. + long_ago = int(_time.time()) - 7200 + conn = connect(db) + try: + with transaction(conn): + conn.execute( + "UPDATE providence_cache SET created_at = ? WHERE cache_key = ?", + (long_ago, OLD), + ) + finally: + conn.close() + + parser = build_parser() + args = parser.parse_args([ + "--db", str(db), + "burn-kindergarten", + "--kindergarten-seconds", "3600", + ]) + rc = args.func(args) + assert rc == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["status"] == "burned" + assert payload["examined"] == 1 # only FRESH met the cutoff + assert payload["burned"] == 1 + + conn = connect(db) + try: + rows = {r["cache_key"]: r for r in conn.execute( + "SELECT cache_key FROM providence_cache" + ).fetchall()} + finally: + conn.close() + assert OLD in rows + assert FRESH not in rows + + +def test_burn_kindergarten_dry_run_writes_nothing(tmp_path, capsys): + """--dry-run reports what would burn but doesn't write or audit.""" + from aborist.cli import build_parser + + db = tmp_path / "qa.db" + KEY = "ef" * 32 + _seed_providence_record(db, cache_key=KEY) + conn = connect(db) + try: + events_before = conn.execute( + "SELECT COUNT(*) FROM audit_events" + ).fetchone()[0] + finally: + conn.close() + + parser = build_parser() + args = parser.parse_args([ + "--db", str(db), + "burn-kindergarten", + "--kindergarten-seconds", "3600", + "--dry-run", + ]) + rc = args.func(args) + assert rc == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["status"] == "dry_run" + assert payload["examined"] == 1 + assert payload["burned"] == 0 + # Row still exists; audit chain unchanged. + conn = connect(db) + try: + still_there = conn.execute( + "SELECT 1 FROM providence_cache WHERE cache_key=?", (KEY,) + ).fetchone() + events_after = conn.execute( + "SELECT COUNT(*) FROM audit_events" + ).fetchone()[0] + finally: + conn.close() + assert still_there is not None + assert events_after == events_before + + +def test_burn_kindergarten_zero_seconds_burns_everything(tmp_path, capsys): + """0-second window = burn every live providence_cache row.""" + from aborist.cli import build_parser + + db = tmp_path / "qa.db" + K1 = "11" * 32 + K2 = "22" * 32 + _seed_providence_record(db, cache_key=K1, question="q1") + _seed_providence_record(db, cache_key=K2, question="q2") + + parser = build_parser() + args = parser.parse_args([ + "--db", str(db), + "burn-kindergarten", + "--kindergarten-seconds", "0", + ]) + rc = args.func(args) + assert rc == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["burned"] == 2 + + conn = connect(db) + try: + remaining = conn.execute("SELECT COUNT(*) FROM providence_cache").fetchone()[0] + finally: + conn.close() + assert remaining == 0 + + +def test_burn_kindergarten_respects_children_gate_without_force(tmp_path, capsys): + """A row with falsifications is REFUSED unless --force. Audit chain + still grows on burned rows; refused rows leave no audit entry.""" + from aborist.cli import build_parser + + db = tmp_path / "qa.db" + BURNABLE = "33" * 32 + HAS_KIDS = "44" * 32 + _seed_providence_record(db, cache_key=BURNABLE) + _seed_providence_record(db, cache_key=HAS_KIDS) + _seed_falsification(db, cache_key=HAS_KIDS) + + parser = build_parser() + args = parser.parse_args([ + "--db", str(db), + "burn-kindergarten", + "--kindergarten-seconds", "3600", + ]) + rc = args.func(args) + assert rc == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["burned"] == 1 + assert payload["refused_has_children"] == 1 + + def test_burn_cli_returns_non_zero_on_refused(tmp_path, capsys): """Non-zero exit when refused so scripts can detect and react.""" from aborist.cli import build_parser