checkpoint: matrix-agent-validated (2026-04-25)
Architectural snapshot of the lakehouse codebase at the point where the
full matrix-driven agent loop with Mem0 versioning + deletion was
validated end-to-end.
WHAT THIS REPO IS
A clean single-commit snapshot of the lakehouse code. Heavy test data
(.parquet datasets, vector indexes) excluded — see REPLICATION.md for
regen path. Full lakehouse history at git.agentview.dev/profit/lakehouse.
WHAT WAS PROVEN
- Vector retrieval across multi-corpora matrix (chicago_permits + entity
briefs + sec_tickers + distilled procedural + llm_team runs)
- Observer hand-review (cloud + heuristic fallback) gating each candidate
- Local-model agent loop (qwen3.5:latest) with tool use + scratchpad
- Playbook seal on success → next-iter retrieval surfaces it as preamble
- Mem0 versioning + deletion in pathway_memory:
* UPSERT: ADD on new workflow, UPDATE bumps replay_count on identical
* REVISE: chains versions, parent.superseded_at + superseded_by stamped
* RETIRE: marks specific trace retired with reason, excluded from retrieval
* HISTORY: walks chain root→tip, cycle-safe
KEY DIRECTORIES
- crates/vectord/src/pathway_memory.rs — Mem0 ops live here
- crates/vectord/src/playbook_memory.rs — original Mem0 reference
- tests/agent_test/ — local-model agent harness + PRD + session archives
- scripts/dump_raw_corpus.sh — MinIO bucket dump (raw test corpus)
- scripts/vectorize_raw_corpus.ts — corpus → vector indexes
- scripts/analyze_chicago_contracts.ts — real inference pipeline
- scripts/seal_agent_playbook.ts — Mem0 upsert from agent traces
Replication: see REPLICATION.md for Debian 13 clean install + cloud-only
adaptation (no local Ollama).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+134
@@ -0,0 +1,134 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Lakehouse · Visual Control Plane</title>
|
||||
<link rel="stylesheet" href="/ui.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- TOP BAR — service heartbeats + view switcher + run selector -->
|
||||
<header id="topbar">
|
||||
<div class="brand">
|
||||
<span class="sig">◆</span>
|
||||
<span>LAKEHOUSE · VCP</span>
|
||||
<span class="build" id="build-ts"></span>
|
||||
</div>
|
||||
<nav id="views">
|
||||
<button data-view="map" class="on">MAP</button>
|
||||
<button data-view="trace">TRACE</button>
|
||||
<button data-view="trajectory">TRAJECTORY</button>
|
||||
<button data-view="metrics">METRICS</button>
|
||||
<button data-view="kb">KB</button>
|
||||
<button data-view="console">CONSOLE</button>
|
||||
</nav>
|
||||
<div id="hb">
|
||||
<span class="hbchip" data-svc="gateway">GW</span>
|
||||
<span class="hbchip" data-svc="sidecar">SC</span>
|
||||
<span class="hbchip" data-svc="observer">OBS</span>
|
||||
<span class="hbchip" data-svc="mcp">MCP</span>
|
||||
<span class="hbchip" data-svc="context7">CTX7</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- MAIN STAGE — view-switched but context panel persists -->
|
||||
<section id="stage">
|
||||
<!-- System Map view -->
|
||||
<div id="view-map" class="view on">
|
||||
<div id="overlay-controls">
|
||||
<span class="lbl">OVERLAY:</span>
|
||||
<button data-ov="status" class="on">status</button>
|
||||
<button data-ov="confidence">confidence</button>
|
||||
<button data-ov="gradient">gradient</button>
|
||||
<button data-ov="verdict">verdict</button>
|
||||
<button data-ov="activity">activity</button>
|
||||
</div>
|
||||
<svg id="map"></svg>
|
||||
<div id="legend">
|
||||
<span class="lg healthy">● healthy</span>
|
||||
<span class="lg degraded">● degraded</span>
|
||||
<span class="lg down">● down</span>
|
||||
<span class="lg active">◆ active flow</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trace view -->
|
||||
<div id="view-trace" class="view">
|
||||
<div class="subhead">
|
||||
<span>TASK TRACE — file:</span>
|
||||
<span id="trace-file">—</span>
|
||||
<span class="spacer"></span>
|
||||
<span id="trace-runs"></span>
|
||||
</div>
|
||||
<div id="trace-timeline"></div>
|
||||
<div id="trace-detail"></div>
|
||||
</div>
|
||||
|
||||
<!-- Trajectory view — scrum trajectory, refactor signals, reverse index -->
|
||||
<div id="view-trajectory" class="view">
|
||||
<div class="traj-header">
|
||||
<input id="traj-search" type="text" placeholder="Reverse index: search every scrum review for a phrase ('auth', 'middleware', 'dead code', 'ADR-012', …)" />
|
||||
<div id="traj-stats"></div>
|
||||
</div>
|
||||
<div id="traj-body"></div>
|
||||
</div>
|
||||
|
||||
<!-- Metrics view -->
|
||||
<div id="view-metrics" class="view">
|
||||
<div class="metric-grid" id="metric-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- KB view -->
|
||||
<div id="view-kb" class="view">
|
||||
<div class="kb-grid" id="kb-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- Console view — per-service live logs -->
|
||||
<div id="view-console" class="view">
|
||||
<div class="console-toolbar">
|
||||
<span class="con-eyebrow">SERVICE LOGS</span>
|
||||
<span id="con-tabs">
|
||||
<button data-svc="gateway" class="on">gateway</button>
|
||||
<button data-svc="sidecar">sidecar</button>
|
||||
<button data-svc="observer">observer</button>
|
||||
<button data-svc="mcp">mcp</button>
|
||||
<button data-svc="context7">ctx7</button>
|
||||
<button data-svc="auditor">auditor</button>
|
||||
<button data-svc="langfuse">langfuse</button>
|
||||
<button data-svc="summary">▣ all</button>
|
||||
</span>
|
||||
<span class="spacer"></span>
|
||||
<span id="con-unit"></span>
|
||||
</div>
|
||||
<div id="console-log"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CONTINUOUS CONTEXT PANEL — right rail, always visible -->
|
||||
<aside id="context">
|
||||
<div class="ctx-header">
|
||||
<span class="ctx-eyebrow">CONTEXT</span>
|
||||
<span id="ctx-target">no selection</span>
|
||||
</div>
|
||||
<div id="ctx-body">
|
||||
<div class="ctx-hint">Click a node or a file in KB to inspect. Context persists across view switches.</div>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<!-- BOTTOM STRIP — btop-style real-time stream -->
|
||||
<section id="stream">
|
||||
<div class="stream-head">
|
||||
<span class="stream-eyebrow">STREAM</span>
|
||||
<span class="spacer"></span>
|
||||
<span id="stream-file">—</span>
|
||||
<span class="dot" id="stream-tick"></span>
|
||||
</div>
|
||||
<div id="stream-body"></div>
|
||||
</section>
|
||||
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script src="/ui.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+455
@@ -0,0 +1,455 @@
|
||||
// Visual Control Plane server — v1
|
||||
// Single Bun.serve process on :3950. Serves static index.html and
|
||||
// /data/* endpoints that fan out to the live services + tail jsonl KB
|
||||
// files. No build step, no node_modules. Restart via systemd or
|
||||
// `bun run ui/server.ts`.
|
||||
|
||||
const PORT = Number(process.env.LH_UI_PORT ?? 3950);
|
||||
const KB = "/home/profit/lakehouse/data/_kb";
|
||||
const REPO = "/home/profit/lakehouse";
|
||||
|
||||
const GATEWAY = "http://localhost:3100";
|
||||
const SIDECAR = "http://localhost:3200";
|
||||
const OBSERVER = "http://localhost:3800";
|
||||
const MCP = "http://localhost:3700";
|
||||
const CONTEXT7 = "http://localhost:3900";
|
||||
|
||||
// Tail helper — read last N lines of a jsonl file without loading
|
||||
// the whole thing. For files up to a few MB this is fine to read fully.
|
||||
async function tailJsonl(path: string, n = 50): Promise<any[]> {
|
||||
try {
|
||||
const text = await Bun.file(path).text();
|
||||
const lines = text.trim().split("\n").filter(Boolean);
|
||||
const tail = lines.slice(-n);
|
||||
return tail.map(l => {
|
||||
try { return JSON.parse(l); } catch { return { _raw: l, _error: "parse" }; }
|
||||
});
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function tryFetch(url: string, timeout = 1500): Promise<any | null> {
|
||||
try {
|
||||
const r = await fetch(url, { signal: AbortSignal.timeout(timeout) });
|
||||
if (!r.ok) return null;
|
||||
// Fix 2026-04-24: some upstream services (observer Bun.serve) return
|
||||
// JSON without an application/json content-type. Don't rely on header
|
||||
// — try parsing the body as JSON; fall back to raw text on failure.
|
||||
const body = await r.text();
|
||||
try { return JSON.parse(body); } catch { return body; }
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Compact the massive /vectors/indexes response into just the shape the
|
||||
// UI needs: [{name, source, model, dims, chunks, bucket, backend}]
|
||||
async function indexesSummary(): Promise<any> {
|
||||
const j = await tryFetch(`${GATEWAY}/vectors/indexes`);
|
||||
if (!Array.isArray(j)) return { count: 0, items: [] };
|
||||
const items = j.slice(0, 12).map((i: any) => ({
|
||||
name: i.index_name,
|
||||
source: i.source,
|
||||
dims: i.dimensions,
|
||||
chunks: i.chunk_count,
|
||||
backend: i.vector_backend,
|
||||
bucket: i.bucket,
|
||||
}));
|
||||
return { count: j.length, items };
|
||||
}
|
||||
|
||||
async function servicesSnapshot() {
|
||||
const [gw, sc, obs, mcp, c7, jstats, ustats] = await Promise.all([
|
||||
tryFetch(`${GATEWAY}/health`),
|
||||
tryFetch(`${SIDECAR}/health`),
|
||||
tryFetch(`${OBSERVER}/health`),
|
||||
tryFetch(`${MCP}/health`),
|
||||
tryFetch(`${CONTEXT7}/health`),
|
||||
tryFetch(`${GATEWAY}/journal/stats`),
|
||||
tryFetch(`${GATEWAY}/v1/usage`),
|
||||
]);
|
||||
return {
|
||||
ts: new Date().toISOString(),
|
||||
nodes: [
|
||||
{ id: "gateway", label: "Gateway :3100", status: gw ? "healthy" : "down", health: gw },
|
||||
{ id: "sidecar", label: "Sidecar :3200", status: sc ? "healthy" : "down", health: sc },
|
||||
{ id: "observer", label: "Observer :3800", status: obs ? "healthy" : "down", health: obs,
|
||||
stats: await tryFetch(`${OBSERVER}/stats`) },
|
||||
{ id: "mcp", label: "MCP :3700", status: mcp ? "healthy" : "down", health: mcp },
|
||||
{ id: "context7", label: "Context7 :3900", status: c7 ? "healthy" : "down", health: c7 },
|
||||
],
|
||||
// Virtual nodes — backed by gateway subsystems rather than own ports
|
||||
subsystems: [
|
||||
{ id: "journal", label: "Journal", stats: jstats },
|
||||
{ id: "usage", label: "Usage /v1", stats: ustats },
|
||||
{ id: "vectord", label: "Vectord", stats: await indexesSummary() },
|
||||
{ id: "playbook", label: "Playbook", stats: await tryFetch(`${GATEWAY}/vectors/playbook_memory/status`) },
|
||||
{ id: "agent", label: "Autotune", stats: await tryFetch(`${GATEWAY}/vectors/agent/status`) },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Extract phrase-level markers that indicate "this should be removed,
|
||||
// simplified, or refactored" across scrum suggestions. These are the
|
||||
// signals that accumulate into a refactor recommendation.
|
||||
const REFACTOR_PHRASES = [
|
||||
"should be removed", "remove this", "dead code", "unused", "unnecessary",
|
||||
"duplicate of", "duplicates", "redundant",
|
||||
"consolidate", "merge with", "extract into",
|
||||
"refactor", "rewrite", "replace with",
|
||||
"orphaned", "stale", "deprecated",
|
||||
"pseudocode", "placeholder", "stub",
|
||||
"split this file", "too large",
|
||||
];
|
||||
|
||||
// Signal-class classifier — per file, given 2+ consecutive iterations'
|
||||
// reviews, tag the file's behavior:
|
||||
// CONVERGING — resolved > novel, score ↑
|
||||
// LOOPING — 3+ same findings repeat, novel = 0, score flat
|
||||
// ORBITING — novel findings each iter, no resolved (healthy depth)
|
||||
// PLATEAU — score flat + findings flat (diminishing returns)
|
||||
// MIXED — partial/unclear
|
||||
// This is the foundation for iter-6+ auto-routing: each class gets a
|
||||
// different sub-pipeline (specialist model, reviewer rotation, etc).
|
||||
const SIGNAL_PHRASES = [
|
||||
"pseudocode", "placeholder", "stub", "unwired", "missing", "dead code", "orphaned",
|
||||
"duplicate", "redundant", "refactor", "rewrite", "remove", "unused", "unnecessary",
|
||||
];
|
||||
|
||||
async function signalClasses(): Promise<any> {
|
||||
const runsDir = `${REPO}/tests/real-world/runs`;
|
||||
// Load every review, group by file, sort by timestamp
|
||||
const perFile: Record<string, Array<{run: string, phrases: Set<string>, score: number | null, conf_avg: number | null, findings: number, ts: number}>> = {};
|
||||
try {
|
||||
const dirs = await Array.fromAsync(new Bun.Glob("scrum_*").scan({ cwd: runsDir, onlyFiles: false }));
|
||||
for (const d of dirs) {
|
||||
const files = await Array.fromAsync(new Bun.Glob("review_*.json").scan({ cwd: `${runsDir}/${d}` }));
|
||||
for (const f of files) {
|
||||
try {
|
||||
const p = `${runsDir}/${d}/${f}`;
|
||||
const j = JSON.parse(await Bun.file(p).text());
|
||||
const key = j.file?.replace("/home/profit/lakehouse/", "") ?? "?";
|
||||
const sug = (j.suggestions ?? "").toLowerCase();
|
||||
const phrases = new Set<string>();
|
||||
for (const ph of SIGNAL_PHRASES) if (sug.includes(ph)) phrases.add(ph);
|
||||
const scoreMatch = sug.match(/(\d(?:\.\d)?)\s*\/\s*10\b/);
|
||||
const score = scoreMatch ? parseFloat(scoreMatch[1]) : null;
|
||||
const mconf = [...sug.matchAll(/(?:confidence[*:\s]*\s*|\|\s*)(\d{1,3})\s*%/gi)].map(m=>parseInt(m[1],10));
|
||||
const jconf = [...sug.matchAll(/"confidence"\s*:\s*(\d{1,3})(?!\d)/gi)].map(m=>parseInt(m[1],10));
|
||||
const all = [...mconf, ...jconf].filter(x => 0 <= x && x <= 100);
|
||||
const conf_avg = all.length ? Math.round(all.reduce((a,b)=>a+b,0)/all.length) : null;
|
||||
const ts = (await Bun.file(p).stat()).mtime.getTime();
|
||||
(perFile[key] ??= []).push({ run: d, phrases, score, conf_avg, findings: all.length, ts });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return { error: String(e), classes: {} };
|
||||
}
|
||||
|
||||
const classes: Record<string, any> = {};
|
||||
for (const [file, runs] of Object.entries(perFile)) {
|
||||
runs.sort((a, b) => a.ts - b.ts);
|
||||
if (runs.length < 2) { classes[file] = { cls: "NEW", runs: runs.length }; continue; }
|
||||
const last = runs[runs.length - 1];
|
||||
const prev = runs[runs.length - 2];
|
||||
const novel = [...last.phrases].filter(p => !prev.phrases.has(p));
|
||||
const resolved = [...prev.phrases].filter(p => !last.phrases.has(p));
|
||||
const looping = [...prev.phrases].filter(p => last.phrases.has(p));
|
||||
const dScore = (last.score != null && prev.score != null) ? last.score - prev.score : null;
|
||||
const dConf = (last.conf_avg != null && prev.conf_avg != null) ? last.conf_avg - prev.conf_avg : null;
|
||||
const dFindings = last.findings - prev.findings;
|
||||
|
||||
let cls: string;
|
||||
if (dScore != null && dScore > 0 && resolved.length > novel.length) cls = "CONVERGING";
|
||||
else if (looping.length >= 3 && novel.length === 0 && (dScore == null || Math.abs(dScore) < 0.5)) cls = "LOOPING";
|
||||
else if (novel.length >= 2 && resolved.length === 0) cls = "ORBITING";
|
||||
else if (Math.abs(dFindings) <= 1 && (dScore == null || Math.abs(dScore) < 0.5)) cls = "PLATEAU";
|
||||
else cls = "MIXED";
|
||||
|
||||
classes[file] = {
|
||||
cls,
|
||||
runs: runs.length,
|
||||
iter_span: `${runs[0].run}…${last.run}`,
|
||||
prev_score: prev.score,
|
||||
last_score: last.score,
|
||||
delta_score: dScore,
|
||||
delta_conf: dConf,
|
||||
delta_findings: dFindings,
|
||||
novel,
|
||||
resolved,
|
||||
looping,
|
||||
};
|
||||
}
|
||||
|
||||
// Summary counts
|
||||
const counts: Record<string, number> = {};
|
||||
for (const v of Object.values(classes)) counts[v.cls] = (counts[v.cls] ?? 0) + 1;
|
||||
|
||||
return { generated_at: new Date().toISOString(), counts, classes };
|
||||
}
|
||||
|
||||
async function refactorSignals(): Promise<any> {
|
||||
// Walk every accepted review across all scrum runs. For each file,
|
||||
// count how many times its suggestions mention a refactor phrase.
|
||||
// Return a sorted list — files most often flagged for refactor first.
|
||||
const runsDir = `${REPO}/tests/real-world/runs`;
|
||||
const perFile: Record<string, { file: string; hits: number; phrases: Record<string, number>; examples: string[]; iterations: number }> = {};
|
||||
try {
|
||||
const dirs = await Array.fromAsync(new Bun.Glob("scrum_*").scan({ cwd: runsDir, onlyFiles: false }));
|
||||
for (const d of dirs) {
|
||||
const files = await Array.fromAsync(new Bun.Glob("review_*.json").scan({ cwd: `${runsDir}/${d}` }));
|
||||
for (const f of files) {
|
||||
const p = `${runsDir}/${d}/${f}`;
|
||||
try {
|
||||
const j = JSON.parse(await Bun.file(p).text());
|
||||
const file = j.file?.replace("/home/profit/lakehouse/", "") ?? "?";
|
||||
const sug = (j.suggestions ?? "").toLowerCase();
|
||||
if (!perFile[file]) perFile[file] = { file, hits: 0, phrases: {}, examples: [], iterations: 0 };
|
||||
perFile[file].iterations++;
|
||||
for (const phrase of REFACTOR_PHRASES) {
|
||||
const count = (sug.match(new RegExp(phrase, "gi")) ?? []).length;
|
||||
if (count > 0) {
|
||||
perFile[file].hits += count;
|
||||
perFile[file].phrases[phrase] = (perFile[file].phrases[phrase] ?? 0) + count;
|
||||
// Pull one example sentence around the phrase
|
||||
if (perFile[file].examples.length < 3) {
|
||||
const idx = sug.indexOf(phrase);
|
||||
if (idx >= 0) {
|
||||
const s = Math.max(0, idx - 60);
|
||||
const e = Math.min(sug.length, idx + phrase.length + 80);
|
||||
perFile[file].examples.push("…" + sug.slice(s, e).replace(/\s+/g, " ") + "…");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return { error: String(e), signals: [] };
|
||||
}
|
||||
const signals = Object.values(perFile)
|
||||
.filter(x => x.hits > 0)
|
||||
.sort((a, b) => b.hits - a.hits)
|
||||
.slice(0, 30);
|
||||
return { signals, scanned: Object.keys(perFile).length };
|
||||
}
|
||||
|
||||
async function reverseIndex(query: string, limit = 20): Promise<any> {
|
||||
// Grep-like substring search across every review's suggestions.
|
||||
// Returns file + snippet + which iter it was in + score + verdict.
|
||||
const runsDir = `${REPO}/tests/real-world/runs`;
|
||||
if (!query || query.length < 2) return { query, hits: [] };
|
||||
const q = query.toLowerCase();
|
||||
const hits: any[] = [];
|
||||
try {
|
||||
const dirs = await Array.fromAsync(new Bun.Glob("scrum_*").scan({ cwd: runsDir, onlyFiles: false }));
|
||||
for (const d of dirs) {
|
||||
const files = await Array.fromAsync(new Bun.Glob("review_*.json").scan({ cwd: `${runsDir}/${d}` }));
|
||||
for (const f of files) {
|
||||
const p = `${runsDir}/${d}/${f}`;
|
||||
try {
|
||||
const j = JSON.parse(await Bun.file(p).text());
|
||||
const sug = j.suggestions ?? "";
|
||||
const lower = sug.toLowerCase();
|
||||
const idx = lower.indexOf(q);
|
||||
if (idx < 0) continue;
|
||||
const s = Math.max(0, idx - 80);
|
||||
const e = Math.min(sug.length, idx + q.length + 200);
|
||||
hits.push({
|
||||
file: j.file?.replace("/home/profit/lakehouse/", ""),
|
||||
run_id: d,
|
||||
model: j.escalated_to_model,
|
||||
snippet: sug.slice(s, e).replace(/\s+/g, " "),
|
||||
});
|
||||
if (hits.length >= limit) break;
|
||||
} catch {}
|
||||
}
|
||||
if (hits.length >= limit) break;
|
||||
}
|
||||
} catch (e) {
|
||||
return { query, error: String(e), hits: [] };
|
||||
}
|
||||
return { query, hits };
|
||||
}
|
||||
|
||||
async function fileHistory(relpath: string): Promise<any> {
|
||||
// Walk all scrum_<id>/review_*.json files and gather every review
|
||||
// for this file path. Returns timeline rows keyed by run_id.
|
||||
const runsDir = `${REPO}/tests/real-world/runs`;
|
||||
const out: any[] = [];
|
||||
try {
|
||||
const dirs = await Array.fromAsync(new Bun.Glob("scrum_*").scan({ cwd: runsDir, onlyFiles: false }));
|
||||
for (const d of dirs) {
|
||||
const safe = relpath.replaceAll("/", "_");
|
||||
const p = `${runsDir}/${d}/review_${safe}.json`;
|
||||
if (await Bun.file(p).exists()) {
|
||||
const j = JSON.parse(await Bun.file(p).text());
|
||||
const sug = j.suggestions ?? "";
|
||||
const scoreMatch = sug.match(/(?:score[\s*:]*)?(\d(?:\.\d)?)\s*\/\s*10\b/i);
|
||||
const score = scoreMatch ? parseFloat(scoreMatch[1]) : null;
|
||||
const confs = [...sug.matchAll(/(?:Confidence[*:\s]*\s*|\|\s*)(\d{1,3})\s*%/gi)]
|
||||
.map(m => parseInt(m[1], 10)).filter(x => x >= 0 && x <= 100);
|
||||
const jsonConfs = [...sug.matchAll(/"confidence"\s*:\s*(\d{1,3})(?!\d)/gi)]
|
||||
.map(m => parseInt(m[1], 10)).filter(x => x >= 0 && x <= 100);
|
||||
const all = [...confs, ...jsonConfs];
|
||||
const mt = await Bun.file(p).stat();
|
||||
out.push({
|
||||
run_id: d,
|
||||
reviewed_at: j.reviewed_at ?? mt.mtime,
|
||||
model: j.escalated_to_model,
|
||||
score,
|
||||
chars: sug.length,
|
||||
conf_avg: all.length ? Math.round(all.reduce((a,b)=>a+b,0)/all.length) : null,
|
||||
conf_min: all.length ? Math.min(...all) : null,
|
||||
findings: all.length,
|
||||
output_format: sug.includes('"verdict"') ? "forensic_json" : "markdown",
|
||||
// first 1200 chars preview
|
||||
preview: sug.slice(0, 1200),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return { error: String(e), history: [] };
|
||||
}
|
||||
out.sort((a, b) => String(a.reviewed_at).localeCompare(String(b.reviewed_at)));
|
||||
return { file: relpath, history: out };
|
||||
}
|
||||
|
||||
Bun.serve({
|
||||
port: PORT,
|
||||
hostname: "0.0.0.0",
|
||||
async fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
const path = url.pathname;
|
||||
|
||||
// Static shell
|
||||
if (path === "/" || path === "/index.html") {
|
||||
return new Response(Bun.file(`${REPO}/ui/index.html`));
|
||||
}
|
||||
if (path === "/ui.css") {
|
||||
return new Response(Bun.file(`${REPO}/ui/ui.css`), { headers: { "content-type": "text/css" } });
|
||||
}
|
||||
if (path === "/ui.js") {
|
||||
return new Response(Bun.file(`${REPO}/ui/ui.js`), { headers: { "content-type": "application/javascript" } });
|
||||
}
|
||||
|
||||
// Data API
|
||||
if (path === "/data/services") return Response.json(await servicesSnapshot());
|
||||
if (path === "/data/reviews") {
|
||||
const n = Number(url.searchParams.get("tail") ?? 50);
|
||||
return Response.json(await tailJsonl(`${KB}/scrum_reviews.jsonl`, n));
|
||||
}
|
||||
if (path === "/data/findings") return Response.json(await tailJsonl(`${KB}/phase_sweep_findings.jsonl`));
|
||||
if (path === "/data/metrics") return Response.json(await tailJsonl(`${KB}/scrum_loop_metrics.jsonl`));
|
||||
if (path === "/data/trust") return Response.json(await tailJsonl(`${KB}/model_trust.jsonl`, 200));
|
||||
if (path === "/data/overrides") return Response.json(await tailJsonl(`${KB}/human_overrides.jsonl`));
|
||||
if (path === "/data/outcomes") return Response.json(await tailJsonl(`${KB}/outcomes.jsonl`, 30));
|
||||
if (path === "/data/audit_facts") return Response.json(await tailJsonl(`${KB}/audit_facts.jsonl`, 30));
|
||||
|
||||
// Pathway memory — consensus-designed sidecar (2026-04-24). Two
|
||||
// exposed metrics: reuse_rate (activity — is it firing?) and
|
||||
// avg_rungs_saved_per_commit (value — is it earning its keep?).
|
||||
// Round-3 consensus (qwen3.5:397b) pointed out that activity
|
||||
// without value tells us nothing; the UI needs both to judge the
|
||||
// health of the hot-swap learning loop.
|
||||
if (path === "/data/pathway_stats") {
|
||||
try {
|
||||
const r = await fetch("http://localhost:3100/vectors/pathway/stats", { signal: AbortSignal.timeout(3000) });
|
||||
if (!r.ok) return Response.json({ error: `vectord ${r.status}`, stats: null });
|
||||
const stats = await r.json();
|
||||
// Tail recent scrum events to compute avg_rungs_saved_per_commit
|
||||
// (a committed review = any row in scrum_reviews.jsonl; rungs_saved
|
||||
// only populates when pathway memory fired AND the recommended
|
||||
// model actually produced the accept).
|
||||
const reviews = await tailJsonl(`${KB}/scrum_reviews.jsonl`, 200);
|
||||
let totalCommits = 0;
|
||||
let totalRungsSaved = 0;
|
||||
let hotSwapHits = 0;
|
||||
for (const r of reviews) {
|
||||
totalCommits++;
|
||||
if (r.pathway_hot_swap_hit) hotSwapHits++;
|
||||
if (typeof r.rungs_saved === "number") totalRungsSaved += r.rungs_saved;
|
||||
}
|
||||
return Response.json({
|
||||
stats,
|
||||
scrum_window: {
|
||||
reviews: totalCommits,
|
||||
hot_swap_hits: hotSwapHits,
|
||||
pathway_reuse_rate: totalCommits ? hotSwapHits / totalCommits : 0,
|
||||
avg_rungs_saved_per_commit: totalCommits ? totalRungsSaved / totalCommits : 0,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
return Response.json({ error: (e as Error).message, stats: null });
|
||||
}
|
||||
}
|
||||
|
||||
if (path.startsWith("/data/file/")) {
|
||||
const relpath = decodeURIComponent(path.slice("/data/file/".length));
|
||||
return Response.json(await fileHistory(relpath));
|
||||
}
|
||||
if (path === "/data/refactor_signals") {
|
||||
return Response.json(await refactorSignals());
|
||||
}
|
||||
if (path === "/data/signal_classes") {
|
||||
return Response.json(await signalClasses());
|
||||
}
|
||||
if (path === "/data/search") {
|
||||
const q = url.searchParams.get("q") ?? "";
|
||||
return Response.json(await reverseIndex(q, 30));
|
||||
}
|
||||
|
||||
// Per-service systemd log tail. Allowed service list is fixed so the
|
||||
// :service path param can never be used to invoke arbitrary units.
|
||||
if (path.startsWith("/data/logs/")) {
|
||||
const svc = path.slice("/data/logs/".length).split("?")[0];
|
||||
const UNITS: Record<string, string> = {
|
||||
gateway: "lakehouse.service",
|
||||
sidecar: "lakehouse-sidecar.service",
|
||||
observer: "lakehouse-observer.service",
|
||||
mcp: "lakehouse-agent.service",
|
||||
context7: "lakehouse-context7-bridge.service",
|
||||
auditor: "lakehouse-auditor.service",
|
||||
langfuse: "lakehouse-langfuse-bridge.service",
|
||||
};
|
||||
const unit = UNITS[svc];
|
||||
if (!unit) return Response.json({ error: "unknown service", allowed: Object.keys(UNITS) }, { status: 400 });
|
||||
const n = Number(url.searchParams.get("n") ?? 60);
|
||||
try {
|
||||
// Use execFile-style API: pass args as array, never shell-interpolate
|
||||
const proc = Bun.spawn(["journalctl", "-u", unit, "-n", String(n), "--no-pager", "--output=short-iso"], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const text = await new Response(proc.stdout).text();
|
||||
await proc.exited;
|
||||
const lines = text.split("\n").filter(Boolean);
|
||||
return Response.json({ service: svc, unit, lines });
|
||||
} catch (e) {
|
||||
return Response.json({ service: svc, unit, error: String(e), lines: [] });
|
||||
}
|
||||
}
|
||||
|
||||
// Live scrum log tail — best-effort
|
||||
if (path === "/data/scrum_log") {
|
||||
try {
|
||||
const bg = await Array.fromAsync(new Bun.Glob("scrum_iter*.log").scan({ cwd: "/tmp" }));
|
||||
if (bg.length === 0) return Response.json({ lines: [] });
|
||||
bg.sort();
|
||||
const latest = `/tmp/${bg[bg.length - 1]}`;
|
||||
const text = await Bun.file(latest).text();
|
||||
const lines = text.split("\n").slice(-80);
|
||||
return Response.json({ file: latest, lines });
|
||||
} catch (e) {
|
||||
return Response.json({ error: String(e) });
|
||||
}
|
||||
}
|
||||
|
||||
return new Response("not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`[ui] visual control plane listening on http://0.0.0.0:${PORT}`);
|
||||
@@ -0,0 +1,440 @@
|
||||
/* Lakehouse Visual Control Plane — neo-brutalist dark */
|
||||
|
||||
:root {
|
||||
--bg: #0a0c10;
|
||||
--bg-1: #10141a;
|
||||
--bg-2: #171c24;
|
||||
--border: #2a303b;
|
||||
--border-hi: #3a4252;
|
||||
--fg: #e8ecf3;
|
||||
--fg-dim: #8a94a7;
|
||||
--fg-muted: #525c6f;
|
||||
--green: #3eed86;
|
||||
--yellow: #ffbf3c;
|
||||
--red: #ff4d6e;
|
||||
--blue: #55c5ff;
|
||||
--purple: #b57cff;
|
||||
--orange: #ff9f43;
|
||||
--shadow: 0 2px 0 #000;
|
||||
--mono: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, monospace;
|
||||
--sans: -apple-system, Inter, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { height: 100%; overflow: hidden; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: var(--sans);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ────── TOP BAR ────── */
|
||||
#topbar {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 8px; font-weight: 700; letter-spacing: 0.08em; }
|
||||
.brand .sig { color: var(--green); font-size: 16px; }
|
||||
.brand .build { color: var(--fg-muted); font-size: 10px; font-family: var(--mono); margin-left: 6px; }
|
||||
|
||||
#views { display: flex; gap: 2px; margin-left: 20px; }
|
||||
#views button {
|
||||
background: transparent; border: 1px solid var(--border); color: var(--fg-dim);
|
||||
font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em;
|
||||
padding: 5px 10px; cursor: pointer; text-transform: uppercase;
|
||||
}
|
||||
#views button:hover { border-color: var(--border-hi); color: var(--fg); }
|
||||
#views button.on { background: var(--fg); color: var(--bg); border-color: var(--fg); font-weight: 700; }
|
||||
|
||||
#hb { margin-left: auto; display: flex; gap: 6px; }
|
||||
.hbchip {
|
||||
font-family: var(--mono); font-size: 10px; letter-spacing: 0.05em;
|
||||
padding: 4px 8px; border: 1px solid var(--border); border-radius: 2px;
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
.hbchip[data-status="healthy"] { border-color: var(--green); color: var(--green); }
|
||||
.hbchip[data-status="down"] { border-color: var(--red); color: var(--red); }
|
||||
.hbchip[data-status="degraded"]{ border-color: var(--yellow); color: var(--yellow); }
|
||||
|
||||
/* ────── MAIN ────── */
|
||||
main {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 380px;
|
||||
min-height: 0;
|
||||
}
|
||||
#stage { position: relative; border-right: 1px solid var(--border); min-height: 0; overflow: hidden; }
|
||||
|
||||
.view { display: none; width: 100%; height: 100%; }
|
||||
.view.on { display: block; }
|
||||
|
||||
.subhead {
|
||||
height: 32px; display: flex; align-items: center; gap: 10px;
|
||||
padding: 0 14px; border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-1);
|
||||
font-family: var(--mono); font-size: 11px; color: var(--fg-dim);
|
||||
text-transform: uppercase; letter-spacing: 0.08em;
|
||||
}
|
||||
.subhead .spacer { flex: 1; }
|
||||
|
||||
/* ────── MAP ────── */
|
||||
#view-map { position: relative; }
|
||||
#overlay-controls {
|
||||
position: absolute; top: 10px; left: 10px; z-index: 2;
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
background: rgba(16,20,26,0.95);
|
||||
border: 1px solid var(--border); padding: 4px;
|
||||
}
|
||||
#overlay-controls .lbl { font-family: var(--mono); font-size: 10px; color: var(--fg-muted); padding: 0 8px; }
|
||||
#overlay-controls button {
|
||||
background: transparent; color: var(--fg-dim); border: 1px solid var(--border);
|
||||
font-family: var(--mono); font-size: 10px; padding: 3px 8px; cursor: pointer;
|
||||
text-transform: lowercase; letter-spacing: 0.05em;
|
||||
}
|
||||
#overlay-controls button:hover { border-color: var(--border-hi); color: var(--fg); }
|
||||
#overlay-controls button.on { background: var(--fg); color: var(--bg); border-color: var(--fg); }
|
||||
|
||||
#map { width: 100%; height: 100%; }
|
||||
#map .node-circle { stroke: var(--fg); stroke-width: 2; cursor: pointer; transition: r 200ms; }
|
||||
#map .node-circle:hover { stroke-width: 3; }
|
||||
#map .node-label {
|
||||
font-family: var(--mono); font-size: 11px; fill: var(--fg);
|
||||
pointer-events: none; text-anchor: middle; font-weight: 600;
|
||||
}
|
||||
#map .node-sub {
|
||||
font-family: var(--mono); font-size: 9px; fill: var(--fg-muted);
|
||||
pointer-events: none; text-anchor: middle;
|
||||
}
|
||||
#map .edge { stroke: var(--border-hi); stroke-width: 1.5; fill: none; }
|
||||
#map .edge.active { stroke: var(--blue); stroke-width: 2; stroke-dasharray: 4 3; animation: dash 1.5s linear infinite; }
|
||||
@keyframes dash { to { stroke-dashoffset: -14; } }
|
||||
|
||||
#map .node-selected { stroke: var(--yellow); stroke-width: 3; }
|
||||
|
||||
#legend {
|
||||
position: absolute; bottom: 10px; left: 10px;
|
||||
display: flex; gap: 16px; font-family: var(--mono); font-size: 10px;
|
||||
background: rgba(16,20,26,0.95); border: 1px solid var(--border); padding: 6px 10px;
|
||||
}
|
||||
.lg { color: var(--fg-muted); }
|
||||
.lg.healthy::before { content: ''; }
|
||||
.lg.healthy { color: var(--green); }
|
||||
.lg.degraded { color: var(--yellow); }
|
||||
.lg.down { color: var(--red); }
|
||||
.lg.active { color: var(--blue); }
|
||||
|
||||
/* ────── CONTEXT PANEL ────── */
|
||||
#context {
|
||||
background: var(--bg-1); overflow-y: auto; overflow-x: hidden;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.ctx-header {
|
||||
height: 44px; display: flex; flex-direction: column; justify-content: center;
|
||||
padding: 4px 14px; border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-2);
|
||||
}
|
||||
.ctx-eyebrow { font-family: var(--mono); font-size: 10px; color: var(--fg-muted); letter-spacing: 0.1em; }
|
||||
#ctx-target { font-family: var(--mono); font-size: 12px; color: var(--fg); font-weight: 600; margin-top: 2px; }
|
||||
#ctx-body { padding: 12px 14px; flex: 1; overflow-y: auto; }
|
||||
.ctx-hint { color: var(--fg-muted); font-style: italic; font-size: 11px; }
|
||||
|
||||
.ctx-row { padding: 6px 0; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; gap: 10px; font-family: var(--mono); font-size: 11px; }
|
||||
.ctx-row .k { color: var(--fg-muted); text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
.ctx-row .v { color: var(--fg); text-align: right; word-break: break-all; }
|
||||
.ctx-row .v.good { color: var(--green); }
|
||||
.ctx-row .v.warn { color: var(--yellow); }
|
||||
.ctx-row .v.bad { color: var(--red); }
|
||||
|
||||
.ctx-section-hd { font-family: var(--mono); font-size: 10px; color: var(--fg-muted); letter-spacing: 0.1em; margin: 14px 0 4px; text-transform: uppercase; }
|
||||
|
||||
.pill {
|
||||
display: inline-block; font-family: var(--mono); font-size: 10px;
|
||||
padding: 2px 7px; border: 1px solid var(--border); margin-right: 4px;
|
||||
}
|
||||
.pill.tier-auto { border-color: var(--green); color: var(--green); }
|
||||
.pill.tier-dry_run { border-color: var(--blue); color: var(--blue); }
|
||||
.pill.tier-simulation { border-color: var(--yellow); color: var(--yellow); }
|
||||
.pill.tier-block { border-color: var(--red); color: var(--red); }
|
||||
.pill.ver-needs_patch { border-color: var(--orange); color: var(--orange); }
|
||||
.pill.ver-pass { border-color: var(--green); color: var(--green); }
|
||||
.pill.ver-fail { border-color: var(--red); color: var(--red); }
|
||||
.pill.fmt-forensic_json { border-color: var(--purple); color: var(--purple); }
|
||||
.pill.fmt-markdown { border-color: var(--fg-dim); color: var(--fg-dim); }
|
||||
|
||||
/* ────── TRACE ────── */
|
||||
#trace-timeline {
|
||||
display: flex; gap: 0; padding: 20px;
|
||||
overflow-x: auto; border-bottom: 1px solid var(--border);
|
||||
min-height: 140px;
|
||||
}
|
||||
.trace-node {
|
||||
position: relative; flex: 0 0 140px; padding: 10px 12px;
|
||||
border: 2px solid var(--border); background: var(--bg-1); cursor: pointer;
|
||||
font-family: var(--mono); font-size: 10px;
|
||||
}
|
||||
.trace-node:hover { border-color: var(--border-hi); }
|
||||
.trace-node.active { border-color: var(--yellow); background: var(--bg-2); }
|
||||
.trace-node::after {
|
||||
content: '→'; position: absolute; right: -14px; top: 50%; transform: translateY(-50%);
|
||||
color: var(--fg-muted); font-size: 16px;
|
||||
}
|
||||
.trace-node:last-child::after { display: none; }
|
||||
.trace-node .tn-run { color: var(--fg-muted); letter-spacing: 0.05em; margin-bottom: 4px; }
|
||||
.trace-node .tn-score { font-size: 22px; font-weight: 700; color: var(--fg); }
|
||||
.trace-node .tn-conf { color: var(--fg-dim); margin-top: 4px; }
|
||||
.trace-node .tn-model { color: var(--purple); margin-top: 4px; font-size: 9px; }
|
||||
|
||||
#trace-detail { padding: 16px; overflow-y: auto; height: calc(100% - 172px); }
|
||||
#trace-detail pre { font-family: var(--mono); font-size: 11px; color: var(--fg-dim); white-space: pre-wrap; word-break: break-word; }
|
||||
|
||||
/* ────── METRICS ────── */
|
||||
.metric-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 12px; padding: 16px; overflow-y: auto; max-height: 100%;
|
||||
}
|
||||
.metric {
|
||||
border: 1px solid var(--border); background: var(--bg-1);
|
||||
padding: 14px; display: flex; flex-direction: column; gap: 6px;
|
||||
}
|
||||
.metric .m-label { font-family: var(--mono); font-size: 10px; color: var(--fg-muted); letter-spacing: 0.08em; text-transform: uppercase; }
|
||||
.metric .m-big { font-size: 28px; font-weight: 800; letter-spacing: -0.02em; }
|
||||
.metric .m-sub { font-family: var(--mono); font-size: 11px; color: var(--fg-dim); }
|
||||
.metric .m-explain {
|
||||
color: var(--fg); font-family: var(--sans); font-size: 12px;
|
||||
line-height: 1.45; margin-top: 2px; font-weight: 400;
|
||||
}
|
||||
.metric .m-source {
|
||||
color: var(--fg-muted); font-size: 10px; letter-spacing: 0.05em;
|
||||
border-top: 1px dashed var(--border); padding-top: 6px; margin-top: 6px;
|
||||
}
|
||||
.metric .m-good {
|
||||
color: var(--green); font-size: 10px; letter-spacing: 0.03em;
|
||||
line-height: 1.5; opacity: 0.85;
|
||||
}
|
||||
.metric.warn .m-good { color: var(--yellow); }
|
||||
.metric.bad .m-good { color: var(--red); }
|
||||
.metric.good .m-big { color: var(--green); }
|
||||
.metric.warn .m-big { color: var(--yellow); }
|
||||
.metric.bad .m-big { color: var(--red); }
|
||||
|
||||
.bar { display: flex; height: 8px; border: 1px solid var(--border); background: var(--bg); margin-top: 4px; }
|
||||
.bar > span { display: block; height: 100%; }
|
||||
.bar .seg-auto { background: var(--green); }
|
||||
.bar .seg-dry_run { background: var(--blue); }
|
||||
.bar .seg-simulation { background: var(--yellow); }
|
||||
.bar .seg-block { background: var(--red); }
|
||||
|
||||
/* ────── KB ────── */
|
||||
.kb-grid {
|
||||
padding: 16px; display: grid; gap: 10px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
|
||||
overflow-y: auto; max-height: 100%;
|
||||
}
|
||||
.kb-banner {
|
||||
grid-column: 1 / -1;
|
||||
border: 1px solid var(--border); background: var(--bg-1);
|
||||
padding: 14px 16px; border-left: 3px solid var(--blue);
|
||||
}
|
||||
.kb-banner-title {
|
||||
font-family: var(--mono); font-size: 11px; color: var(--blue);
|
||||
letter-spacing: 0.1em; font-weight: 700; margin-bottom: 6px;
|
||||
}
|
||||
.kb-banner-body {
|
||||
color: var(--fg); font-size: 12px; line-height: 1.55;
|
||||
}
|
||||
.kb-statline {
|
||||
grid-column: 1 / -1;
|
||||
display: flex; gap: 18px;
|
||||
font-family: var(--mono); font-size: 11px; color: var(--fg-dim);
|
||||
padding: 8px 14px; border: 1px solid var(--border);
|
||||
background: var(--bg-2); margin-bottom: 2px;
|
||||
}
|
||||
.kb-statline .stat-warn { color: var(--yellow); font-weight: 700; }
|
||||
.kb-file { border: 1px solid var(--border); background: var(--bg-1); padding: 10px 12px; cursor: pointer; }
|
||||
.kb-file:hover { border-color: var(--border-hi); }
|
||||
.kb-file .kf-path { font-family: var(--mono); font-size: 11px; color: var(--fg); word-break: break-all; }
|
||||
.kb-file .kf-meta { font-family: var(--mono); font-size: 10px; color: var(--fg-muted); margin-top: 4px; display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.kb-file .kf-score { font-weight: 700; color: var(--fg); }
|
||||
.kb-file .kf-delta.up { color: var(--green); }
|
||||
.kb-file .kf-delta.down { color: var(--red); }
|
||||
|
||||
/* ────── CONSOLE ────── */
|
||||
#view-console { background: #000; display: none; flex-direction: column; }
|
||||
#view-console.on { display: flex; }
|
||||
.console-toolbar {
|
||||
height: 36px; display: flex; align-items: center; gap: 8px;
|
||||
padding: 0 12px; background: var(--bg-1); border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.console-toolbar .con-eyebrow {
|
||||
font-family: var(--mono); font-size: 10px; color: var(--fg-muted); letter-spacing: 0.1em;
|
||||
text-transform: uppercase; margin-right: 6px;
|
||||
}
|
||||
.console-toolbar .spacer { flex: 1; }
|
||||
#con-tabs { display: flex; gap: 2px; }
|
||||
#con-tabs button {
|
||||
background: transparent; border: 1px solid var(--border); color: var(--fg-dim);
|
||||
font-family: var(--mono); font-size: 10px; letter-spacing: 0.05em;
|
||||
padding: 4px 10px; cursor: pointer; text-transform: lowercase;
|
||||
}
|
||||
#con-tabs button:hover { border-color: var(--border-hi); color: var(--fg); }
|
||||
#con-tabs button.on { background: var(--fg); color: var(--bg); border-color: var(--fg); font-weight: 700; }
|
||||
#con-unit { font-family: var(--mono); font-size: 10px; color: var(--fg-muted); }
|
||||
|
||||
#console-log {
|
||||
flex: 1; padding: 12px 16px; overflow-y: auto;
|
||||
font-family: var(--mono); font-size: 11px; color: var(--fg);
|
||||
line-height: 1.5;
|
||||
}
|
||||
#console-log .cl-line { white-space: pre-wrap; word-break: break-all; }
|
||||
#console-log .cl-info { color: var(--fg-dim); }
|
||||
#console-log .cl-ok { color: var(--green); }
|
||||
#console-log .cl-warn { color: var(--yellow); }
|
||||
#console-log .cl-err { color: var(--red); }
|
||||
|
||||
/* ────── STREAM (bottom) ────── */
|
||||
#stream {
|
||||
height: 180px; background: var(--bg-1);
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex; flex-direction: column; flex-shrink: 0;
|
||||
}
|
||||
.stream-head {
|
||||
height: 26px; display: flex; align-items: center; gap: 10px;
|
||||
padding: 0 14px; border-bottom: 1px solid var(--border);
|
||||
font-family: var(--mono); font-size: 10px; color: var(--fg-muted);
|
||||
letter-spacing: 0.1em; text-transform: uppercase;
|
||||
}
|
||||
.stream-head .spacer { flex: 1; }
|
||||
#stream-file { color: var(--fg-dim); font-size: 10px; }
|
||||
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); animation: pulse 1.4s ease-in-out infinite; }
|
||||
@keyframes pulse { 50% { opacity: 0.35; transform: scale(0.8); } }
|
||||
|
||||
#stream-body {
|
||||
flex: 1; padding: 8px 14px; overflow-y: auto; font-family: var(--mono); font-size: 11px; color: var(--fg-dim);
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.sline { padding: 1px 0; white-space: pre; }
|
||||
.sline.ok { color: var(--green); }
|
||||
.sline.thin { color: var(--yellow); }
|
||||
.sline.err { color: var(--red); }
|
||||
.sline.info { color: var(--fg-dim); }
|
||||
.sline.head { color: var(--fg); font-weight: 600; }
|
||||
|
||||
/* ────── TRAJECTORY view ────── */
|
||||
#view-trajectory { display: none; flex-direction: column; }
|
||||
#view-trajectory.on { display: flex; }
|
||||
.traj-header {
|
||||
border-bottom: 1px solid var(--border); background: var(--bg-1);
|
||||
padding: 12px 16px; display: flex; flex-direction: column; gap: 6px;
|
||||
}
|
||||
#traj-search {
|
||||
width: 100%; background: var(--bg-2); color: var(--fg);
|
||||
border: 1px solid var(--border); padding: 8px 10px;
|
||||
font-family: var(--mono); font-size: 12px;
|
||||
}
|
||||
#traj-search:focus { outline: none; border-color: var(--blue); }
|
||||
#traj-stats {
|
||||
font-family: var(--mono); font-size: 10px; color: var(--fg-muted); letter-spacing: 0.06em;
|
||||
}
|
||||
#traj-body {
|
||||
flex: 1; overflow-y: auto; padding: 16px;
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
}
|
||||
.traj-section-head {
|
||||
font-family: var(--mono); font-size: 11px; color: var(--blue);
|
||||
letter-spacing: 0.1em; font-weight: 700; margin-top: 14px; margin-bottom: 4px;
|
||||
border-bottom: 1px solid var(--border); padding-bottom: 6px;
|
||||
}
|
||||
.traj-section-head:first-child { margin-top: 0; }
|
||||
.traj-section-explain {
|
||||
color: var(--fg); font-size: 12px; line-height: 1.55; margin-bottom: 6px;
|
||||
padding: 8px 10px; background: var(--bg-1); border-left: 2px solid var(--blue);
|
||||
}
|
||||
.traj-table { display: flex; flex-direction: column; border: 1px solid var(--border); }
|
||||
.traj-row {
|
||||
display: grid; grid-template-columns: 40px 1.5fr 80px 2fr 80px; gap: 12px;
|
||||
padding: 8px 12px; border-bottom: 1px solid var(--border);
|
||||
font-family: var(--mono); font-size: 11px; cursor: pointer;
|
||||
}
|
||||
.traj-row:hover { background: var(--bg-2); }
|
||||
.traj-row:last-child { border-bottom: none; }
|
||||
.traj-col-rank { color: var(--fg-muted); font-weight: 700; }
|
||||
.traj-col-file { color: var(--fg); }
|
||||
.traj-col-hits { color: var(--red); font-weight: 700; }
|
||||
.traj-col-phrases { color: var(--fg-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.traj-col-iters { color: var(--fg-muted); text-align: right; }
|
||||
|
||||
.traj-spark-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(480px, 1fr)); gap: 10px;
|
||||
}
|
||||
.traj-spark {
|
||||
border: 1px solid var(--border); background: var(--bg-1);
|
||||
padding: 12px; cursor: pointer;
|
||||
}
|
||||
.traj-spark:hover { border-color: var(--border-hi); }
|
||||
.traj-spark-file { font-family: var(--mono); font-size: 11px; color: var(--fg); margin-bottom: 8px; }
|
||||
.traj-spark-line { display: flex; align-items: center; gap: 6px; }
|
||||
.traj-spark-pt {
|
||||
flex: 0 0 80px; padding: 6px 8px; border: 1px solid var(--border);
|
||||
background: var(--bg-2); text-align: center;
|
||||
}
|
||||
.traj-pt-score { font-family: var(--mono); font-size: 14px; font-weight: 800; color: var(--fg); }
|
||||
.traj-pt-conf { font-family: var(--mono); font-size: 10px; color: var(--fg-dim); }
|
||||
.traj-pt-label { font-family: var(--mono); font-size: 9px; color: var(--fg-muted); letter-spacing: 0.06em; }
|
||||
.traj-spark-arrow { color: var(--fg-muted); font-size: 14px; }
|
||||
.traj-spark-delta { font-family: var(--mono); font-size: 11px; color: var(--fg-dim); margin-top: 8px; }
|
||||
.traj-spark-delta .delta-up { color: var(--green); }
|
||||
.traj-spark-delta .delta-down { color: var(--red); }
|
||||
.traj-spark-empty { font-family: var(--mono); font-size: 10px; color: var(--fg-muted); font-style: italic; }
|
||||
|
||||
.traj-hit {
|
||||
border: 1px solid var(--border); padding: 10px 12px; background: var(--bg-1); cursor: pointer;
|
||||
}
|
||||
.traj-hit:hover { border-color: var(--border-hi); }
|
||||
.traj-hit-top { display: flex; gap: 14px; margin-bottom: 6px; }
|
||||
.traj-hit-file { font-family: var(--mono); font-size: 11px; color: var(--fg); font-weight: 600; }
|
||||
.traj-hit-meta { font-family: var(--mono); font-size: 10px; color: var(--fg-muted); }
|
||||
.traj-hit-snip { font-family: var(--mono); font-size: 11px; color: var(--fg-dim); line-height: 1.5; }
|
||||
|
||||
/* signal classes */
|
||||
.signal-class-row { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
|
||||
.signal-chip {
|
||||
font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em;
|
||||
padding: 4px 10px; border: 1px solid var(--border); font-weight: 700;
|
||||
}
|
||||
.signal-converging { color: var(--green); border-color: var(--green); }
|
||||
.signal-looping { color: var(--red); border-color: var(--red); }
|
||||
.signal-orbiting { color: var(--purple); border-color: var(--purple); }
|
||||
.signal-plateau { color: var(--yellow); border-color: var(--yellow); }
|
||||
.signal-mixed { color: var(--blue); border-color: var(--blue); }
|
||||
.signal-new { color: var(--fg-muted); border-color: var(--fg-muted); }
|
||||
.signal-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(480px, 1fr)); gap: 8px;
|
||||
}
|
||||
.signal-card {
|
||||
border: 1px solid var(--border); background: var(--bg-1);
|
||||
padding: 10px 12px; cursor: pointer; border-left: 3px solid var(--fg-muted);
|
||||
}
|
||||
.signal-card.signal-converging { border-left-color: var(--green); }
|
||||
.signal-card.signal-looping { border-left-color: var(--red); }
|
||||
.signal-card.signal-orbiting { border-left-color: var(--purple); }
|
||||
.signal-card.signal-plateau { border-left-color: var(--yellow); }
|
||||
.signal-card.signal-mixed { border-left-color: var(--blue); }
|
||||
.signal-card:hover { border-color: var(--border-hi); background: var(--bg-2); }
|
||||
.signal-card-top { display: flex; gap: 10px; align-items: center; margin-bottom: 6px; }
|
||||
.signal-card-file { font-family: var(--mono); font-size: 11px; color: var(--fg); }
|
||||
.signal-card-body { font-family: var(--mono); font-size: 10px; color: var(--fg-dim); line-height: 1.55; }
|
||||
.signal-card-body > div { margin-top: 2px; }
|
||||
.signal-novel { color: var(--purple); }
|
||||
.signal-resolved { color: var(--green); }
|
||||
.signal-loop { color: var(--red); }
|
||||
|
||||
@@ -0,0 +1,883 @@
|
||||
// Visual Control Plane — client (vanilla JS, D3 from CDN)
|
||||
// Design note: KB data flows from local jsonl files we control, but we
|
||||
// still use DOM methods (createElement/textContent) for every
|
||||
// data-derived node to satisfy static analysis and keep a clean XSS
|
||||
// boundary if the UI ever gets exposed.
|
||||
|
||||
const POLL_MS = 3000;
|
||||
|
||||
const state = {
|
||||
view: "map",
|
||||
overlay: "status",
|
||||
selected: null,
|
||||
services: null,
|
||||
reviews: [],
|
||||
metrics: [],
|
||||
overrides: [],
|
||||
trust: [],
|
||||
findings: [],
|
||||
};
|
||||
|
||||
// ───── view switcher ─────
|
||||
document.querySelectorAll("#views button").forEach(b => {
|
||||
b.addEventListener("click", () => {
|
||||
document.querySelectorAll("#views button").forEach(x => x.classList.remove("on"));
|
||||
b.classList.add("on");
|
||||
state.view = b.dataset.view;
|
||||
document.querySelectorAll(".view").forEach(v => v.classList.remove("on"));
|
||||
document.getElementById(`view-${state.view}`).classList.add("on");
|
||||
renderView();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("#overlay-controls button").forEach(b => {
|
||||
b.addEventListener("click", () => {
|
||||
document.querySelectorAll("#overlay-controls button").forEach(x => x.classList.remove("on"));
|
||||
b.classList.add("on");
|
||||
state.overlay = b.dataset.ov;
|
||||
if (state.services) drawMap(state.services);
|
||||
});
|
||||
});
|
||||
|
||||
// ───── helpers ─────
|
||||
function el(tag, opts = {}, ...kids) {
|
||||
const n = document.createElement(tag);
|
||||
if (opts.className) n.className = opts.className;
|
||||
if (opts.text != null) n.textContent = String(opts.text);
|
||||
if (opts.data) for (const k in opts.data) n.dataset[k] = opts.data[k];
|
||||
if (opts.attrs) for (const k in opts.attrs) n.setAttribute(k, opts.attrs[k]);
|
||||
if (opts.style) for (const k in opts.style) n.style[k] = opts.style[k];
|
||||
for (const k of kids) if (k != null) n.append(k);
|
||||
return n;
|
||||
}
|
||||
function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
|
||||
function row(k, v, valClass) {
|
||||
const r = el("div", { className: "ctx-row" });
|
||||
r.append(el("span", { className: "k", text: k }));
|
||||
const vv = el("span", { className: "v" + (valClass ? " " + valClass : ""), text: String(v ?? "-") });
|
||||
r.append(vv);
|
||||
return r;
|
||||
}
|
||||
function short(v) {
|
||||
if (v == null) return "-";
|
||||
if (typeof v === "object") return JSON.stringify(v).slice(0, 80);
|
||||
return String(v).slice(0, 80);
|
||||
}
|
||||
|
||||
// ───── polling ─────
|
||||
async function poll() {
|
||||
try {
|
||||
const [svc, rev, met, ov, tr, fin] = await Promise.all([
|
||||
fetch("/data/services").then(r => r.json()),
|
||||
fetch("/data/reviews?tail=80").then(r => r.json()),
|
||||
fetch("/data/metrics").then(r => r.json()),
|
||||
fetch("/data/overrides").then(r => r.json()),
|
||||
fetch("/data/trust").then(r => r.json()),
|
||||
fetch("/data/findings").then(r => r.json()),
|
||||
]);
|
||||
state.services = svc;
|
||||
state.reviews = Array.isArray(rev) ? rev : [];
|
||||
state.metrics = Array.isArray(met) ? met : [];
|
||||
state.overrides = Array.isArray(ov) ? ov : [];
|
||||
state.trust = Array.isArray(tr) ? tr : [];
|
||||
state.findings = Array.isArray(fin) ? fin : [];
|
||||
document.getElementById("build-ts").textContent = new Date(svc.ts).toLocaleTimeString();
|
||||
svc.nodes.forEach(n => {
|
||||
const chip = document.querySelector(`.hbchip[data-svc="${n.id}"]`);
|
||||
if (chip) chip.setAttribute("data-status", n.status);
|
||||
});
|
||||
renderView();
|
||||
renderContext();
|
||||
pollStream();
|
||||
} catch (e) { console.error("poll error", e); }
|
||||
}
|
||||
|
||||
async function pollStream() {
|
||||
try {
|
||||
const j = await fetch("/data/scrum_log").then(r => r.json());
|
||||
if (!j.lines) return;
|
||||
document.getElementById("stream-file").textContent = j.file ? j.file.split("/").pop() : "—";
|
||||
const body = document.getElementById("stream-body");
|
||||
clear(body);
|
||||
j.lines.slice(-30).forEach(line => {
|
||||
const cls = /✓ ACCEPTED/.test(line) ? "ok"
|
||||
: /✗ thin/.test(line) ? "thin"
|
||||
: /error|failed|FAIL/i.test(line) ? "err"
|
||||
: /^\[scrum\] file:/.test(line) ? "head"
|
||||
: "info";
|
||||
body.append(el("div", { className: "sline " + cls, text: line }));
|
||||
});
|
||||
body.scrollTop = body.scrollHeight;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function renderView() {
|
||||
if (!state.services) return;
|
||||
if (state.view === "map") drawMap(state.services);
|
||||
else if (state.view === "trace") drawTrace();
|
||||
else if (state.view === "trajectory") drawTrajectory();
|
||||
else if (state.view === "metrics") drawMetrics();
|
||||
else if (state.view === "kb") drawKB();
|
||||
else if (state.view === "console") drawConsole();
|
||||
}
|
||||
|
||||
// ───── MAP ─────
|
||||
const NODES_STATIC = [
|
||||
{ id: "gateway", x: 0.5, y: 0.15 },
|
||||
{ id: "sidecar", x: 0.2, y: 0.3 },
|
||||
{ id: "observer", x: 0.8, y: 0.3 },
|
||||
{ id: "mcp", x: 0.85, y: 0.1 },
|
||||
{ id: "context7", x: 0.15, y: 0.1 },
|
||||
{ id: "journal", x: 0.35, y: 0.55 },
|
||||
{ id: "vectord", x: 0.5, y: 0.5 },
|
||||
{ id: "playbook", x: 0.65, y: 0.55 },
|
||||
{ id: "agent", x: 0.5, y: 0.75 },
|
||||
{ id: "usage", x: 0.2, y: 0.75 },
|
||||
];
|
||||
const EDGES = [
|
||||
["gateway","sidecar"],["gateway","observer"],["gateway","mcp"],["gateway","context7"],
|
||||
["gateway","journal"],["gateway","vectord"],["gateway","playbook"],["gateway","agent"],["gateway","usage"],
|
||||
["vectord","playbook"],["agent","vectord"],["observer","playbook"],["sidecar","vectord"],
|
||||
];
|
||||
|
||||
function drawMap(svc) {
|
||||
const svg = d3.select("#map");
|
||||
const box = svg.node().getBoundingClientRect();
|
||||
const W = box.width, H = box.height;
|
||||
svg.selectAll("*").remove();
|
||||
const statusMap = {};
|
||||
[...svc.nodes, ...svc.subsystems].forEach(n => statusMap[n.id] = n);
|
||||
svg.selectAll(".edge").data(EDGES).enter().append("line")
|
||||
.attr("class", d => "edge" + (overlayEdgeActive(d) ? " active" : ""))
|
||||
.attr("x1", d => nodePos(d[0]).x * W).attr("y1", d => nodePos(d[0]).y * H)
|
||||
.attr("x2", d => nodePos(d[1]).x * W).attr("y2", d => nodePos(d[1]).y * H);
|
||||
const g = svg.selectAll(".node").data(NODES_STATIC).enter().append("g")
|
||||
.attr("class", "node")
|
||||
.attr("transform", d => `translate(${d.x * W}, ${d.y * H})`)
|
||||
.on("click", (_ev, d) => { state.selected = { type:"node", id:d.id }; renderContext(); drawMap(svc); });
|
||||
g.append("circle")
|
||||
.attr("class", d => "node-circle" + (state.selected?.type==="node" && state.selected.id===d.id ? " node-selected" : ""))
|
||||
.attr("r", d => nodeRadius(d, statusMap))
|
||||
.attr("fill", d => nodeColor(d, statusMap));
|
||||
// SVG tooltip — hover a node, browser shows a native tooltip with
|
||||
// what this node DOES, not just its name.
|
||||
g.append("title").text(d => nodeTooltip(d.id));
|
||||
g.append("text").attr("class","node-label").attr("y", -30).text(d => nodeLabel(d.id));
|
||||
g.append("text").attr("class","node-sub").attr("y", 40).text(d => nodeSub(d, statusMap));
|
||||
}
|
||||
function nodePos(id) { return NODES_STATIC.find(x => x.id === id) ?? { x:0, y:0 }; }
|
||||
function nodeLabel(id) {
|
||||
return ({gateway:"GATEWAY",sidecar:"SIDECAR",observer:"OBSERVER",mcp:"MCP",context7:"CTX7",
|
||||
journal:"JOURNAL",vectord:"VECTORD",playbook:"PLAYBOOK",agent:"AUTOTUNE",usage:"USAGE"})[id] ?? id;
|
||||
}
|
||||
function nodeRadius(d, m) {
|
||||
const n = m[d.id];
|
||||
if (state.overlay === "activity") {
|
||||
if (d.id === "journal" && n?.stats?.total_events_created != null) return 14 + Math.min(20, Math.log2(n.stats.total_events_created + 1) * 2);
|
||||
if (d.id === "vectord" && n?.stats?.count != null) return 14 + Math.min(20, Math.log2(n.stats.count + 1) * 2);
|
||||
if (d.id === "playbook" && n?.stats?.total != null) return 14 + Math.min(20, Math.log2(n.stats.total + 1));
|
||||
if (d.id === "observer" && n?.stats?.total != null) return 14 + Math.min(20, Math.log2(n.stats.total + 1));
|
||||
if (d.id === "usage" && n?.stats?.requests != null) return 14 + Math.min(20, Math.log2(n.stats.requests + 1) * 2);
|
||||
}
|
||||
return 18;
|
||||
}
|
||||
function nodeColor(d, m) {
|
||||
const n = m[d.id];
|
||||
const ov = state.overlay;
|
||||
if (ov === "status" || ov === "activity") {
|
||||
const st = n?.status ?? (n?.stats ? "healthy" : "unknown");
|
||||
return { healthy:"#3eed86", degraded:"#ffbf3c", down:"#ff4d6e", unknown:"#525c6f" }[st] ?? "#525c6f";
|
||||
}
|
||||
if (ov === "confidence") {
|
||||
const c = recentAvgConfidence(d.id);
|
||||
if (c == null) return "#525c6f";
|
||||
if (c >= 88) return "#3eed86";
|
||||
if (c >= 70) return "#55c5ff";
|
||||
if (c >= 50) return "#ffbf3c";
|
||||
return "#ff4d6e";
|
||||
}
|
||||
if (ov === "gradient") {
|
||||
const t = recentGradientTier(d.id);
|
||||
return t ? ({auto:"#3eed86",dry_run:"#55c5ff",simulation:"#ffbf3c",block:"#ff4d6e"}[t] ?? "#525c6f") : "#525c6f";
|
||||
}
|
||||
if (ov === "verdict") {
|
||||
const v = recentVerdict(d.id);
|
||||
return {pass:"#3eed86",needs_patch:"#ff9f43",fail:"#ff4d6e"}[v] ?? "#525c6f";
|
||||
}
|
||||
return "#55c5ff";
|
||||
}
|
||||
function nodeSub(d, m) {
|
||||
const n = m[d.id];
|
||||
if (!n) return "…";
|
||||
if (d.id === "journal" && n.stats) return `${n.stats.total_events_created ?? 0} events · ${n.stats.persisted_files ?? 0} parquet`;
|
||||
if (d.id === "usage" && n.stats) return `${n.stats.requests ?? 0} requests · ${Math.round((n.stats.total_tokens ?? 0)/1000)}k tokens`;
|
||||
if (d.id === "vectord" && typeof n.stats === "object" && n.stats) return `${n.stats.count ?? 0} indexes`;
|
||||
if (d.id === "playbook" && n.stats) return `${n.stats.active ?? 0} active · ${n.stats.retired ?? 0} retired`;
|
||||
if (d.id === "agent" && n.stats) return `${n.stats.trials_run ?? 0} trials · ${n.stats.promotions ?? 0} promotions`;
|
||||
if (d.id === "observer" && n.stats) return `${n.stats.total ?? 0} observed ops`;
|
||||
return String(n.status ?? "");
|
||||
}
|
||||
|
||||
// Describes what each node DOES — shown as SVG <title> tooltip.
|
||||
function nodeTooltip(id) {
|
||||
return ({
|
||||
gateway: "GATEWAY — Rust/Axum HTTP on :3100. Every external call enters here: /v1/chat, /ingest, /query, /tools, /journal, /vectors. Also hosts gRPC on :3101.",
|
||||
sidecar: "SIDECAR — Python FastAPI on :3200. Adapter from Rust to local Ollama (:11434). Handles /embed /generate /rerank. Stateless.",
|
||||
observer: "OBSERVER — Bun on :3800. Ring buffer of recent ops across the system. Feeds analyzeErrors + PLAYBOOK_BUILDER loops. Scrum events now land here (P45 fix).",
|
||||
mcp: "MCP — Bun on :3700. Model Context Protocol tool gateway. Agent-facing tool endpoints.",
|
||||
context7: "CONTEXT7 — Bun on :3900. Doc-drift resolver — checks playbook doc_refs against current docs for version drift (Phase 45 target).",
|
||||
journal: "JOURNAL — ADR-012 append-only mutation log inside the gateway. Every ingest/delta-write/tombstone should record here. Currently ~1 real event (P9-001 still mostly unwired).",
|
||||
vectord: "VECTORD — Embeddings store + HNSW index + autotune harness. The 'indexes' count = named vector indexes live right now (one per source × model_version).",
|
||||
playbook: "PLAYBOOK — Meta-index. Each entry = a successful past pattern + geo/role + 768d embedding. Active entries boost future vector-search results (Phase 19).",
|
||||
agent: "AUTOTUNE — Background agent that continuously proposes HNSW config trials, picks Pareto winners above min_recall, promotes, and rolls back. Self-tuning vector index.",
|
||||
usage: "USAGE — /v1/chat token counters. Tracks requests, prompt/completion tokens, per-provider breakdown. Grows with scrum + audit traffic.",
|
||||
})[id] ?? id;
|
||||
}
|
||||
function overlayEdgeActive(edge) {
|
||||
if (!state.reviews.length) return false;
|
||||
const latest = state.reviews[state.reviews.length - 1];
|
||||
if (!latest?.reviewed_at) return false;
|
||||
const age = Date.now() - new Date(latest.reviewed_at).getTime();
|
||||
if (age > 60000) return false;
|
||||
return edge.includes("gateway") && (edge.includes("observer") || edge.includes("vectord"));
|
||||
}
|
||||
function matchesNode(r, id) {
|
||||
if (!r?.file) return false;
|
||||
const f = r.file.toLowerCase();
|
||||
if (id === "gateway") return f.includes("/gateway/");
|
||||
if (id === "vectord") return f.includes("/vectord");
|
||||
if (id === "journal") return f.includes("/journald");
|
||||
if (id === "playbook")return f.includes("playbook_memory");
|
||||
if (id === "sidecar") return f.includes("sidecar");
|
||||
if (id === "agent") return f.includes("agent.rs") || f.includes("autotune");
|
||||
return false;
|
||||
}
|
||||
function recentAvgConfidence(id) {
|
||||
const rs = state.reviews.filter(r => matchesNode(r, id));
|
||||
const vs = rs.map(r => r.confidence_avg).filter(v => v != null);
|
||||
return vs.length ? vs.reduce((a,b)=>a+b,0)/vs.length : null;
|
||||
}
|
||||
function recentGradientTier(id) {
|
||||
const rs = state.reviews.filter(r => matchesNode(r, id));
|
||||
const ts = rs.map(r => r.gradient_tier).filter(Boolean);
|
||||
return ts[ts.length - 1] ?? null;
|
||||
}
|
||||
function recentVerdict(id) {
|
||||
const rs = state.reviews.filter(r => matchesNode(r, id));
|
||||
const vs = rs.map(r => r.verdict).filter(Boolean);
|
||||
return vs[vs.length - 1] ?? null;
|
||||
}
|
||||
|
||||
// ───── CONTEXT ─────
|
||||
function renderContext() {
|
||||
const target = document.getElementById("ctx-target");
|
||||
const body = document.getElementById("ctx-body");
|
||||
clear(body);
|
||||
if (!state.selected) {
|
||||
target.textContent = "no selection";
|
||||
body.append(el("div", { className: "ctx-hint", text: "Click a node or a file in KB to inspect. Context persists across view switches." }));
|
||||
body.append(el("div", { className: "ctx-section-hd", text: "System totals" }));
|
||||
appendSummaryKV(body);
|
||||
return;
|
||||
}
|
||||
if (state.selected.type === "node") renderNodeContext(state.selected.id, target, body);
|
||||
else if (state.selected.type === "file") renderFileContext(state.selected.id, target, body);
|
||||
}
|
||||
|
||||
function appendSummaryKV(body) {
|
||||
const s = state.services;
|
||||
if (!s) { body.append(el("div", { className: "ctx-hint", text: "loading…" })); return; }
|
||||
const get = id => s.nodes.concat(s.subsystems).find(n => n.id === id);
|
||||
const journal = get("journal")?.stats ?? {};
|
||||
const usage = get("usage")?.stats ?? {};
|
||||
const playbook = get("playbook")?.stats ?? {};
|
||||
const agent = get("agent")?.stats ?? {};
|
||||
const observer = get("observer")?.stats ?? {};
|
||||
body.append(row("scrum reviews", state.reviews.length));
|
||||
body.append(row("journal events", journal.total_events_created ?? 0));
|
||||
body.append(row("usage tokens", (usage.total_tokens ?? 0).toLocaleString()));
|
||||
body.append(row("playbook active", playbook.active ?? 0));
|
||||
body.append(row("autotune trials", agent.trials_run ?? 0));
|
||||
body.append(row("observer ops", observer.total ?? 0));
|
||||
body.append(row("findings (h/m/l)", `${countFindingsSev("high")}/${countFindingsSev("medium")}/${countFindingsSev("low")}`));
|
||||
}
|
||||
|
||||
function countFindingsSev(sev) {
|
||||
let n = 0;
|
||||
for (const row of state.findings) for (const f of row.findings ?? []) if (f.severity === sev) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
function renderNodeContext(id, target, body) {
|
||||
target.textContent = `NODE · ${id.toUpperCase()}`;
|
||||
const n = [...state.services.nodes, ...state.services.subsystems].find(x => x.id === id);
|
||||
if (n?.health) {
|
||||
body.append(el("div", { className: "ctx-section-hd", text: "Health" }));
|
||||
// Fix 2026-04-24: some /health endpoints return a plain string like
|
||||
// "lakehouse ok". Don't Object.entries() on strings — that iterates
|
||||
// characters. Detect primitive vs object explicitly.
|
||||
if (typeof n.health === "string" || typeof n.health === "number" || typeof n.health === "boolean") {
|
||||
body.append(row("response", String(n.health).slice(0, 80)));
|
||||
} else if (typeof n.health === "object" && n.health !== null) {
|
||||
Object.entries(n.health).slice(0, 8).forEach(([k,v]) => body.append(row(k, short(v))));
|
||||
}
|
||||
}
|
||||
if (n?.stats) {
|
||||
body.append(el("div", { className: "ctx-section-hd", text: "Stats" }));
|
||||
if (typeof n.stats === "string") {
|
||||
body.append(row("raw", String(n.stats).slice(0, 80)));
|
||||
} else if (typeof n.stats === "object" && n.stats !== null) {
|
||||
Object.entries(n.stats).slice(0, 10).forEach(([k,v]) => body.append(row(k, short(v))));
|
||||
}
|
||||
}
|
||||
const related = state.reviews.filter(r => matchesNode(r, id)).slice(-5).reverse();
|
||||
if (related.length) {
|
||||
body.append(el("div", { className: "ctx-section-hd", text: "Recent reviews" }));
|
||||
related.forEach(r => {
|
||||
const rr = row(r.file.split("/").pop(), `${r.confidence_avg ?? "-"}% · ${r.alignment_score ?? "?"}/10`);
|
||||
rr.style.cursor = "pointer";
|
||||
rr.addEventListener("click", () => { state.selected = { type:"file", id:r.file }; renderContext(); });
|
||||
body.append(rr);
|
||||
});
|
||||
}
|
||||
if (!body.firstChild) body.append(el("div", { className: "ctx-hint", text: "no data yet" }));
|
||||
}
|
||||
|
||||
function renderFileContext(fpath, target, body) {
|
||||
target.textContent = fpath.split("/").slice(-3).join("/");
|
||||
const fileReviews = state.reviews.filter(r => r.file === fpath).slice(-6);
|
||||
if (!fileReviews.length) {
|
||||
body.append(el("div", { className: "ctx-hint", text: `no reviews for ${fpath}` }));
|
||||
return;
|
||||
}
|
||||
const latest = fileReviews[fileReviews.length - 1];
|
||||
const pillRow = el("div", { style: { paddingBottom: "6px" } });
|
||||
if (latest.gradient_tier) pillRow.append(el("span", { className: `pill tier-${latest.gradient_tier}`, text: latest.gradient_tier }));
|
||||
if (latest.verdict) pillRow.append(el("span", { className: `pill ver-${latest.verdict}`, text: latest.verdict }));
|
||||
if (latest.output_format) pillRow.append(el("span", { className: `pill fmt-${latest.output_format}`, text: latest.output_format }));
|
||||
body.append(pillRow);
|
||||
const rows = [
|
||||
["file", fpath],
|
||||
["score", latest.alignment_score != null ? `${latest.alignment_score}/10` : "-"],
|
||||
["conf avg", latest.confidence_avg != null ? `${latest.confidence_avg}%` : "-"],
|
||||
["conf min", latest.confidence_min != null ? `${latest.confidence_min}%` : "-"],
|
||||
["findings", latest.findings_count ?? 0],
|
||||
["critical", latest.critical_failures_count ?? 0],
|
||||
["verified", latest.verified_components_count ?? 0],
|
||||
["missing", latest.missing_components_count ?? 0],
|
||||
["model", latest.accepted_model ?? "-"],
|
||||
["attempts", latest.attempts_made ?? 1],
|
||||
["tree split", latest.tree_split_fired ? "yes" : "no"],
|
||||
];
|
||||
rows.forEach(([k,v]) => body.append(row(k, short(v))));
|
||||
body.append(el("div", { className: "ctx-section-hd", text: "Score history" }));
|
||||
fileReviews.forEach(r => body.append(row(new Date(r.reviewed_at).toLocaleTimeString(), `${r.alignment_score ?? "?"}/10 · ${r.confidence_avg ?? "-"}%`)));
|
||||
body.append(el("div", { className: "ctx-section-hd", text: "Preview" }));
|
||||
const pre = el("pre", { text: latest.suggestions_preview ?? "", style: { whiteSpace: "pre-wrap", fontFamily: "var(--mono)", fontSize: "10px", color: "var(--fg-dim)", maxHeight: "200px", overflowY: "auto" } });
|
||||
body.append(pre);
|
||||
document.getElementById("stream-file").textContent = fpath.split("/").pop();
|
||||
}
|
||||
|
||||
// ───── TRACE ─────
|
||||
async function drawTrace() {
|
||||
const fpath = state.selected?.type === "file" ? state.selected.id : state.reviews[state.reviews.length-1]?.file;
|
||||
const tl = document.getElementById("trace-timeline");
|
||||
const detail = document.getElementById("trace-detail");
|
||||
clear(tl); clear(detail);
|
||||
document.getElementById("trace-file").textContent = fpath ?? "—";
|
||||
if (!fpath) { tl.append(el("div", { className: "ctx-hint", text: "no file selected — pick one in KB view" })); return; }
|
||||
const r = await fetch(`/data/file/${encodeURIComponent(fpath)}`).then(r => r.json());
|
||||
const history = r.history ?? [];
|
||||
document.getElementById("trace-runs").textContent = `${history.length} runs`;
|
||||
history.forEach((h, i) => {
|
||||
const node = el("div", { className: "trace-node" + (i === history.length - 1 ? " active" : "") });
|
||||
node.append(el("div", { className: "tn-run", text: h.run_id }));
|
||||
node.append(el("div", { className: "tn-score", text: h.score != null ? String(h.score) : "?" }));
|
||||
node.append(el("div", { className: "tn-conf", text: `conf ${h.conf_avg ?? "-"}% · ${h.findings}f` }));
|
||||
node.append(el("div", { className: "tn-model", text: (h.model ?? "").split("/").pop() }));
|
||||
node.addEventListener("click", () => {
|
||||
tl.querySelectorAll(".trace-node").forEach(x => x.classList.remove("active"));
|
||||
node.classList.add("active");
|
||||
clear(detail);
|
||||
detail.append(el("pre", { text: h.preview ?? "" }));
|
||||
});
|
||||
tl.append(node);
|
||||
});
|
||||
if (history.length) { clear(detail); detail.append(el("pre", { text: history[history.length-1].preview ?? "" })); }
|
||||
}
|
||||
|
||||
// ───── TRAJECTORY — refactor signals + reverse index + per-file delta ─────
|
||||
|
||||
let trajectorySearchTimer = null;
|
||||
document.getElementById("traj-search")?.addEventListener("input", (e) => {
|
||||
const q = e.target.value.trim();
|
||||
clearTimeout(trajectorySearchTimer);
|
||||
trajectorySearchTimer = setTimeout(() => runReverseIndex(q), 300);
|
||||
});
|
||||
|
||||
async function runReverseIndex(query) {
|
||||
const body = document.getElementById("traj-body");
|
||||
if (!query) { drawTrajectory(); return; }
|
||||
clear(body);
|
||||
const res = await fetch(`/data/search?q=${encodeURIComponent(query)}`).then(r => r.json());
|
||||
const hdr = el("div", { className: "traj-section-head", text: `REVERSE INDEX · "${query}" · ${res.hits?.length ?? 0} hits` });
|
||||
body.append(hdr);
|
||||
(res.hits ?? []).forEach(h => {
|
||||
const card = el("div", { className: "traj-hit" });
|
||||
card.append(el("div", { className: "traj-hit-top" },
|
||||
el("span", { className: "traj-hit-file", text: h.file }),
|
||||
el("span", { className: "traj-hit-meta", text: `${h.run_id} · ${(h.model ?? "").split("/").pop()}` })
|
||||
));
|
||||
card.append(el("div", { className: "traj-hit-snip", text: h.snippet }));
|
||||
card.addEventListener("click", () => {
|
||||
state.selected = { type: "file", id: `/home/profit/lakehouse/${h.file}` };
|
||||
renderContext();
|
||||
document.querySelector('#views button[data-view="trace"]').click();
|
||||
});
|
||||
body.append(card);
|
||||
});
|
||||
}
|
||||
|
||||
async function drawTrajectory() {
|
||||
const body = document.getElementById("traj-body");
|
||||
clear(body);
|
||||
const statsEl = document.getElementById("traj-stats");
|
||||
clear(statsEl);
|
||||
|
||||
// SECTION 0 — signal classes (CONVERGING/LOOPING/ORBITING/PLATEAU/MIXED)
|
||||
try {
|
||||
const sc = await fetch("/data/signal_classes").then(r => r.json());
|
||||
body.append(el("div", { className: "traj-section-head", text: "SIGNAL CLASSES · iter-to-iter behavior per file" }));
|
||||
body.append(el("div", { className: "traj-section-explain", text:
|
||||
"Each file compared iter-to-iter: CONVERGING = fix landed (resolved > novel + score↑), " +
|
||||
"LOOPING = same findings repeating (deadlock candidate for hyper-focus), " +
|
||||
"ORBITING = novel findings every iter (healthy depth-first), " +
|
||||
"PLATEAU = score+findings flat (diminishing returns, needs different angle), " +
|
||||
"MIXED = partial movement, NEW = only 1 iter so far."
|
||||
}));
|
||||
const classRow = el("div", { className: "signal-class-row" });
|
||||
for (const [cls, n] of Object.entries(sc.counts ?? {})) {
|
||||
const chip = el("span", { className: `signal-chip signal-${cls.toLowerCase()}`, text: `${cls} ${n}` });
|
||||
classRow.append(chip);
|
||||
}
|
||||
body.append(classRow);
|
||||
const grid = el("div", { className: "signal-grid" });
|
||||
const sorted = Object.entries(sc.classes ?? {}).sort((a, b) => {
|
||||
const order = { CONVERGING: 0, LOOPING: 1, ORBITING: 2, MIXED: 3, PLATEAU: 4, NEW: 5 };
|
||||
return (order[a[1].cls] ?? 9) - (order[b[1].cls] ?? 9);
|
||||
});
|
||||
for (const [file, info] of sorted) {
|
||||
const card = el("div", { className: `signal-card signal-${info.cls.toLowerCase()}` });
|
||||
card.append(el("div", { className: "signal-card-top" },
|
||||
el("span", { className: `signal-chip signal-${info.cls.toLowerCase()}`, text: info.cls }),
|
||||
el("span", { className: "signal-card-file", text: file })
|
||||
));
|
||||
const body2 = el("div", { className: "signal-card-body" });
|
||||
if (info.prev_score != null || info.last_score != null) {
|
||||
body2.append(el("div", { text: `score ${info.prev_score ?? "?"} → ${info.last_score ?? "?"} (Δ ${info.delta_score != null ? (info.delta_score > 0 ? "+" : "") + info.delta_score.toFixed(1) : "?"})` }));
|
||||
}
|
||||
if (info.novel?.length) body2.append(el("div", { className: "signal-novel", text: `NEW: ${info.novel.join(", ")}` }));
|
||||
if (info.resolved?.length) body2.append(el("div", { className: "signal-resolved", text: `RESOLVED: ${info.resolved.join(", ")}` }));
|
||||
if (info.looping?.length) body2.append(el("div", { className: "signal-loop", text: `LOOPING: ${info.looping.join(", ")}` }));
|
||||
card.append(body2);
|
||||
card.addEventListener("click", () => {
|
||||
state.selected = { type: "file", id: `/home/profit/lakehouse/${file}` };
|
||||
renderContext();
|
||||
document.querySelector('#views button[data-view="trace"]').click();
|
||||
});
|
||||
grid.append(card);
|
||||
}
|
||||
body.append(grid);
|
||||
} catch (e) {
|
||||
body.append(el("div", { className: "ctx-hint", text: `signal classes error: ${e}` }));
|
||||
}
|
||||
|
||||
// SECTION 1 — refactor signals
|
||||
const sig = await fetch("/data/refactor_signals").then(r => r.json());
|
||||
const sigs = sig.signals ?? [];
|
||||
const totalHits = sigs.reduce((a,s) => a + s.hits, 0);
|
||||
statsEl.textContent = `${sig.scanned ?? 0} files scanned · ${sigs.length} with refactor hints · ${totalHits} phrase hits total`;
|
||||
|
||||
const sigHead = el("div", { className: "traj-section-head", text: "REFACTOR SIGNALS · files the scrum repeatedly flagged as dead / redundant / stub / needs-rewrite" });
|
||||
body.append(sigHead);
|
||||
|
||||
const explain = el("div", { className: "traj-section-explain", text:
|
||||
"Aggregates across all scrum iterations. A phrase hit = one time the reviewer used language like 'remove', 'duplicate', 'refactor', 'pseudocode', 'orphaned'. " +
|
||||
"Files near the top are the strongest refactor candidates — the scrum keeps calling them out. Click a row to jump to its per-iteration trace."
|
||||
});
|
||||
body.append(explain);
|
||||
|
||||
const table = el("div", { className: "traj-table" });
|
||||
sigs.slice(0, 30).forEach(s => {
|
||||
const r = el("div", { className: "traj-row" });
|
||||
r.append(el("div", { className: "traj-col-rank", text: String(sigs.indexOf(s) + 1) }));
|
||||
r.append(el("div", { className: "traj-col-file", text: s.file }));
|
||||
r.append(el("div", { className: "traj-col-hits", text: `${s.hits}×` }));
|
||||
const topPhrases = Object.entries(s.phrases).sort((a,b)=>b[1]-a[1]).slice(0,3)
|
||||
.map(([p,n]) => `${p} (${n})`).join(", ");
|
||||
r.append(el("div", { className: "traj-col-phrases", text: topPhrases }));
|
||||
r.append(el("div", { className: "traj-col-iters", text: `${s.iterations} iter` }));
|
||||
r.addEventListener("click", () => {
|
||||
state.selected = { type: "file", id: `/home/profit/lakehouse/${s.file}` };
|
||||
renderContext();
|
||||
document.querySelector('#views button[data-view="trace"]').click();
|
||||
});
|
||||
table.append(r);
|
||||
});
|
||||
body.append(table);
|
||||
|
||||
// SECTION 2 — per-file trajectory: pick the top-5 refactor candidates and
|
||||
// show their score/conf delta across iterations inline.
|
||||
if (sigs.length) {
|
||||
body.append(el("div", { className: "traj-section-head", text: "SCORE TRAJECTORY — top refactor candidates" }));
|
||||
const grid = el("div", { className: "traj-spark-grid" });
|
||||
for (const s of sigs.slice(0, 6)) {
|
||||
const card = el("div", { className: "traj-spark" });
|
||||
card.append(el("div", { className: "traj-spark-file", text: s.file }));
|
||||
// pull history
|
||||
const hist = await fetch(`/data/file/${encodeURIComponent("/home/profit/lakehouse/" + s.file)}`)
|
||||
.then(r => r.json()).catch(() => ({ history: [] }));
|
||||
const runs = hist.history ?? [];
|
||||
if (runs.length === 0) { card.append(el("div", { className: "traj-spark-empty", text: "no history" })); }
|
||||
else {
|
||||
const line = el("div", { className: "traj-spark-line" });
|
||||
runs.forEach((h,i) => {
|
||||
const pt = el("div", { className: "traj-spark-pt" });
|
||||
pt.append(el("div", { className: "traj-pt-score", text: h.score != null ? `${h.score}/10` : "?" }));
|
||||
pt.append(el("div", { className: "traj-pt-conf", text: `${h.conf_avg ?? "-"}%` }));
|
||||
pt.append(el("div", { className: "traj-pt-label", text: `iter${i+1}` }));
|
||||
line.append(pt);
|
||||
if (i < runs.length - 1) line.append(el("div", { className: "traj-spark-arrow", text: "→" }));
|
||||
});
|
||||
card.append(line);
|
||||
// delta summary
|
||||
if (runs.length >= 2) {
|
||||
const first = runs[0], last = runs[runs.length - 1];
|
||||
const dScore = (last.score != null && first.score != null) ? (last.score - first.score) : null;
|
||||
const dConf = (last.conf_avg != null && first.conf_avg != null) ? (last.conf_avg - first.conf_avg) : null;
|
||||
const delta = el("div", { className: "traj-spark-delta" });
|
||||
if (dScore != null) delta.append(el("span", { text: `Δscore ${dScore > 0 ? "+" : ""}${dScore.toFixed(1)}`, className: dScore < 0 ? "delta-down" : dScore > 0 ? "delta-up" : "" }));
|
||||
if (dConf != null) delta.append(el("span", { text: ` · Δconf ${dConf > 0 ? "+" : ""}${dConf}%`, className: dConf > 0 ? "delta-up" : dConf < 0 ? "delta-down" : "" }));
|
||||
card.append(delta);
|
||||
}
|
||||
}
|
||||
card.addEventListener("click", () => {
|
||||
state.selected = { type: "file", id: `/home/profit/lakehouse/${s.file}` };
|
||||
renderContext();
|
||||
document.querySelector('#views button[data-view="trace"]').click();
|
||||
});
|
||||
grid.append(card);
|
||||
}
|
||||
body.append(grid);
|
||||
}
|
||||
}
|
||||
|
||||
// ───── METRICS ─────
|
||||
function metricBox(label, big, kind, opts = {}) {
|
||||
// opts: { source, good, explain }
|
||||
// source = where the number comes from (data path)
|
||||
// good = the "what's a healthy value" sentence
|
||||
// explain = one-line definition of what this counts
|
||||
const box = el("div", { className: "metric" + (kind ? " " + kind : "") });
|
||||
box.append(el("div", { className: "m-label", text: label }));
|
||||
box.append(el("div", { className: "m-big", text: big }));
|
||||
if (opts.explain) box.append(el("div", { className: "m-sub m-explain", text: opts.explain }));
|
||||
if (opts.source) box.append(el("div", { className: "m-sub m-source", text: "SOURCE · " + opts.source }));
|
||||
if (opts.good) box.append(el("div", { className: "m-sub m-good", text: "GOOD · " + opts.good }));
|
||||
return box;
|
||||
}
|
||||
function drawMetrics() {
|
||||
const grid = document.getElementById("metric-grid");
|
||||
clear(grid);
|
||||
// Kick off pathway fetch in parallel; render when it resolves so the
|
||||
// rest of the metrics grid appears immediately. The cards append to
|
||||
// the grid after the synchronous block below — they'll show up at
|
||||
// the bottom of the grid within a tick of first render.
|
||||
fetch("/data/pathway_stats").then(r => r.ok ? r.json() : null).then(j => {
|
||||
if (!j || !j.stats) return;
|
||||
const s = j.stats;
|
||||
const w = j.scrum_window ?? {};
|
||||
// Activity metric — is the hot-swap firing at all?
|
||||
grid.append(metricBox("pathway reuse rate", `${Math.round((w.pathway_reuse_rate ?? 0) * 100)}%`,
|
||||
(w.pathway_reuse_rate ?? 0) > 0.1 ? "good" : (w.pathway_reuse_rate ?? 0) > 0 ? "warn" : "bad", {
|
||||
explain: "% of recent reviews where a pathway hot-swap fired (narrow fingerprint match + 0.80 success rate + ≥3 replays + audit_consensus pass + 0.90 similarity).",
|
||||
source: `scrum_reviews.jsonl .pathway_hot_swap_hit over last ${w.reviews ?? 0} reviews (${w.hot_swap_hits ?? 0} hits)`,
|
||||
good: "≥10% sustained = index earning its keep. <10% over many iters = fingerprint too narrow or probation too strict. 0% on fresh install is expected (no replays yet).",
|
||||
}));
|
||||
// Value metric — how much compute did hot-swap actually save?
|
||||
const saved = w.avg_rungs_saved_per_commit ?? 0;
|
||||
grid.append(metricBox("avg rungs saved", saved.toFixed(2),
|
||||
saved >= 1 ? "good" : saved > 0 ? "warn" : "bad", {
|
||||
explain: "Average ladder rungs skipped per committed review by hot-swap. Rungs_saved = recommended_rung - 1 when the recommended model succeeded (otherwise 0).",
|
||||
source: "scrum_reviews.jsonl .rungs_saved averaged",
|
||||
good: "Every 1.0 here ≈ one less model call per review. At 21 files/iter, 1.0 saved = 21 cloud calls avoided. Value only counts when the replay actually succeeded.",
|
||||
}));
|
||||
// Stability metric — retired pathways indicate the learning loop is correcting itself.
|
||||
grid.append(metricBox("pathways tracked", String(s.total_pathways),
|
||||
s.total_pathways > 0 ? "good" : "warn", {
|
||||
explain: `Total pathway traces stored. ${s.retired} retired (below 0.80 success after ≥3 replays). ${s.with_audit_pass} audit-passed, eligible for hot-swap probation.`,
|
||||
source: "/vectors/pathway/stats",
|
||||
good: `Grows monotonically with scrum runs. Retired=${s.retired} is HEALTHY — it means the learning loop is pruning pathways that stopped working. replay_success_rate=${(s.replay_success_rate*100).toFixed(0)}% aggregates all historical replays.`,
|
||||
}));
|
||||
}).catch(() => {});
|
||||
const byTier = { auto:0, dry_run:0, simulation:0, block:0, unknown:0 };
|
||||
state.reviews.forEach(r => { const t = r.gradient_tier ?? "unknown"; if (byTier[t] != null) byTier[t]++; });
|
||||
const total = state.reviews.length || 1;
|
||||
const confRows = state.reviews.filter(r => r.confidence_avg != null);
|
||||
const avg = confRows.length ? Math.round(confRows.reduce((a,r)=>a+r.confidence_avg,0)/confRows.length) : 0;
|
||||
const verdictCount = { pass:0, needs_patch:0, fail:0, unknown:0 };
|
||||
state.reviews.forEach(r => { const v=r.verdict??"unknown"; if(verdictCount[v]!=null) verdictCount[v]++; });
|
||||
const findingsTotal = state.reviews.reduce((a,r)=>a+(r.findings_count??0),0);
|
||||
const critTotal = state.reviews.reduce((a,r)=>a+(r.critical_failures_count??0),0);
|
||||
const verTotal = state.reviews.reduce((a,r)=>a+(r.verified_components_count??0),0);
|
||||
const usage = state.services?.subsystems?.find(n=>n.id==="usage")?.stats ?? {};
|
||||
const journal = state.services?.subsystems?.find(n=>n.id==="journal")?.stats ?? {};
|
||||
|
||||
grid.append(metricBox("avg confidence", `${avg}%`, avg>=85?"good":avg>=70?"warn":"bad", {
|
||||
explain: "Self-assessed probability per suggestion, averaged across every review.",
|
||||
source: "scrum_reviews.jsonl .confidence_avg",
|
||||
good: "≥85% — model is confident. 70-84% routine. <70% means the scrum is uncertain and findings need human review.",
|
||||
}));
|
||||
grid.append(metricBox("scrum reviews", String(state.reviews.length), "good", {
|
||||
explain: "Every source file reviewed by the scrum master, across all iterations.",
|
||||
source: `${state.metrics.length} scrum runs tracked in scrum_loop_metrics.jsonl`,
|
||||
good: "Grows every run — 21 files × N iterations. Stall = pipeline broken.",
|
||||
}));
|
||||
grid.append(metricBox("critical failures", String(critTotal), critTotal>50?"bad":critTotal>10?"warn":"good", {
|
||||
explain: "Hard FAILs flagged by the forensic reviewer — pseudocode, fake implementations, unwired invariants. Each one is a concrete code-level gap.",
|
||||
source: "scrum_reviews.jsonl .critical_failures_count (forensic JSON format only)",
|
||||
good: "Trending DOWN each iteration = fixes are landing. Rising = new gaps surfacing faster than we close them.",
|
||||
}));
|
||||
grid.append(metricBox("verified components", String(verTotal), verTotal>0?"good":"warn", {
|
||||
explain: "What the scrum CONFIRMED is working — with file/line evidence. The inverse of critical_failures.",
|
||||
source: "scrum_reviews.jsonl .verified_components_count",
|
||||
good: "Trending UP = the system has more provably-real parts over time. Should grow as fixes land.",
|
||||
}));
|
||||
grid.append(metricBox("findings captured", String(findingsTotal), "good", {
|
||||
explain: "Total individual suggestions the scrum produced across all reviews (tables + JSON).",
|
||||
source: "scrum_reviews.jsonl .findings_count summed",
|
||||
good: "Higher = more scrutiny per file. Per-file average ≥10 means the review is substantive.",
|
||||
}));
|
||||
grid.append(metricBox("journal events", String(journal.total_events_created ?? 0), "good", {
|
||||
explain: "Mutation events recorded via ADR-012 append-only journal. Every ingest/delta-write should emit one.",
|
||||
source: "/journal/stats → total_events_created",
|
||||
good: "Should grow with ingest traffic. 1 = only a test probe fired; internal callers still unwired on most paths (P9-001).",
|
||||
}));
|
||||
grid.append(metricBox("v1 requests", String(usage.requests ?? 0), "good", {
|
||||
explain: "Calls through the Universal API /v1/chat endpoint (Phase 38). Captures all scrum + audit traffic.",
|
||||
source: `/v1/usage → requests. ${(usage.total_tokens ?? 0).toLocaleString()} tokens total`,
|
||||
good: "Every iteration adds ~21 requests. Stall = scrum paused OR callers bypassing the gateway (P44-style bypass).",
|
||||
}));
|
||||
|
||||
// gradient bar
|
||||
const gb = el("div", { className: "metric" });
|
||||
gb.append(el("div", { className: "m-label", text: "permission gradient" }));
|
||||
gb.append(el("div", { className: "m-big", text: String(state.reviews.length) }));
|
||||
gb.append(el("div", { className: "m-sub m-explain", text: "Tiers the scrum's suggestions by confidence: how much auto-apply we can trust per file." }));
|
||||
const bar = el("div", { className: "bar" });
|
||||
bar.append(el("span", { className: "seg-auto", style: { width: `${100*byTier.auto/total}%` } }));
|
||||
bar.append(el("span", { className: "seg-dry_run", style: { width: `${100*byTier.dry_run/total}%` } }));
|
||||
bar.append(el("span", { className: "seg-simulation", style: { width: `${100*byTier.simulation/total}%` } }));
|
||||
bar.append(el("span", { className: "seg-block", style: { width: `${100*byTier.block/total}%` } }));
|
||||
gb.append(bar);
|
||||
gb.append(el("div", { className: "m-sub", text: `auto ${byTier.auto} · dry_run ${byTier.dry_run} · sim ${byTier.simulation} · block ${byTier.block}` }));
|
||||
gb.append(el("div", { className: "m-sub m-good", text:
|
||||
"AUTO (≥90%): ship the suggestion. DRY_RUN (70-89): apply then diff. SIMULATION (50-69): test first. BLOCK (<50): human review — the model doesn't trust itself."
|
||||
}));
|
||||
grid.append(gb);
|
||||
|
||||
const vb = el("div", { className: "metric" });
|
||||
vb.append(el("div", { className: "m-label", text: "verdict distribution" }));
|
||||
vb.append(el("div", { className: "m-big", text: String(verdictCount.pass + verdictCount.needs_patch + verdictCount.fail) }));
|
||||
vb.append(el("div", { className: "m-sub m-explain", text: "Forensic audit verdict per file: pass = works, needs_patch = fixable gaps, fail = not trustable." }));
|
||||
vb.append(el("div", { className: "m-sub", text: `pass ${verdictCount.pass} · needs_patch ${verdictCount.needs_patch} · fail ${verdictCount.fail}` }));
|
||||
vb.append(el("div", { className: "m-sub m-source", text: "SOURCE · scrum_reviews.jsonl .verdict (forensic JSON only — markdown rows count as unknown)" }));
|
||||
grid.append(vb);
|
||||
}
|
||||
|
||||
// ───── KB ─────
|
||||
function drawKB() {
|
||||
const grid = document.getElementById("kb-grid");
|
||||
clear(grid);
|
||||
|
||||
// Explanatory banner — each iteration the scrum re-reviews every
|
||||
// target file and writes a row here. A card = one file's latest
|
||||
// review. Click to drill into its trace across all iterations.
|
||||
const banner = el("div", { className: "kb-banner" });
|
||||
banner.append(el("div", { className: "kb-banner-title", text: "KNOWLEDGE BASE — every source file reviewed by the scrum master" }));
|
||||
banner.append(el("div", { className: "kb-banner-body", text:
|
||||
"Each card below is the LATEST scrum review of one source file. The review itself lives in data/_kb/scrum_reviews.jsonl. " +
|
||||
"Fields: score (scrum's alignment rating, 1-10 vs PRD intent), conf (model's self-assessed confidence per suggestion, avg'd), " +
|
||||
"findings (# of suggestions), crit (critical_failures — hard FAILs found), verified (verified_components — what's confirmed working). " +
|
||||
"Pills show: permission gradient (can we trust auto-apply), verdict (pass/needs_patch/fail), output format (JSON = forensic, markdown = legacy). " +
|
||||
"Click a card to see its trace across all iterations (iter 1 → iter N) and watch scores trend."
|
||||
}));
|
||||
grid.append(banner);
|
||||
|
||||
const byFile = new Map();
|
||||
state.reviews.forEach(r => { if (r.file) byFile.set(r.file, r); });
|
||||
const rows = [...byFile.values()].sort((a,b) => (b.confidence_avg??0) - (a.confidence_avg??0));
|
||||
|
||||
// Quick stats above the cards
|
||||
const statLine = el("div", { className: "kb-statline" });
|
||||
const avgConf = rows.length ? Math.round(rows.reduce((a,r)=>a+(r.confidence_avg??0),0) / rows.length) : 0;
|
||||
const scoreMean = rows.filter(r=>r.alignment_score!=null);
|
||||
const avgScore = scoreMean.length ? (scoreMean.reduce((a,r)=>a+r.alignment_score,0) / scoreMean.length).toFixed(1) : "?";
|
||||
const blockCount = rows.filter(r => r.gradient_tier === "block").length;
|
||||
statLine.append(el("span", { text: `${rows.length} files tracked` }));
|
||||
statLine.append(el("span", { text: `mean score ${avgScore}/10` }));
|
||||
statLine.append(el("span", { text: `mean confidence ${avgConf}%` }));
|
||||
statLine.append(el("span", { text: `${blockCount} blocked (need human review)`, className: blockCount > 0 ? "stat-warn" : "" }));
|
||||
grid.append(statLine);
|
||||
|
||||
rows.forEach(r => {
|
||||
const card = el("div", { className: "kb-file", data: { file: r.file } });
|
||||
card.append(el("div", { className: "kf-path", text: r.file }));
|
||||
const meta = el("div", { className: "kf-meta" });
|
||||
const scoreSpan = el("span", { className: "kf-score", text: `${r.alignment_score ?? "?"}/10` });
|
||||
scoreSpan.title = "Scrum's alignment score (1-10) — how well this file matches PRD intent. Lower = more gaps.";
|
||||
meta.append(scoreSpan);
|
||||
const confSpan = el("span", { text: `conf ${r.confidence_avg ?? "-"}%` });
|
||||
confSpan.title = "Average self-confidence across suggestions. <70% = model uncertain, treat carefully.";
|
||||
meta.append(confSpan);
|
||||
const findingsSpan = el("span", { text: `${r.findings_count ?? 0} findings` });
|
||||
findingsSpan.title = "Total suggestions in this review (table rows or JSON array entries).";
|
||||
meta.append(findingsSpan);
|
||||
const critSpan = el("span", { text: `${r.critical_failures_count ?? 0} crit` });
|
||||
critSpan.title = "Critical failures: pseudocode, fake implementations, unwired invariants. Hard FAILs.";
|
||||
if ((r.critical_failures_count ?? 0) > 0) critSpan.style.color = "var(--red)";
|
||||
meta.append(critSpan);
|
||||
const verSpan = el("span", { text: `${r.verified_components_count ?? 0} verified` });
|
||||
verSpan.title = "Verified components: things the scrum CONFIRMED work, with file/line evidence.";
|
||||
if ((r.verified_components_count ?? 0) > 0) verSpan.style.color = "var(--green)";
|
||||
meta.append(verSpan);
|
||||
meta.append(el("span", { text: (r.accepted_model ?? "").split("/").pop(), attrs: { title: "Which model produced this review" } }));
|
||||
card.append(meta);
|
||||
const pills = el("div", { className: "kf-meta" });
|
||||
if (r.gradient_tier) {
|
||||
const p = el("span", { className: `pill tier-${r.gradient_tier}`, text: r.gradient_tier });
|
||||
p.title = ({
|
||||
auto: "AUTO — confidence ≥90%, suggestions safe to apply automatically",
|
||||
dry_run: "DRY_RUN — confidence 70-89%, apply then review the diff",
|
||||
simulation: "SIMULATION — confidence 50-69%, test in sandbox first",
|
||||
block: "BLOCK — confidence <50%, requires human review, do not auto-apply",
|
||||
})[r.gradient_tier] ?? r.gradient_tier;
|
||||
pills.append(p);
|
||||
}
|
||||
if (r.verdict) {
|
||||
const p = el("span", { className: `pill ver-${r.verdict}`, text: r.verdict });
|
||||
p.title = ({
|
||||
pass: "PASS — scrum confirms this file meets its PRD intent",
|
||||
needs_patch: "NEEDS_PATCH — gaps exist but are fixable; scrum has concrete suggestions",
|
||||
fail: "FAIL — file cannot be trusted for its claimed purpose without structural changes",
|
||||
})[r.verdict] ?? r.verdict;
|
||||
pills.append(p);
|
||||
}
|
||||
if (r.output_format) {
|
||||
const p = el("span", { className: `pill fmt-${r.output_format}`, text: r.output_format });
|
||||
p.title = r.output_format === "forensic_json"
|
||||
? "FORENSIC_JSON — structured output with verdict/critical/verified/missing fields. Richer signal."
|
||||
: "MARKDOWN — legacy tabular output. Lower structure; we only extract confidence scalars from these.";
|
||||
pills.append(p);
|
||||
}
|
||||
card.append(pills);
|
||||
card.addEventListener("click", () => {
|
||||
state.selected = { type: "file", id: r.file };
|
||||
renderContext();
|
||||
document.querySelector('#views button[data-view="trace"]').click();
|
||||
});
|
||||
grid.append(card);
|
||||
});
|
||||
}
|
||||
|
||||
// ───── CONSOLE ─────
|
||||
// Persistent selection across polls so tab switches survive.
|
||||
state.consoleSvc = "gateway";
|
||||
|
||||
// Hook tab buttons once
|
||||
document.querySelectorAll("#con-tabs button").forEach(b => {
|
||||
b.addEventListener("click", () => {
|
||||
document.querySelectorAll("#con-tabs button").forEach(x => x.classList.remove("on"));
|
||||
b.classList.add("on");
|
||||
state.consoleSvc = b.dataset.svc;
|
||||
drawConsole();
|
||||
});
|
||||
});
|
||||
|
||||
async function drawConsole() {
|
||||
const log = document.getElementById("console-log");
|
||||
clear(log);
|
||||
const unit = document.getElementById("con-unit");
|
||||
if (unit) unit.textContent = "";
|
||||
|
||||
if (state.consoleSvc === "summary") {
|
||||
drawConsoleSummary(log);
|
||||
return;
|
||||
}
|
||||
|
||||
// Per-service log tail
|
||||
const svc = state.consoleSvc;
|
||||
try {
|
||||
const res = await fetch(`/data/logs/${svc}?n=120`).then(r => r.json());
|
||||
if (unit && res.unit) unit.textContent = `unit · ${res.unit}`;
|
||||
if (res.error) {
|
||||
log.append(lineInfo(`[error] ${res.error}`, "cl-err"));
|
||||
return;
|
||||
}
|
||||
const lines = res.lines ?? [];
|
||||
if (!lines.length) { log.append(lineInfo("(no log lines — unit may have just started)", "cl-info")); return; }
|
||||
lines.forEach(l => {
|
||||
const cls = /\berror\b|\bERROR\b|panic|\[ERROR|failed/.test(l) ? "cl-err"
|
||||
: /\bwarn\b|\bWARN\b|\bwarning\b|\[WARN/.test(l) ? "cl-warn"
|
||||
: /\baccepted\b|\bok\b|\bOK\b|success|complete|ready/.test(l) ? "cl-ok"
|
||||
: "cl-info";
|
||||
log.append(lineInfo(l, cls));
|
||||
});
|
||||
// autoscroll to bottom
|
||||
log.scrollTop = log.scrollHeight;
|
||||
} catch (e) {
|
||||
log.append(lineInfo(`[fetch-error] ${e}`, "cl-err"));
|
||||
}
|
||||
}
|
||||
|
||||
function lineInfo(text, cls) {
|
||||
return el("div", { className: "cl-line " + cls, text });
|
||||
}
|
||||
|
||||
function drawConsoleSummary(log) {
|
||||
const info = t => lineInfo(t, "cl-info");
|
||||
const ok = t => lineInfo(t, "cl-ok");
|
||||
const warn = t => lineInfo(t, "cl-warn");
|
||||
const err = t => lineInfo(t, "cl-err");
|
||||
log.append(info(`# Lakehouse VCP · ${new Date().toLocaleTimeString()}`));
|
||||
log.append(info(`# Services`));
|
||||
for (const n of state.services?.nodes ?? []) {
|
||||
const line = `[${String(n.status).padEnd(8)}] ${n.label}`;
|
||||
log.append(n.status === "healthy" ? ok(line) : n.status === "down" ? err(line) : warn(line));
|
||||
}
|
||||
log.append(info(`# Subsystems`));
|
||||
for (const s of state.services?.subsystems ?? []) {
|
||||
log.append(info(` ${String(s.id).padEnd(10)} ${JSON.stringify(s.stats ?? {}).slice(0, 120)}`));
|
||||
}
|
||||
log.append(info(`# Recent overrides (layer 10)`));
|
||||
for (const o of state.overrides.slice(-6)) {
|
||||
log.append(warn(` [${o.ts}] ${o.task_signature}: ${o.human_fix}`));
|
||||
}
|
||||
log.append(info(`# Model trust accumulated`));
|
||||
const agg = {};
|
||||
for (const t of state.trust) {
|
||||
const k = t.accepted_model ?? "?";
|
||||
agg[k] = agg[k] ?? { accepts:0, thin:0, attempts:0 };
|
||||
agg[k].accepts++;
|
||||
agg[k].thin += t.thin_rejections ?? 0;
|
||||
agg[k].attempts += t.attempts_made ?? 0;
|
||||
}
|
||||
for (const [m, s] of Object.entries(agg)) {
|
||||
log.append(info(` ${String(m).padEnd(48)} accepts=${s.accepts} thin=${s.thin} attempts=${s.attempts}`));
|
||||
}
|
||||
}
|
||||
|
||||
// ───── boot ─────
|
||||
poll();
|
||||
setInterval(poll, POLL_MS);
|
||||
window.addEventListener("resize", () => { if (state.services && state.view === "map") drawMap(state.services); });
|
||||
Reference in New Issue
Block a user