sessions: richer tree cards, answer inline, /cache/<key> permalink
The bare-tree layout fox flagged ("kinda sucks") showed one line per
node and a huge empty page. Each node is now a card with:
- bates + audit chip + branch/current pills in the header
- the question, full width
- cited-title pills row
- <details> "answer (N chars)" expandable to the full claim-lattice
pointer-line answer (monospace, scrollable, indented with a
cyan accent stripe)
- <details> "hashes / metadata" with cache_key (clickable to
/cache/<key>), node_hash, subtree_hash, created_at, parent_bates
cmd-panel-wide on the tree page so cards fill the screen instead of
leaving 70% empty.
New /cache/{cache_key} page lists every session node referencing a
providence cache_key — surfaces the "this answer reused across N
turns/sessions" structure. /api/cache-key/{cache_key} JSON twin.
/api/resolve/{hash} now dispatches a cache_key to /cache/<key> (was
/sessions/find?q=<hash> which FTS5 couldn't tokenize). Also
recognises a session node_hash or subtree_hash → /sessions/{sid}.
Routes: /cache/{cache_key}, /api/cache-key/{cache_key} (distinct
from the pre-existing /api/cache/{cache_key} which targets the
providence_cache row itself; the ticket-§13 surface stays unchanged).
CSS: cmd-node-card, cmd-answer-pre, cmd-meta-dl, cmd-current-pill,
cmd-branch-pill, cmd-panel-wide; dark-themed with status-coded
borders (cyan for current, amber for branch points).
This commit is contained in:
parent
60e8640d86
commit
f78eab6fa4
6 changed files with 269 additions and 31 deletions
|
|
@ -59,9 +59,14 @@ def includeme(config):
|
||||||
config.add_route("sessions_list", "/sessions")
|
config.add_route("sessions_list", "/sessions")
|
||||||
config.add_route("sessions_find", "/sessions/find")
|
config.add_route("sessions_find", "/sessions/find")
|
||||||
config.add_route("sessions_tree", "/sessions/{sid}")
|
config.add_route("sessions_tree", "/sessions/{sid}")
|
||||||
|
config.add_route("cache_key_page", "/cache/{cache_key}")
|
||||||
config.add_route("api_sessions_list", "/api/sessions")
|
config.add_route("api_sessions_list", "/api/sessions")
|
||||||
config.add_route("api_sessions_find", "/api/sessions/find")
|
config.add_route("api_sessions_find", "/api/sessions/find")
|
||||||
config.add_route("api_sessions_tree", "/api/sessions/{sid}")
|
config.add_route("api_sessions_tree", "/api/sessions/{sid}")
|
||||||
|
# Sessions-shard view of a providence cache_key (every session node
|
||||||
|
# referencing it). Distinct from /api/cache/ which targets the
|
||||||
|
# providence_cache row itself (ticket #000069 §13 §6.7).
|
||||||
|
config.add_route("api_session_cache_key", "/api/cache-key/{cache_key}")
|
||||||
|
|
||||||
# SSE — §3-corrected replaces WS routes with text/event-stream.
|
# SSE — §3-corrected replaces WS routes with text/event-stream.
|
||||||
config.add_route("sse_audit_live", "/sse/audit/live")
|
config.add_route("sse_audit_live", "/sse/audit/live")
|
||||||
|
|
|
||||||
|
|
@ -673,3 +673,70 @@ code, pre { font-family: var(--mono); font-size: 12px; }
|
||||||
.cmd-cite { font-size: 0.75em; opacity: 0.7;
|
.cmd-cite { font-size: 0.75em; opacity: 0.7;
|
||||||
padding: 0.05em 0.4em; border-radius: 0.2em;
|
padding: 0.05em 0.4em; border-radius: 0.2em;
|
||||||
background: rgba(255,255,255,0.06); }
|
background: rgba(255,255,255,0.06); }
|
||||||
|
|
||||||
|
/* ---- node cards + answer + metadata (richer tree views) -------------- */
|
||||||
|
|
||||||
|
.cmd-panel-wide { max-width: none; padding: 1.5em 2em; }
|
||||||
|
|
||||||
|
.cmd-hash-block { word-break: break-all; font-size: 0.85em; opacity: 0.85; }
|
||||||
|
|
||||||
|
.cmd-node-card {
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
border-radius: 0.3em;
|
||||||
|
padding: 0.8em 1em;
|
||||||
|
margin: 0.4em 0;
|
||||||
|
}
|
||||||
|
.cmd-node-card-head {
|
||||||
|
display: flex; gap: 0.6em; align-items: baseline; flex-wrap: wrap;
|
||||||
|
margin-bottom: 0.4em;
|
||||||
|
}
|
||||||
|
.cmd-node-q {
|
||||||
|
font-size: 1.05em; font-weight: 500;
|
||||||
|
margin: 0.3em 0 0.5em 0;
|
||||||
|
}
|
||||||
|
.cmd-current-pill, .cmd-branch-pill {
|
||||||
|
font-size: 0.7em; padding: 0.05em 0.5em;
|
||||||
|
border-radius: 0.2em; font-weight: 600;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.cmd-current-pill { background: rgba(120, 200, 255, 0.25); color: #8af; }
|
||||||
|
.cmd-branch-pill { background: rgba(220, 180, 80, 0.25); color: #db8; }
|
||||||
|
|
||||||
|
.cmd-node-answer summary,
|
||||||
|
.cmd-node-meta summary {
|
||||||
|
cursor: pointer; opacity: 0.7; font-size: 0.85em;
|
||||||
|
padding: 0.3em 0; user-select: none;
|
||||||
|
}
|
||||||
|
.cmd-node-answer[open] summary,
|
||||||
|
.cmd-node-meta[open] summary { opacity: 1; }
|
||||||
|
.cmd-answer-pre {
|
||||||
|
background: rgba(0,0,0,0.25);
|
||||||
|
padding: 0.8em 1em;
|
||||||
|
border-left: 2px solid rgba(120, 200, 255, 0.4);
|
||||||
|
border-radius: 0.2em;
|
||||||
|
font-size: 0.85em;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-x: auto;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmd-meta-dl {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: max-content 1fr;
|
||||||
|
gap: 0.2em 1em;
|
||||||
|
font-size: 0.85em;
|
||||||
|
padding: 0.5em 0;
|
||||||
|
}
|
||||||
|
.cmd-meta-dl dt { opacity: 0.65; }
|
||||||
|
.cmd-meta-dl dd { margin: 0; word-break: break-all; }
|
||||||
|
.cmd-meta-dl code { font-size: 0.95em; }
|
||||||
|
|
||||||
|
.cmd-tree-current > .cmd-node-card { border-color: rgba(120, 200, 255, 0.5); }
|
||||||
|
.cmd-tree-branch > .cmd-node-card { border-color: rgba(220, 180, 80, 0.5); }
|
||||||
|
|
||||||
|
/* Override the tree-line styles (old layout) — cards replace them. */
|
||||||
|
.cmd-tree-current > .cmd-tree-line::after { content: none; }
|
||||||
|
|
|
||||||
52
arborist_viz/templates/cache_key.jinja2
Normal file
52
arborist_viz/templates/cache_key.jinja2
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
{% extends "base.jinja2" %}
|
||||||
|
{% block body %}
|
||||||
|
<section class="cmd-panel">
|
||||||
|
<header class="cmd-panel-head">
|
||||||
|
<h1>cache_key</h1>
|
||||||
|
<p class="cmd-hash-block"><code class="cmd-hash">{{ cache_key }}</code></p>
|
||||||
|
<p class="cmd-panel-sub">
|
||||||
|
{{ hits|length }} session node(s) reference this providence cache entry —
|
||||||
|
a cache hit reused across {{ hits|map(attribute='sid')|unique|list|length }} session(s).
|
||||||
|
</p>
|
||||||
|
<p><a href="/sessions">← all sessions</a> · <a href="/api/cache-key/{{ cache_key }}">JSON</a></p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{% if hits %}
|
||||||
|
{% set first = hits[0] %}
|
||||||
|
<article class="cmd-node-card">
|
||||||
|
<header class="cmd-node-card-head">
|
||||||
|
<span class="cmd-tree-audit cmd-audit-{{ first.audit_mode|lower }}">{{ first.audit_mode }}</span>
|
||||||
|
<span class="cmd-node-q">{{ first.question }}</span>
|
||||||
|
</header>
|
||||||
|
{% if first.cited_titles %}
|
||||||
|
<div class="cmd-tree-cites">
|
||||||
|
{% for t in first.cited_titles %}<span class="cmd-cite">{{ t }}</span>{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<details class="cmd-node-answer" open>
|
||||||
|
<summary>answer</summary>
|
||||||
|
<pre class="cmd-answer-pre">{{ first.answer_text }}</pre>
|
||||||
|
</details>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<h2>Referenced by</h2>
|
||||||
|
<table class="cmd-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>bates</th><th>session</th><th>audit</th><th>created</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for n in hits %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/sessions/{{ n.sid }}"><code>{{ n.bates }}</code></a></td>
|
||||||
|
<td><code>{{ n.sid }}</code></td>
|
||||||
|
<td><span class="cmd-tree-audit cmd-audit-{{ n.audit_mode|lower }}">{{ n.audit_mode }}</span></td>
|
||||||
|
<td>{{ n.created_at }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="cmd-empty">no session nodes reference this cache_key</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -2,18 +2,50 @@
|
||||||
|
|
||||||
{% macro render_node(node, kids_of, branch_set, current_bates, this_sid, depth=0) -%}
|
{% macro render_node(node, kids_of, branch_set, current_bates, this_sid, depth=0) -%}
|
||||||
<li class="cmd-tree-node {% if node.bates == current_bates %}cmd-tree-current{% endif %} {% if node.bates in branch_set %}cmd-tree-branch{% endif %}">
|
<li class="cmd-tree-node {% if node.bates == current_bates %}cmd-tree-current{% endif %} {% if node.bates in branch_set %}cmd-tree-branch{% endif %}">
|
||||||
<div class="cmd-tree-line">
|
<article class="cmd-node-card">
|
||||||
<span class="cmd-tree-bates"><code>{{ node.bates }}</code></span>
|
<header class="cmd-node-card-head">
|
||||||
<span class="cmd-tree-audit cmd-audit-{{ node.audit_mode|lower }}">{{ node.audit_mode }}</span>
|
<code class="cmd-tree-bates">{{ node.bates }}</code>
|
||||||
{% if node.sid != this_sid %}<span class="cmd-tree-from">[from {{ node.sid }}]</span>{% endif %}
|
<span class="cmd-tree-audit cmd-audit-{{ node.audit_mode|lower }}">{{ node.audit_mode }}</span>
|
||||||
{% if node.label %}<span class="cmd-tree-label">{{ '{' + node.label + '}' }}</span>{% endif %}
|
{% if node.sid != this_sid %}<span class="cmd-tree-from">[from {{ node.sid }}]</span>{% endif %}
|
||||||
<span class="cmd-tree-q">{{ (node.question or '')[:120] }}</span>
|
{% if node.label %}<span class="cmd-tree-label">{{ '{' + node.label + '}' }}</span>{% endif %}
|
||||||
</div>
|
{% if node.bates == current_bates %}<span class="cmd-current-pill">current</span>{% endif %}
|
||||||
{% if node.cited_titles %}
|
{% if node.bates in branch_set %}<span class="cmd-branch-pill">branch</span>{% endif %}
|
||||||
<div class="cmd-tree-cites">
|
</header>
|
||||||
{% for t in node.cited_titles %}<span class="cmd-cite">{{ t }}</span>{% endfor %}
|
|
||||||
</div>
|
<div class="cmd-node-q">{{ node.question }}</div>
|
||||||
{% endif %}
|
|
||||||
|
{% if node.cited_titles %}
|
||||||
|
<div class="cmd-tree-cites">
|
||||||
|
{% for t in node.cited_titles %}<span class="cmd-cite">{{ t }}</span>{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if node.answer_text %}
|
||||||
|
<details class="cmd-node-answer">
|
||||||
|
<summary>answer ({{ node.answer_text|length }} chars)</summary>
|
||||||
|
<pre class="cmd-answer-pre">{{ node.answer_text }}</pre>
|
||||||
|
</details>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<details class="cmd-node-meta">
|
||||||
|
<summary>hashes / metadata</summary>
|
||||||
|
<dl class="cmd-meta-dl">
|
||||||
|
<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>
|
||||||
|
<dt>node_hash</dt>
|
||||||
|
<dd><code class="cmd-hash">{{ node.node_hash }}</code></dd>
|
||||||
|
<dt>subtree_hash</dt>
|
||||||
|
<dd><code class="cmd-hash">{{ node.subtree_hash }}</code></dd>
|
||||||
|
<dt>created</dt>
|
||||||
|
<dd>{{ node.created_at }}</dd>
|
||||||
|
{% if node.parent_bates %}
|
||||||
|
<dt>parent</dt>
|
||||||
|
<dd><code>{{ node.parent_bates }}</code></dd>
|
||||||
|
{% endif %}
|
||||||
|
</dl>
|
||||||
|
</details>
|
||||||
|
</article>
|
||||||
|
|
||||||
{% set kids = kids_of.get(node.bates, []) %}
|
{% set kids = kids_of.get(node.bates, []) %}
|
||||||
{% if kids %}
|
{% if kids %}
|
||||||
<ul class="cmd-tree-children">
|
<ul class="cmd-tree-children">
|
||||||
|
|
@ -26,27 +58,27 @@
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<section class="cmd-panel">
|
<section class="cmd-panel cmd-panel-wide">
|
||||||
<header class="cmd-panel-head">
|
<header class="cmd-panel-head">
|
||||||
<h1>session <code>{{ session.sid }}</code></h1>
|
<h1>session <code>{{ session.sid }}</code>{% if session.label %} <span class="cmd-tree-label">{{ '{' + session.label + '}' }}</span>{% endif %}</h1>
|
||||||
<p class="cmd-panel-sub">
|
<p class="cmd-panel-sub">
|
||||||
{{ session.n_nodes }} node(s) · {{ branches|length }} branch point(s) ·
|
{{ session.n_nodes }} node(s) · {{ branches|length }} branch point(s) ·
|
||||||
<code class="cmd-hash">root={{ session.session_root }}</code> ·
|
current → <a href="#node-{{ session.current_bates }}"><code>{{ session.current_bates }}</code></a>
|
||||||
current → <code>{{ session.current_bates }}</code>
|
|
||||||
</p>
|
</p>
|
||||||
|
<p class="cmd-hash-block">root <code class="cmd-hash">{{ session.session_root }}</code></p>
|
||||||
<p><a href="/sessions">← all sessions</a> · <a href="/api/sessions/{{ session.sid }}">JSON</a></p>
|
<p><a href="/sessions">← all sessions</a> · <a href="/api/sessions/{{ session.sid }}">JSON</a></p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{% set kids_of = children %}
|
{% set kids_of = children %}
|
||||||
{% set root_node = nodes[0] if nodes and nodes[0].seq == 0 else None %}
|
{% set root_node = nodes[0] if nodes and nodes[0].seq == 0 else None %}
|
||||||
{% if root_node %}
|
{% if root_node and kids_of.get(root_node.bates) %}
|
||||||
<ul class="cmd-tree">
|
<ul class="cmd-tree">
|
||||||
{% for child in kids_of.get(root_node.bates, []) %}
|
{% for child in kids_of.get(root_node.bates, []) %}
|
||||||
{{ render_node(child, kids_of, branches, session.current_bates, session.sid) }}
|
{{ render_node(child, kids_of, branches, session.current_bates, session.sid) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="cmd-empty">empty session</p>
|
<p class="cmd-empty">empty session — no turns yet. <code>make session SID={{ session.sid }}</code> to continue.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -192,22 +192,72 @@ def get_lattice(request):
|
||||||
|
|
||||||
@view_config(route_name="api_resolve", request_method="GET")
|
@view_config(route_name="api_resolve", request_method="GET")
|
||||||
def resolve_hash(request):
|
def resolve_hash(request):
|
||||||
"""Drop-a-hash dispatcher. Returns whichever arborist object owns the hash."""
|
"""Drop-a-hash dispatcher. Returns whichever arborist object owns the hash.
|
||||||
if request.shards is None:
|
|
||||||
return _no_shard()
|
Resolution order: corpus shards first (document_root / leaf_hash /
|
||||||
|
audit_event / merkle_interior / qa cache_key / context_root /
|
||||||
|
run_dag_root), then sessions shard (cache_key referenced by N
|
||||||
|
session nodes; node_hash / subtree_hash on a session node).
|
||||||
|
"""
|
||||||
h = request.matchdict["hex_hash"].strip().lower()
|
h = request.matchdict["hex_hash"].strip().lower()
|
||||||
if not h or len(h) > 128 or not all(c in "0123456789abcdef" for c in h):
|
if not h or len(h) > 128 or not all(c in "0123456789abcdef" for c in h):
|
||||||
return _json({"error": "not_hex", "hash": h}, status=400)
|
return _json({"error": "not_hex", "hash": h}, status=400)
|
||||||
res = request.shards.resolve(h)
|
|
||||||
# The browser still needs a /url to navigate to — synthesize from kind.
|
# First: corpus shards (when configured).
|
||||||
url = _nav_url(res)
|
if request.shards is not None:
|
||||||
return _json({
|
res = request.shards.resolve(h)
|
||||||
"kind": res.kind,
|
if res.kind != "unknown":
|
||||||
"hash": res.hash,
|
return _json({
|
||||||
"shard": res.shard_path,
|
"kind": res.kind, "hash": res.hash,
|
||||||
"url": url,
|
"shard": res.shard_path, "url": _nav_url(res),
|
||||||
**res.extra,
|
**res.extra,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Second: sessions shard. cache_key OR node_hash OR subtree_hash.
|
||||||
|
sessions = request.sessions
|
||||||
|
if sessions is not None and sessions.is_open:
|
||||||
|
hits = sessions.find_by_cache_key(h)
|
||||||
|
if hits:
|
||||||
|
first = hits[0]
|
||||||
|
return _json({
|
||||||
|
"kind": "session_cache_key",
|
||||||
|
"hash": h,
|
||||||
|
"shard": sessions.path,
|
||||||
|
"n_session_nodes": len(hits),
|
||||||
|
"sids": sorted({n.sid for n in hits}),
|
||||||
|
"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),
|
||||||
|
})
|
||||||
|
# node_hash / subtree_hash search: scan one row.
|
||||||
|
# (No index on these; rare path — direct query.)
|
||||||
|
try:
|
||||||
|
row = sessions._conn.execute( # type: ignore[attr-defined]
|
||||||
|
"SELECT bates, sid, audit_mode, question, node_hash, "
|
||||||
|
" subtree_hash FROM nodes "
|
||||||
|
"WHERE node_hash=? OR subtree_hash=? LIMIT 1",
|
||||||
|
(h, h),
|
||||||
|
).fetchone()
|
||||||
|
except Exception:
|
||||||
|
row = None
|
||||||
|
if row:
|
||||||
|
kind = (
|
||||||
|
"session_node_hash" if row["node_hash"] == h
|
||||||
|
else "session_subtree_hash"
|
||||||
|
)
|
||||||
|
return _json({
|
||||||
|
"kind": kind,
|
||||||
|
"hash": h,
|
||||||
|
"shard": sessions.path,
|
||||||
|
"bates": row["bates"],
|
||||||
|
"sid": row["sid"],
|
||||||
|
"audit_mode": row["audit_mode"],
|
||||||
|
"question": row["question"],
|
||||||
|
"url": "/sessions/{0}".format(row["sid"]),
|
||||||
|
})
|
||||||
|
|
||||||
|
return _json({"kind": "unknown", "hash": h, "url": "/"})
|
||||||
|
|
||||||
|
|
||||||
def _nav_url(res) -> str:
|
def _nav_url(res) -> str:
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,38 @@ def sessions_tree(request):
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
@view_config(route_name="cache_key_page", renderer="cache_key.jinja2")
|
||||||
|
def cache_key_page(request):
|
||||||
|
"""Show every session node referencing a given providence cache_key.
|
||||||
|
A cache hit reused across sessions/turns lights up here."""
|
||||||
|
sessions = request.sessions
|
||||||
|
cache_key = request.matchdict["cache_key"].strip().lower()
|
||||||
|
hits = (
|
||||||
|
sessions.find_by_cache_key(cache_key)
|
||||||
|
if (sessions and sessions.is_open) else []
|
||||||
|
)
|
||||||
|
ctx = _sessions_ctx(request)
|
||||||
|
ctx["title"] = f"cache_key {cache_key[:12]}… — Arborist VIZ"
|
||||||
|
ctx["cache_key"] = cache_key
|
||||||
|
ctx["hits"] = hits
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
@view_config(route_name="api_session_cache_key", renderer="json")
|
||||||
|
def api_cache_key(request):
|
||||||
|
sessions = request.sessions
|
||||||
|
cache_key = request.matchdict["cache_key"].strip().lower()
|
||||||
|
hits = (
|
||||||
|
sessions.find_by_cache_key(cache_key)
|
||||||
|
if (sessions and sessions.is_open) else []
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"cache_key": cache_key,
|
||||||
|
"n_session_nodes": len(hits),
|
||||||
|
"nodes": [_node_to_json(n) for n in hits],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@view_config(route_name="sessions_find", renderer="sessions_find.jinja2")
|
@view_config(route_name="sessions_find", renderer="sessions_find.jinja2")
|
||||||
def sessions_find(request):
|
def sessions_find(request):
|
||||||
sessions = request.sessions
|
sessions = request.sessions
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue