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).
276 lines
9.3 KiB
Python
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,
|
|
)
|