#!/usr/bin/env python3 """Scripted walkthrough of the canonical FablePool demo flow. ask -> why -> refute -> cascade -> correct -> delegated export -> audit This script narrates the milestone's end-state in one reproducible run: 1. Bootstrap a fresh user-owned node and ingest the bundled sample evidence (calendar, notes, mock photo metadata). 2. Ask the node the canonical question: "what do you know about me and why?" 3. Drill into one claim's provenance, down to raw evidence. 4. Refute that claim and verify the cascade: every downstream claim that was derived from it is invalidated. 5. Correct a different claim and verify the corrected value surfaces. 6. Export the full graph in the wire format, then export only one topic — the subset a delegated third-party node would be authorized to receive. 7. Audit the signed, append-only operation log. Run it from a checkout with the package installed (``pip install -e .``): python scripts/demo_walkthrough.py python scripts/demo_walkthrough.py --home /tmp/my-demo-node Every step asserts the behaviour it claims; the script exits non-zero if any expectation fails, so it doubles as an end-to-end smoke test. """ from __future__ import annotations import argparse import contextlib import io import json import sys import tempfile from pathlib import Path from typing import Dict, List, Optional, Sequence, Tuple from fablepool.cli import main as cli_main WIDTH = 72 # Status vocabularies, kept slightly tolerant so the narration stays honest # even if the model layer uses a close synonym. REFUTED = {"refuted"} INVALIDATED = {"invalidated", "stale", "refuted"} CORRECTED = {"corrected", "superseded"} def banner(title: str) -> None: print("\n" + "=" * WIDTH) print(title) print("=" * WIDTH) def say(text: str) -> None: print(f"\n{text}") def expect(condition: bool, message: str) -> None: if condition: print(f" [ok] {message}") else: print(f" [FAIL] {message}") raise SystemExit(f"demo check failed: {message}") def run(home: Path, *args: str, capture: bool = False) -> str: """Run one CLI command, echoing it like a terminal transcript.""" printable = " ".join(str(a) for a in args) print(f"\n$ fablepool {printable}") buf = io.StringIO() try: with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf): code = cli_main(["--home", str(home), *[str(a) for a in args]]) except SystemExit as exc: code = exc.code if isinstance(exc.code, int) else 1 code = 0 if code is None else int(code) out = buf.getvalue() if capture: print(f" ({len(out.splitlines())} line(s) of output captured)") else: sys.stdout.write(out) if code != 0: if capture: sys.stdout.write(out) raise SystemExit(f"\ndemo aborted: 'fablepool {printable}' exited {code}") return out # --------------------------------------------------------------------------- # # JSON helpers (mirrors of the CLI's --json contract) # --------------------------------------------------------------------------- # def _claim_list(text: str) -> List[dict]: data = json.loads(text) if isinstance(data, dict): for key in ("claims", "items", "results"): if key in data and isinstance(data[key], list): data = data[key] break if not isinstance(data, list): raise SystemExit("unexpected claims --json shape") return data def get_claims(home: Path, *extra: str) -> List[dict]: return _claim_list(run(home, "claims", "--json", *extra, capture=True)) def status_of(home: Path, claim_id: str) -> str: data = json.loads(run(home, "show", claim_id, "--json", capture=True)) if isinstance(data, dict) and isinstance(data.get("claim"), dict): data = data["claim"] return str(data.get("status", "?")) def claim_inputs(claim: dict) -> List[str]: refs = claim.get("inputs") or claim.get("derived_from") or [] return [str(r) for r in refs] def pick_refutation_target(claims: List[dict]) -> Tuple[str, List[str]]: """Pick the claim with the most downstream dependents, plus those dependents.""" by_id = {str(c["id"]): c for c in claims} dependents: Dict[str, List[str]] = {} for c in claims: cid = str(c["id"]) for ref in claim_inputs(c): if ref in by_id and ref != cid: dependents.setdefault(ref, []).append(cid) if dependents: target = max(dependents, key=lambda k: len(dependents[k])) return target, dependents[target] return str(claims[0]["id"]), [] def describe(claims: List[dict], claim_id: str) -> str: for c in claims: if str(c["id"]) == claim_id: return f"{claim_id[:12]} [{c.get('topic', '-')}] {c.get('statement', '')}" return claim_id[:12] # --------------------------------------------------------------------------- # # The walkthrough # --------------------------------------------------------------------------- # def main(argv: Optional[Sequence[str]] = None) -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument( "--home", help="node home directory (default: a fresh temporary directory)", ) ns = parser.parse_args(argv) home = Path(ns.home) if ns.home else Path(tempfile.mkdtemp(prefix="fablepool-demo-")) banner("STEP 0 — bootstrap a user-owned node and ingest sample evidence") say( "We create a brand-new node (fresh identity, empty signed log) and feed\n" "it the bundled sample dataset: calendar events, notes, and mock photo\n" "metadata. The derivation engine then produces claims with provenance." ) run(home, "init") run(home, "seed") banner('STEP 1 — ask: "what do you know about me and why?"') say( "The canonical question. The node answers from its own derived claims,\n" "grouped by topic, each with a confidence and a pointer to its 'why'." ) run(home, "ask", "what do you know about me and why?") claims = get_claims(home) expect(len(claims) > 0, f"the seeded node holds {len(claims)} active claim(s)") target, downstream = pick_refutation_target(claims) expect( len(downstream) > 0, "the seed graph contains at least one claim derived from another claim " "(needed to demonstrate the cascade)", ) banner("STEP 2 — drill into one claim's provenance") say( "We pick a claim that other claims were derived from, and walk its\n" "derivation explanation down to the raw evidence records:\n" f" target: {describe(claims, target)}\n" + "".join(f" depends on it: {describe(claims, d)}\n" for d in downstream) ) run(home, "show", target) run(home, "why", target) banner("STEP 3 — refute the claim; watch the cascade invalidate downstream") say( "The user says: 'that is simply not true.' The refutation is appended\n" "to the signed log, and every claim derived from the refuted one is\n" "mechanically invalidated — corrections cascade, they don't linger." ) run(home, "refute", target, "--reason", "The user says this is not true.") expect( status_of(home, target) in REFUTED, f"claim {target[:12]} is now refuted", ) for dep in downstream: expect( status_of(home, dep) in INVALIDATED, f"downstream claim {dep[:12]} was invalidated by the cascade", ) banner("STEP 4 — correct a different claim instead of refuting it") say( "Refuting kills a claim; correcting replaces its value. We correct one\n" "of the remaining active claims and verify the corrected value shows up." ) remaining = [ c for c in get_claims(home) if str(c["id"]) != target and str(c["id"]) not in set(downstream) ] expect(len(remaining) > 0, "active claims remain after the refutation") to_correct = str(remaining[0]["id"]) marker = "Corrected-by-walkthrough" run( home, "correct", to_correct, "--value", marker, "--reason", "The user supplied the accurate value.", ) expect( status_of(home, to_correct) in CORRECTED | REFUTED, f"original claim {to_correct[:12]} is marked corrected/superseded", ) all_after = run(home, "claims", "--json", "--all", capture=True) expect(marker in all_after, "the corrected value surfaces in a successor claim") banner("STEP 5 — export the full graph, then a delegated topic subset") say( "Export writes signed operations in the wire format, so any conforming\n" "implementation can ingest them. A delegated third party never gets the\n" "whole graph — only the authorized slice (here: one topic)." ) full_path = home / "export-full.fpl" run(home, "export", "--out", str(full_path)) active = get_claims(home) subset_topic = str(active[0]["topic"]) subset_path = home / f"export-{subset_topic}.fpl" run(home, "export", "--topic", subset_topic, "--out", str(subset_path)) expect(full_path.exists() and full_path.stat().st_size > 0, "full export written") expect( subset_path.exists() and subset_path.stat().st_size > 0, f"delegated subset export written (topic: {subset_topic!r})", ) expect( subset_path.stat().st_size <= full_path.stat().st_size, "the delegated subset is no larger than the full graph", ) banner("STEP 6 — audit the signed, append-only operation log") say( "Everything above — ingestion, derivation, refutation, correction,\n" "export — is an operation in the log. Audit verifies signatures and\n" "the hash chain end to end." ) run(home, "audit") run(home, "log", "--limit", "12") banner("DEMO COMPLETE") print( f""" The user asked what the node knows and why, drilled into provenance, refuted a claim and watched the cascade, corrected another, exported the full graph, and produced a delegated subset a third party could receive. node home: {home} full export: {full_path} delegated export: {subset_path} Re-run any command above by hand with: fablepool --home {home} """ ) return 0 if __name__ == "__main__": raise SystemExit(main())