diff --git a/arborist_viz/templates/sessions_tree.jinja2 b/arborist_viz/templates/sessions_tree.jinja2 index 726014c..5fc6b67 100644 --- a/arborist_viz/templates/sessions_tree.jinja2 +++ b/arborist_viz/templates/sessions_tree.jinja2 @@ -21,21 +21,21 @@ {% endif %} {% if node.answer_text %} -
+
answer ({{ node.answer_text|length }} chars)
{{ node.answer_text }}
{% endif %} -
+
hashes / metadata
cache_key
-
{% if node.cache_key %}{{ node.cache_key }}{% else %}(none — canonical_projection){% endif %}
+
{% if node.cache_key %}{{ node.cache_key }}{% else %}(none — canonical_projection){% endif %}
node_hash
-
{{ node.node_hash }}
+
{{ node.node_hash }}
subtree_hash
-
{{ node.subtree_hash }}
+
{{ node.subtree_hash }}
created
{{ node.created_at }}
{% if node.parent_bates %} diff --git a/arborist_viz/views/home.py b/arborist_viz/views/home.py index dcf8578..4cc5a88 100644 --- a/arborist_viz/views/home.py +++ b/arborist_viz/views/home.py @@ -54,22 +54,133 @@ def dashboard_page(request): } -@view_config(route_name="proof_page", renderer="proof.jinja2") +@view_config(route_name="proof_page") def proof_page(request): - """Single-root visualizer — drops a Merkle hash into a 3D lattice + - inclusion proof card + leaf list. A ``?cache_key=...`` query param - threads a QA record through so synthetic context roots can show the - question + answer alongside their evidence manifest.""" - root_hash = request.matchdict["root_hash"] + """Single-root visualizer. Dispatches by hash kind: + + - corpus document_root / leaf / merkle interior → proof.jinja2 + (the 3D Merkle lattice + inclusion proof + leaf-list view) + - session node_hash / subtree_hash → sessions_tree.jinja2 + (the conversation-tree view, scrolled to the matching node) + - providence cache_key → cache_key.jinja2 + (every session node referencing this cache hit) + + All hashes drop through ONE URL — `/proof/{hash}` — per the + ticket's "never mint new kind-named URLs" discipline. + """ + from pyramid.renderers import render_to_response + + root_hash = request.matchdict["root_hash"].strip().lower() leaf_param = request.GET.get("leaf") cache_key = request.GET.get("cache_key") - return { + + # Sessions dispatch — check before falling through to the corpus + # proof renderer. A session_root or session node_hash here means + # the hash IS the answer; the corpus proof page can't help. + sessions = request.sessions + if sessions is not None and sessions.is_open: + # session cache_key? show all referencing nodes. + ck_hits = sessions.find_by_cache_key(root_hash) + if ck_hits: + ctx = _common_ctx(request) + ctx["title"] = "Arborist VIZ — cache_key {0}".format( + root_hash[:12] + ) + ctx["cache_key"] = root_hash + ctx["hits"] = ck_hits + ctx["sessions_stats"] = _sessions_stats_for_ctx(request) + return render_to_response( + "cache_key.jinja2", ctx, request=request + ) + # session node hash (node_hash or subtree_hash)? + node = _find_session_node_by_hash(sessions, root_hash) + if node is not None: + return _render_session_in_proof( + request, node.sid, focus_bates=node.bates, + ) + # session_root? (= subtree_hash of a session's root node) + sid_for_root = _find_sid_by_session_root(sessions, root_hash) + if sid_for_root is not None: + return _render_session_in_proof(request, sid_for_root) + + # Default: corpus root. Hand off to proof.jinja2. + ctx = { "title": "Arborist VIZ — root {0}".format(root_hash[:12]), "root_hash": root_hash, "leaf_index": leaf_param, "cache_key": cache_key, **_common_ctx(request), } + return render_to_response("proof.jinja2", ctx, request=request) + + +def _sessions_stats_for_ctx(request) -> dict: + s = request.sessions + if s is None or not s.is_open: + return {"n_sessions": 0, "n_nodes": 0, + "n_audit_events": 0, "open": False} + c = s.counts() + return { + "n_sessions": c.n_sessions, "n_nodes": c.n_nodes, + "n_audit_events": c.n_audit_events, + "open": True, "path": c.shard_path, + } + + +def _find_session_node_by_hash(sessions, h: str): + """Lookup by node_hash OR subtree_hash. Returns first match or None.""" + try: + r = sessions._conn.execute( # type: ignore[attr-defined] + "SELECT * FROM nodes WHERE node_hash=? OR subtree_hash=? " + "LIMIT 1", + (h, h), + ).fetchone() + except Exception: + return None + if not r: + return None + from arborist.read import _row_to_session_node + return _row_to_session_node(r) + + +def _find_sid_by_session_root(sessions, h: str): + """A session_root is the synthetic root node's subtree_hash — so + look for a seq=0 node with that subtree_hash.""" + try: + r = sessions._conn.execute( # type: ignore[attr-defined] + "SELECT sid FROM nodes WHERE seq=0 AND subtree_hash=? LIMIT 1", + (h,), + ).fetchone() + except Exception: + return None + return r["sid"] if r else None + + +def _render_session_in_proof(request, sid, focus_bates=None): + """Render the session tree inside the unified /proof/{hash} dispatch. + Re-uses sessions_tree.jinja2 so the visualization is consistent + whether you arrived via /proof/{session_root}, /proof/{node_hash}, + or directly via /sessions/{sid}.""" + from pyramid.renderers import render_to_response + sessions = request.sessions + meta = sessions.get_session(sid) + if meta is None: + return _common_ctx(request) + nodes = sessions.all_nodes_in_session(sid) + children: dict[str, list] = {} + for n in nodes: + if n.parent_bates: + children.setdefault(n.parent_bates, []).append(n) + branches = sessions.branches_in_session(sid) + ctx = _common_ctx(request) + ctx["title"] = "Arborist VIZ — session {0}".format(sid) + ctx["session"] = meta + ctx["nodes"] = nodes + ctx["children"] = children + ctx["branches"] = set(branches) + ctx["focus_bates"] = focus_bates + ctx["sessions_stats"] = _sessions_stats_for_ctx(request) + return render_to_response("sessions_tree.jinja2", ctx, request=request) @view_config(route_name="audit_page", renderer="audit.jinja2") diff --git a/arborist_viz/views/roots.py b/arborist_viz/views/roots.py index 4cfe007..d28ce08 100644 --- a/arborist_viz/views/roots.py +++ b/arborist_viz/views/roots.py @@ -228,7 +228,7 @@ def resolve_hash(request): "bates_list": [n.bates for n in hits], "audit_modes": sorted({n.audit_mode for n in hits}), "question": first.question, - "url": "/cache/{0}".format(h), + "url": "/proof/{0}".format(h), }) # node_hash / subtree_hash search: scan one row. # (No index on these; rare path — direct query.) @@ -254,7 +254,7 @@ def resolve_hash(request): "sid": row["sid"], "audit_mode": row["audit_mode"], "question": row["question"], - "url": "/sessions/{0}".format(row["sid"]), + "url": "/proof/{0}".format(h), }) return _json({"kind": "unknown", "hash": h, "url": "/"})