proof dispatch: every hash kind through /proof/{hash}, no kind-named URLs
Two fixes to fox feedback ("rarely like stuff collapsed · why does it
need to be a different screen than the other node types?"):
1. <details open> 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/<key> and /sessions/<sid>).
Templates:
- sessions_tree node-card cache_key + node_hash + subtree_hash are
now clickable → each opens /proof/<hash>.
- <details> on answer + hash panels default-open.
Smoke verified all four dispatch paths:
/proof/<cache_key> → cache_key.jinja2
/proof/<session_root> → sessions_tree.jinja2
/proof/<node_hash> → sessions_tree.jinja2
/proof/<unknown> → proof.jinja2 (corpus fallback)
This commit is contained in:
parent
f78eab6fa4
commit
cb6cab0886
3 changed files with 125 additions and 14 deletions
|
|
@ -21,21 +21,21 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if node.answer_text %}
|
{% if node.answer_text %}
|
||||||
<details class="cmd-node-answer">
|
<details class="cmd-node-answer" open>
|
||||||
<summary>answer ({{ node.answer_text|length }} chars)</summary>
|
<summary>answer ({{ node.answer_text|length }} chars)</summary>
|
||||||
<pre class="cmd-answer-pre">{{ node.answer_text }}</pre>
|
<pre class="cmd-answer-pre">{{ node.answer_text }}</pre>
|
||||||
</details>
|
</details>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<details class="cmd-node-meta">
|
<details class="cmd-node-meta" open>
|
||||||
<summary>hashes / metadata</summary>
|
<summary>hashes / metadata</summary>
|
||||||
<dl class="cmd-meta-dl">
|
<dl class="cmd-meta-dl">
|
||||||
<dt>cache_key</dt>
|
<dt>cache_key</dt>
|
||||||
<dd>{% if node.cache_key %}<a href="/cache/{{ node.cache_key }}"><code class="cmd-hash">{{ node.cache_key }}</code></a>{% else %}<em>(none — canonical_projection)</em>{% endif %}</dd>
|
<dd>{% if node.cache_key %}<a href="/proof/{{ node.cache_key }}"><code class="cmd-hash">{{ node.cache_key }}</code></a>{% else %}<em>(none — canonical_projection)</em>{% endif %}</dd>
|
||||||
<dt>node_hash</dt>
|
<dt>node_hash</dt>
|
||||||
<dd><code class="cmd-hash">{{ node.node_hash }}</code></dd>
|
<dd><a href="/proof/{{ node.node_hash }}"><code class="cmd-hash">{{ node.node_hash }}</code></a></dd>
|
||||||
<dt>subtree_hash</dt>
|
<dt>subtree_hash</dt>
|
||||||
<dd><code class="cmd-hash">{{ node.subtree_hash }}</code></dd>
|
<dd><a href="/proof/{{ node.subtree_hash }}"><code class="cmd-hash">{{ node.subtree_hash }}</code></a></dd>
|
||||||
<dt>created</dt>
|
<dt>created</dt>
|
||||||
<dd>{{ node.created_at }}</dd>
|
<dd>{{ node.created_at }}</dd>
|
||||||
{% if node.parent_bates %}
|
{% if node.parent_bates %}
|
||||||
|
|
|
||||||
|
|
@ -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):
|
def proof_page(request):
|
||||||
"""Single-root visualizer — drops a Merkle hash into a 3D lattice +
|
"""Single-root visualizer. Dispatches by hash kind:
|
||||||
inclusion proof card + leaf list. A ``?cache_key=...`` query param
|
|
||||||
threads a QA record through so synthetic context roots can show the
|
- corpus document_root / leaf / merkle interior → proof.jinja2
|
||||||
question + answer alongside their evidence manifest."""
|
(the 3D Merkle lattice + inclusion proof + leaf-list view)
|
||||||
root_hash = request.matchdict["root_hash"]
|
- 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")
|
leaf_param = request.GET.get("leaf")
|
||||||
cache_key = request.GET.get("cache_key")
|
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]),
|
"title": "Arborist VIZ — root {0}".format(root_hash[:12]),
|
||||||
"root_hash": root_hash,
|
"root_hash": root_hash,
|
||||||
"leaf_index": leaf_param,
|
"leaf_index": leaf_param,
|
||||||
"cache_key": cache_key,
|
"cache_key": cache_key,
|
||||||
**_common_ctx(request),
|
**_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")
|
@view_config(route_name="audit_page", renderer="audit.jinja2")
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,7 @@ def resolve_hash(request):
|
||||||
"bates_list": [n.bates for n in hits],
|
"bates_list": [n.bates for n in hits],
|
||||||
"audit_modes": sorted({n.audit_mode for n in hits}),
|
"audit_modes": sorted({n.audit_mode for n in hits}),
|
||||||
"question": first.question,
|
"question": first.question,
|
||||||
"url": "/cache/{0}".format(h),
|
"url": "/proof/{0}".format(h),
|
||||||
})
|
})
|
||||||
# node_hash / subtree_hash search: scan one row.
|
# node_hash / subtree_hash search: scan one row.
|
||||||
# (No index on these; rare path — direct query.)
|
# (No index on these; rare path — direct query.)
|
||||||
|
|
@ -254,7 +254,7 @@ def resolve_hash(request):
|
||||||
"sid": row["sid"],
|
"sid": row["sid"],
|
||||||
"audit_mode": row["audit_mode"],
|
"audit_mode": row["audit_mode"],
|
||||||
"question": row["question"],
|
"question": row["question"],
|
||||||
"url": "/sessions/{0}".format(row["sid"]),
|
"url": "/proof/{0}".format(h),
|
||||||
})
|
})
|
||||||
|
|
||||||
return _json({"kind": "unknown", "hash": h, "url": "/"})
|
return _json({"kind": "unknown", "hash": h, "url": "/"})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue