arborist-viz/arborist_viz/static/viz/proof-page.js
russell@unturf.com 0cd96ab1d7
Initial commit: arborist VIZ — Merkle command center
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).
2026-05-28 09:22:59 -04:00

335 lines
14 KiB
JavaScript

// Proof-page composer. Loads:
// - /api/root/:hash → header info
// - /api/root/:hash/leaves → leaves strip (privacy-aware)
// - /api/lattice/:hash → all tree layers for the 3D widget
// - /api/proof/:hash/:leaf → proof card (on leaf selection)
// - /api/audit/recent?subject_root_filter=... → linked audit events
// Then mounts the six.js Merkle lattice scene.
import { mountLattice } from "./merkle-lattice.js";
import { chunkHex } from "./hash.js";
export async function mountProofPage(rootHash, initialLeafIndex, cacheKey) {
const rootCard = document.getElementById("root-hash");
const proofCard = document.getElementById("proof-card");
const leavesStrip = document.getElementById("leaves-strip");
const latticeStatus = document.getElementById("lattice-status");
const auditLink = document.getElementById("audit-link");
const leafContent = document.getElementById("leaf-content");
const contentModeLabel = document.getElementById("content-mode-label");
const contentToggle = document.getElementById("content-toggle");
const contentBaseVersion = document.getElementById("content-base-version");
const btnPulse = document.getElementById("btn-pulse");
const btnReset = document.getElementById("btn-reset");
// "prose" = arborist.wikitext.to_base() projection (versioned, derived
// from raw content); "raw" = the bytes the leaf hash actually commits to.
let contentMode = "prose";
let currentLeafIdx = null;
// 1) Root metadata.
const root = await getJSON(`/api/root/${rootHash}`);
if (root.error) {
latticeStatus.textContent = `root not found in shard (${root.error})`;
return;
}
rootCard.textContent = `${chunkHex(rootHash)}\n${root.title || ''}\n${root.document_uri || ''}\n${root.leaf_count} leaves · ${root.chunking_version}`;
// Context-root variant: the hash is a Merkle root over sorted source
// document_roots — same canonicalization as a document tree, so the
// standard widgets (lattice, leaves, inclusion proof) still apply.
// Just additionally surface the QA card + sources manifest.
if (root.kind === "context_root") {
renderContextExtras(root, cacheKey).catch((err) => {
console.warn("context extras render failed:", err);
});
}
// 2) Leaves strip + content cache.
const leavesRes = await getJSON(`/api/root/${rootHash}/leaves`);
const leaves = leavesRes.leaves || [];
if (contentModeLabel) {
contentModeLabel.textContent = leavesRes.private_revealed
? "(private: reveal)"
: "(private: deny — content hidden)";
}
renderLeavesStrip(leavesStrip, leaves, (idx) => loadProof(idx));
// 3) Lattice — fetch all layers, build scene.
const lattice = await getJSON(`/api/lattice/${rootHash}`);
latticeStatus.textContent = `${lattice.layer_count} layers · ${lattice.leaf_count} leaves`;
const sceneApi = await mountLattice(document.getElementById("lattice-canvas"), lattice.layers);
btnPulse.disabled = false;
// Provenance glow: cited chunks (used) bright gold, primary candidates
// soft mint. Leaves carry `used` / `source_role` for context roots;
// regular document roots have neither, so markUsage is a no-op there.
sceneApi.markUsage(
leaves.map((L) => ({ used: !!L.used, role: L.source_role }))
);
// Canvas leaf-click → compute that leaf's inclusion proof (matches
// the leaves-strip chip behavior). Any-node click → surface the
// hash + layer + idx in the inspector so interior + root spheres
// aren't visually inert.
sceneApi.onLeafSelect((idx) => loadProof(idx));
sceneApi.onNodeSelect((node) => {
const inspect = document.getElementById("node-inspect");
if (!inspect) return;
const label = node.isLeaf
? `leaf #${node.idx}`
: (node.layer === lattice.layers.length - 1 ? "root" : `interior · layer ${node.layer} · #${node.idx}`);
inspect.classList.remove("muted");
inspect.innerHTML = `<span class="k">${label}</span> <span class="v mono">${chunkHex(node.hash)}</span>`;
});
// 4) Audit linkage — indexed lookup by subject_root (cheap even on a
// 3.5M-event chain).
try {
const j = await getJSON(`/api/audit/by-root/${rootHash}?limit=10`);
const linked = j.events || [];
if (linked.length === 0) {
auditLink.textContent = "no audit events for this root";
} else {
auditLink.innerHTML = "";
for (const ev of linked.slice(0, 8)) {
const a = document.createElement("a");
a.href = `/audit/${ev.event_hash}`;
a.className = "audit-link-row hash-id";
a.title = ev.event_hash;
a.textContent = `${ev.event_type} ${chunkHex(ev.event_hash)}`;
auditLink.appendChild(a);
}
}
} catch (err) {
auditLink.textContent = `audit lookup failed: ${err.message}`;
}
let lastProof = null;
async function loadProof(idx) {
proofCard.innerHTML = `<p class="muted">computing proof for leaf #${idx}…</p>`;
sceneApi.highlightLeaf(idx);
const proof = await getJSON(`/api/proof/${rootHash}/${idx}`);
lastProof = proof;
renderProofCard(proofCard, proof);
sceneApi.setProofPath(proof);
btnPulse.disabled = false;
// Mirror the canvas-click inspector update so chip clicks also
// surface the full leaf hash (no truncation).
const inspect = document.getElementById("node-inspect");
if (inspect) {
const fullHash = proof.leaf_hash || leaves[idx]?.leaf_hash || "";
inspect.classList.remove("muted");
inspect.innerHTML = `<span class="k">leaf #${idx}</span> <span class="v mono">${chunkHex(fullHash)}</span>`;
}
currentLeafIdx = idx;
renderLeafContent();
}
function renderLeafContent() {
const leaf = currentLeafIdx == null ? null : leaves[currentLeafIdx];
if (!leaf) return;
if (leaf.content == null) {
leafContent.classList.add("muted");
leafContent.textContent = leavesRes.private_revealed
? "(no stored content for this leaf — cold tier?)"
: "private-leaf default-deny — set privacy.reveal_private_leaves=true in the .ini to view content.";
if (contentToggle) contentToggle.hidden = true;
return;
}
// Show toggle only when a prose projection exists AND differs from raw.
const hasProse = leaf.prose != null && leaf.prose !== leaf.content;
if (contentToggle) contentToggle.hidden = !hasProse;
if (contentBaseVersion) {
contentBaseVersion.textContent = hasProse && leaf.base_version
? `· ${leaf.base_version} projection`
: "";
}
leafContent.classList.remove("muted");
if (contentMode === "prose" && hasProse) {
leafContent.textContent = leaf.prose;
} else {
leafContent.textContent = leaf.content;
}
if (contentToggle) {
contentToggle.querySelectorAll("button").forEach((b) => {
b.classList.toggle("active", b.dataset.mode === contentMode);
});
}
}
if (contentToggle) {
contentToggle.addEventListener("click", (ev) => {
const btn = ev.target.closest("button[data-mode]");
if (!btn) return;
contentMode = btn.dataset.mode;
renderLeafContent();
});
}
// Context manifest "leaf #N" badges → highlight + prove that leaf in
// the current tree. Delegated so it works regardless of when the async
// renderContextExtras populated the list.
const sourcesListEl = document.getElementById("sources-list");
if (sourcesListEl) {
sourcesListEl.addEventListener("click", (ev) => {
const badge = ev.target.closest(".leaf-jump");
if (!badge) return;
ev.preventDefault();
const idx = parseInt(badge.dataset.leaf, 10);
if (!Number.isNaN(idx)) loadProof(idx);
});
}
btnPulse.addEventListener("click", () => {
if (lastProof) sceneApi.pulseProofPath();
else if (leaves.length) loadProof(0);
});
btnReset.addEventListener("click", () => {
sceneApi.reset();
proofCard.innerHTML = `<p class="muted">select a leaf to compute its inclusion proof…</p>`;
lastProof = null;
});
if (initialLeafIndex != null && leaves.length > initialLeafIndex) {
await loadProof(initialLeafIndex);
sceneApi.pulseProofPath();
} else if (leaves.length > 0) {
// Auto-select the first leaf so the dashboard is visually populated
// the moment the page lands.
await loadProof(0);
}
}
function renderLeavesStrip(el, leaves, onSelect) {
el.innerHTML = "";
if (!leaves.length) {
el.innerHTML = `<span class="muted">no leaves in shard</span>`;
return;
}
for (const leaf of leaves) {
const btn = document.createElement("button");
const usedTag = leaf.used && leaf.used_pointer_ids && leaf.used_pointer_ids.length
? ` (${leaf.used_pointer_ids.join(",")})` : "";
btn.className = "leaf-chip"
+ (leaf.used ? " used" : (leaf.source_role === "primary_answer_source" ? " primary" : ""));
btn.title = leaf.used ? `${leaf.leaf_hash}\nused${usedTag}` : leaf.leaf_hash;
btn.innerHTML = `<span class="leaf-idx">#${leaf.idx}</span><span class="leaf-hash">${leaf.leaf_hash}</span>`
+ (leaf.used ? `<span class="leaf-used">used${usedTag}</span>` : "");
btn.addEventListener("click", () => {
el.querySelectorAll(".leaf-chip.active").forEach((n) => n.classList.remove("active"));
btn.classList.add("active");
onSelect(leaf.idx);
});
el.appendChild(btn);
}
}
function renderProofCard(el, proof) {
if (proof.error) {
el.innerHTML = `<p class="muted">error: ${proof.error}</p>`;
return;
}
const passClass = proof.passed ? "passed" : "failed";
el.innerHTML = `
<div class="proof-pass ${passClass}">${proof.passed ? "✓ PASSED" : "✗ FAILED"}</div>
<div class="proof-row"><span class="k">leaf #</span><span class="v">${proof.leaf_index}</span></div>
<div class="proof-row"><span class="k">leaf hash</span><span class="v mono">${chunkHex(proof.leaf_hash)}</span></div>
<div class="proof-row"><span class="k">siblings</span><span class="v">${proof.sibling_hashes.length}</span></div>
<div class="proof-row"><span class="k">computed root</span><span class="v mono">${chunkHex(proof.computed_root)}</span></div>
<div class="proof-row"><span class="k">expected root</span><span class="v mono">${chunkHex(proof.expected_root)}</span></div>
<details class="proof-details">
<summary>sibling path</summary>
<ol class="sibling-list">
${proof.sibling_hashes.map((h, i) => `<li title="${h}">${proof.left_right_flags[i] ? "←" : "→"} ${chunkHex(h)}</li>`).join("")}
</ol>
</details>
<div class="muted">Inclusion is integrity, not semantic truth.</div>
`;
}
async function getJSON(url) {
const res = await fetch(url);
const j = await res.json().catch(() => ({}));
if (!res.ok && !j.error) j.error = `HTTP ${res.status}`;
return j;
}
async function renderContextExtras(root, cacheKey) {
// Additive overlay for context-root pages — the lattice + leaves +
// inclusion-proof widgets already render the merkle tree built from
// sorted source document_roots. This adds the QA card (question +
// answer) and a richer sources manifest with per-source links.
const ctxPanel = document.getElementById("context-panel");
const qaCard = document.getElementById("qa-card");
const sourcesList = document.getElementById("sources-list");
const effectiveCacheKey = cacheKey || root.cache_key;
if (effectiveCacheKey && qaCard) {
const qa = await getJSON(`/api/cache/${effectiveCacheKey}`);
if (!qa.error) {
qaCard.hidden = false;
qaCard.innerHTML = `
<div class="qa-row"><span class="k">question</span><span class="v">${escapeHTML(qa.question_text || "")}</span></div>
<div class="qa-row"><span class="k">answer</span><pre class="v qa-answer">${escapeHTML(qa.answer_text || "")}</pre></div>
<div class="qa-row"><span class="k">cache_key</span><span class="v mono">${chunkHex(qa.cache_key)}</span></div>
<div class="qa-row"><span class="k">state</span><span class="v">${escapeHTML(qa.falsification_state || "")} · hits ${qa.hit_count ?? 0}</span></div>
`;
}
}
if (sourcesList) {
sourcesList.innerHTML = "";
const sources = root.sources || [];
if (!sources.length) {
sourcesList.innerHTML = `<li class="muted">no sources recorded in this QA's merkle_proof.</li>`;
} else {
// The context tree's leaves are the source document_roots sorted
// lexicographically (matches arborist.qa.query._context_root and
// the read seam's _context_leaf_hashes). Number every source by
// its position in THAT order so the manifest agrees with the leaf
// chips, the lattice, and the inclusion proof's leaf_index — all
// 0-based. (Was an <ol>, which auto-numbered 1-based, over the
// unsorted score-ranked list — two different orderings + two
// different bases.)
const ordered = [...sources].sort((a, b) =>
(a.document_root || "").toLowerCase().localeCompare((b.document_root || "").toLowerCase())
);
ordered.forEach((s, leafIdx) => {
const li = document.createElement("li");
li.className = "source-row" + (s.used ? " used" : "");
const score = typeof s.score === "number" ? s.score.toFixed(1) : "";
const usedPtrs = s.used_pointer_ids && s.used_pointer_ids.length
? ` (${escapeHTML(s.used_pointer_ids.join(","))})` : "";
const usedTag = s.used
? ` · <span class="used-tag">used${usedPtrs}</span>` : "";
li.innerHTML = `
<button type="button" class="leaf-jump" data-leaf="${leafIdx}" title="highlight leaf #${leafIdx} in the tree">leaf #${leafIdx}</button>
<a class="source-link" href="/proof/${encodeURIComponent(s.document_root)}">
<span class="source-title">${escapeHTML(s.title || s.document_uri || s.document_root.slice(0, 16))}</span>
<span class="source-uri muted">${escapeHTML(s.document_uri || "")}</span>
<span class="source-meta muted">
role: ${escapeHTML(s.source_role || "?")} · chunk #${s.chunk_idx ?? "?"} · score ${score} · shard ${escapeHTML(s.shard || "?")}${usedTag}
</span>
<span class="source-hash mono">${chunkHex(s.document_root)}</span>
</a>
`;
sourcesList.appendChild(li);
});
}
}
if (ctxPanel) ctxPanel.hidden = false;
}
function escapeHTML(s) {
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
})[c]);
}