From cb6cab08865fb267d52d06043ebffae455f44db0 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 1 Jun 2026 18:08:00 -0400 Subject: [PATCH] proof dispatch: every hash kind through /proof/{hash}, no kind-named URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes to fox feedback ("rarely like stuff collapsed · why does it need to be a different screen than the other node types?"): 1.
on answer + metadata panels in sessions_tree.jinja2. Expanded by default — fox rarely wants stuff collapsed; clicks to collapse cost nothing and discoverability of the field is what matters on first read. 2. /proof/{hash} is now THE dispatcher — drop ANY hash here and the page resolves the right view: - corpus document_root / leaf / merkle interior → proof.jinja2 (existing 3D Merkle lattice + inclusion proof + leaf-list) - providence cache_key with session references → cache_key.jinja2 (every session node that hit this cache entry) - session node_hash / subtree_hash → sessions_tree.jinja2 (the session's conversation tree) - session_root (= seq=0 node's subtree_hash) → sessions_tree.jinja2 Aligns with ticket #000069 §13 discipline: "never mint new kind-named URLs." /sessions/{sid} and /cache/{cache_key} stay as working aliases — but the resolver, the metadata-panel links, and the hash-drop bar all route through /proof/{hash} now. Sessions are still tree-shaped (parent/child) where corpus roots are flat leaf lists, so the IN-PAGE view differs. That's a topology fact, not a URL choice — sessions and documents live in the same hash universe under one route. A future six.js tree widget could share the proof.jinja2 3D stage; not in this commit. Resolver: - /api/resolve/{hash} → "url": "/proof/{hash}" for cache_keys and session node hashes (was /cache/ and /sessions/). Templates: - sessions_tree node-card cache_key + node_hash + subtree_hash are now clickable → each opens /proof/. -
on answer + hash panels default-open. Smoke verified all four dispatch paths: /proof/ → cache_key.jinja2 /proof/ → sessions_tree.jinja2 /proof/ → sessions_tree.jinja2 /proof/ → proof.jinja2 (corpus fallback) --- arborist_viz/templates/sessions_tree.jinja2 | 10 +- arborist_viz/views/home.py | 125 ++++++++++++++++++-- arborist_viz/views/roots.py | 4 +- 3 files changed, 125 insertions(+), 14 deletions(-) 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": "/"})