arborist-viz/arborist_viz/views/audit.py
russell@unturf.com 0cd96ab1d7
Initial commit: arborist VIZ — Merkle command center
Pyramid web app for inspecting arborist Merkle trees over sharded SQLite:
document roots, inclusion proofs, an interactive 3D Merkle lattice
(six.js / WebGL with graceful fallback), audit-chain linkage, and a live
SSE event stream.

Generated artifacts (env/, vendored static/six/ bundles, demo databases)
are gitignored and rebuilt via the Makefile (venv, six, init-db, serve).
2026-05-28 09:22:59 -04:00

276 lines
9.3 KiB
Python

"""Audit event + chain endpoints (ticket §6.6, §13).
All access goes through ``arborist.read.Shards`` via ``request.shards``.
VIZ never touches arborist's tables, columns, or helper modules directly.
"""
from __future__ import annotations
from pyramid.response import Response
from pyramid.view import view_config
def _json(body, status=200) -> Response:
return Response(json_body=body, status=status)
def _no_shard() -> Response:
return _json(
{"error": "no_shard", "note": "configure arborist.shards"}, status=503
)
def _audit_to_json(ev) -> dict:
"""Flatten an ``arborist.read.AuditEvent`` to the VIZ JSON shape."""
return {
"seq": ev.seq,
"event_hash": ev.event_hash,
"prev_event_hash": ev.prev_event_hash,
"event_type": ev.event_type,
"subject_root": ev.subject_root,
"body": ev.body,
"ts": ev.ts,
"shard": ev.shard_path,
}
@view_config(route_name="api_audit_event", request_method="GET")
def get_audit_event(request):
if request.shards is None:
return _no_shard()
event_hash = request.matchdict["event_hash"]
ev = request.shards.audit_event(event_hash)
if ev is None:
return _json({"error": "not_found", "event_hash": event_hash}, status=404)
return _json(
{
**_audit_to_json(ev),
"source_roots": [ev.subject_root] if ev.subject_root else [],
}
)
@view_config(route_name="api_audit_chain", request_method="GET")
def get_audit_chain(request):
if request.shards is None:
return _no_shard()
event_hash = request.matchdict["event_hash"]
try:
limit = int(request.GET.get("limit", "100"))
except ValueError:
limit = 100
events = request.shards.audit_chain(event_hash, limit=limit)
return _json(
{
"head_event_hash": event_hash,
"limit": limit,
"events": [_audit_to_json(e) for e in events],
"source_roots": list({e.subject_root for e in events if e.subject_root}),
}
)
@view_config(route_name="api_audit_by_root", request_method="GET")
def get_audit_by_root(request):
"""Audit events whose subject_root matches — uses the indexed lookup."""
if request.shards is None:
return _no_shard()
root_hash = request.matchdict["root_hash"]
try:
limit = int(request.GET.get("limit", "50"))
except ValueError:
limit = 50
events = request.shards.audit_by_root(root_hash, limit=limit)
return _json(
{
"root_hash": root_hash,
"limit": limit,
"count": len(events),
"events": [_audit_to_json(e) for e in events],
"source_roots": [root_hash],
}
)
@view_config(route_name="api_audit_recent", request_method="GET")
def get_audit_recent(request):
"""Recent audit events across every configured shard."""
if request.shards is None:
return _no_shard()
try:
limit = int(request.GET.get("limit", "100"))
except ValueError:
limit = 100
events = request.shards.audit_recent(limit=limit)
return _json(
{
"limit": limit,
"count": len(events),
"events": [_audit_to_json(e) for e in events],
"source_roots": list({e.subject_root for e in events if e.subject_root}),
}
)
@view_config(route_name="api_run", request_method="GET")
def get_run(request):
"""Run-DAG — arborist's ingest pipeline stages.
Phase 0 returned a static stage list; with shard wiring we surface the
canonical stages from §6.5 plus the most recent ingest's audit event
as the visualized run.
"""
run_id = request.matchdict["run_id"]
stages = [
"Question", "Preflight", "Retrieval", "EvidenceMap", "AnswerDraft",
"ClaimExtraction", "Verifier", "FalsifierSearch", "CacheWrite",
"AuditAppend", "StateAdmission",
]
latest_event = None
if request.shards is not None:
events = request.shards.audit_recent(limit=1)
if events:
latest_event = _audit_to_json(events[0])
return _json(
{
"run_id": run_id,
"stages": stages,
"edges": [{"source": stages[i], "target": stages[i + 1]} for i in range(len(stages) - 1)],
"latest_audit_event": latest_event,
"source_roots": [latest_event["subject_root"]] if latest_event and latest_event.get("subject_root") else [],
}
)
@view_config(route_name="api_cache", request_method="GET")
def get_cache_entry(request):
"""Look up one Q&A record by its cache_key (hex hash).
Source layout (single file vs separate provenance shard, etc.) is an
arborist internal — VIZ asks the read seam and trusts what comes back.
"""
if request.shards is None:
return _no_shard()
cache_key = request.matchdict["cache_key"]
qa = request.shards.qa(cache_key)
if qa is None:
return _json(
{"error": "not_found", "cache_key": cache_key, "source_roots": []},
status=404,
)
return _json(
{
"cache_key": qa.cache_key,
"source_root": qa.source_root,
"document_uri": qa.document_uri,
"question_hash": qa.question_hash,
"question_text": qa.question_text,
"answer_text": qa.answer_text,
"model_profile_hash": qa.model_profile_hash,
"conversation_hash": qa.conversation_hash,
"governance_policy_hash": qa.governance_policy_hash,
"schema_version": qa.schema_version,
"canonicalization_version": qa.canonicalization_version,
"chunking_version": qa.chunking_version,
"falsification_state": qa.falsification_state,
"audit_mode": qa.audit_mode,
"audit_event_hash": qa.audit_event_hash,
"run_dag_root": qa.run_dag_root,
"created_at": qa.created_at,
"last_hit_at": qa.last_hit_at,
"hit_count": qa.hit_count,
"n_quotes": qa.n_quotes,
"n_verified": qa.n_verified,
"verifier_method": qa.verifier_method,
"merkle_proof": qa.merkle_proof,
"run_dag_blob": qa.run_dag_blob,
"shard": qa.shard_path,
"source_roots": [qa.source_root] if qa.source_root else [],
}
)
@view_config(route_name="api_fork", request_method="GET")
def get_fork(request):
fork_id = request.matchdict["fork_id"]
return _json({"fork_id": fork_id, "score": None, "source_roots": [], "note": "fork projection not wired in this phase."})
@view_config(route_name="api_diff", request_method="GET")
def get_diff(request):
"""Root diff — leaves added/removed between two roots (across all shards)."""
if request.shards is None:
return _no_shard()
root_a = request.matchdict["root_a"]
root_b = request.matchdict["root_b"]
if request.shards.root(root_a) is None or request.shards.root(root_b) is None:
return _json({"error": "not_found", "root_a": root_a, "root_b": root_b}, status=404)
# Diff cares only about leaf hashes — skip wikitext projection and
# never decompress private content.
leaves_a = {L.leaf_hash: L.idx for L in request.shards.leaves(root_a, project=False)}
leaves_b = {L.leaf_hash: L.idx for L in request.shards.leaves(root_b, project=False)}
added = sorted(set(leaves_b) - set(leaves_a))
removed = sorted(set(leaves_a) - set(leaves_b))
common = sorted(set(leaves_a) & set(leaves_b))
return _json(
{
"root_a": root_a,
"root_b": root_b,
"added_leaves": added,
"removed_leaves": removed,
"shared_leaves": common,
"leaf_count_a": len(leaves_a),
"leaf_count_b": len(leaves_b),
"source_roots": [root_a, root_b],
}
)
@view_config(route_name="api_graph_snapshot", request_method="GET")
def get_graph_snapshot(request):
if request.shards is None:
return _no_shard()
roots = request.shards.roots(limit=200)
nodes = [{"id": r.document_root, "kind": "document_root", "label": r.title or r.document_uri} for r in roots]
return _json(
{
"seq": request.shards.counts().audit_events,
"nodes": nodes,
"edges": [],
"source_roots": [r.document_root for r in roots],
}
)
# ---- #000062 gate -------------------------------------------------------
@view_config(route_name="api_trace", request_method="GET")
def get_trace(request):
trace_id = request.matchdict["trace_id"]
return Response(
json_body={
"error": "not_implemented",
"trace_id": trace_id,
"gate": "arborist #000062 — MechanisticWitnessRoot",
"note": (
"Activation/circuit traces only displayable once arborist's "
"MechanisticWitnessRoot lands. Soft signals must never enter "
"the proof path."
),
},
status=501,
)
@view_config(route_name="api_circuit", request_method="GET")
def get_circuit(request):
circuit_id = request.matchdict["circuit_id"]
return Response(
json_body={
"error": "not_implemented",
"circuit_id": circuit_id,
"gate": "arborist #000062 — MechanisticWitnessRoot",
},
status=501,
)