Some checks failed
lakehouse/auditor 1 blocking issue: cloud: claim not backed — "journal event verified live (total_events_created 0→1 after probe)."
## Infrastructure (scrum loop hardening)
crates/gateway/src/v1/openrouter.rs — new OpenRouter provider
Direct HTTPS to openrouter.ai/api/v1/chat/completions with OpenAI-compatible shape.
Key resolution: OPENROUTER_API_KEY env → /home/profit/.env → /root/llm_team_config.json
(shares LLM Team UI's quota). Added after iter 5 hit repeated Ollama Cloud 502s on
kimi-k2:1t — different provider backbone as rescue rung. Unit tests pin the URL
stripping and OpenAI wire shape.
crates/gateway/src/v1/mod.rs + main.rs
Added `"openrouter" | "openrouter_free"` arm to /v1/chat dispatch.
V1State.openrouter_key loaded at startup via openrouter::resolve_openrouter_key()
mirroring the Ollama Cloud pattern. Startup log:
"v1: OpenRouter key loaded — /v1/chat provider=openrouter enabled"
tests/real-world/scrum_master_pipeline.ts
* 9-rung ladder — kimi-k2:1t → qwen3-coder:480b → deepseek-v3.1:671b →
mistral-large-3:675b → gpt-oss:120b → qwen3.5:397b → openrouter/gpt-oss-120b:free
→ openrouter/gemma-3-27b-it:free → local qwen3.5:latest.
Added qwen3-coder:480b as rung 2 after live probes confirmed it rescues
kimi-k2:1t 502s cleanly (0.9s latency, substantive reviews).
Dropped devstral-2 (displaced by qwen3-coder); dropped kimi-k2.6 (not available);
dropped minimax-m2.7 (returned 0 chars / 400 thinking tokens).
Local fallback promoted qwen3.5:latest per J's direction 2026-04-24.
* MAX_ATTEMPTS bumped 6 → 9 to accommodate the rescue tier.
* Tree-split scratchpad fixed — was concatenating shard markers directly
into the reviewer input, causing kimi-k2:1t to write titles like
"Forensic Audit Report – file.rs (shard 3)". Now uses internal §N§
markers during accumulation and runs a proper reduce step that
collapses per-shard digests into ONE coherent file-level synthesis
with markers stripped. Matches the Phase 21 aibridge::tree_split
map→reduce design. Fallback to stripped scratchpad if reducer returns thin.
tests/real-world/scrum_applier.ts — NEW (737 lines)
The auto-apply pipeline. Reads scrum_reviews.jsonl, filters rows where
gradient_tier ∈ {auto, dry_run} AND confidence_avg ≥ MIN_CONF (default 90),
asks the reviewer model for concrete old_string/new_string patch JSON,
applies via text replacement, runs cargo check after each file, commits
if green and reverts if red. Deny-list: /etc/, config/, ops/, auditor/,
docs/, data/, mcp-server/, ui/, sidecar/, scripts/. Hard caps: per-patch
confidence ≥ MIN_CONF, old_string must be exactly unique, max 20 lines per
patch. Never runs on main without explicit LH_APPLIER_BRANCH override.
Audit trail in data/_kb/auto_apply.jsonl.
Empirical behavior (dry-run over iter 4 reviews):
5 eligible files → 1 green commit-ready, 2 build-red reverts, 2 all-rejected
The build-green gate caught 2 bad patches before they'd have merged.
mcp-server/observer.ts — LLM Team code_review escalation
When a sig_hash accumulates ≥3 failures (ESCALATION_THRESHOLD), fire-and-forget
POST /api/run?mode=code_review at localhost:5000 with the failure cluster context.
Parses facts/entities/relationships/file_hints from the response. Writes to a
new data/_kb/observer_escalations.jsonl surface. Answers J's vision of the
observer triggering richer LLM Team calls when failures pile up.
Non-blocking: runs parallel to existing qwen2.5 analyzer, never replaces it.
Tracks escalated sig_hashes in a session-local Set to avoid re-hammering
LLM Team when a cluster persists across observer cycles.
crates/aibridge/src/context.rs
First auto-applied patch produced by scrum_applier.ts (dry-run path —
applier writes files in dry-run mode but doesn't commit; bug noted for
iter 6 fix). Adds #[deprecated] annotation to the inline estimate_tokens
helper pointing callers to the centralized shared::model_matrix::ModelMatrix
entry point (P21-002 — duplicate token-estimator surfaces). Cargo check
passes with the annotation (verified by applier's own build gate).
## Visual Control Plane (UI)
ui/server.ts — Bun.serve on :3950 with /data/* fan-out:
/data/services, /data/reviews, /data/metrics, /data/trust, /data/overrides,
/data/findings, /data/outcomes, /data/audit_facts, /data/file/:path,
/data/refactor_signals, /data/search?q=, /data/signal_classes,
/data/logs/:svc (journalctl tail per systemd unit), /data/scrum_log.
Bug fix: tryFetch always attempts JSON.parse before falling back to text
— observer's Bun.serve returns JSON without application/json content-type,
which was displaying stats as a raw string ("0 ops" on map) before.
ui/index.html + ui.css — dark neo-brutalist shell. 6 views:
MAP (D3 force-graph + overlays) / TRACE (per-file iter history) /
TRAJECTORY (signal-class cards + refactor-signals table + reverse-index
search box) / METRICS (every card has SOURCE + GOOD lines explaining
where the number comes from and what target trajectory means) /
KB (card grid with tooltips on every field) / CONSOLE (per-service
journalctl tabs).
ui/ui.js — polling client, D3 wiring, signal-class panel, refactor-signals
table, reverse-index search, per-service console tabs. Bug fix:
renderNodeContext had Object.entries() iterating string characters when
/health returned a plain string — now guards with typeof check so
"lakehouse ok" renders as one row instead of "0 l / 1 a / 2 k / ...".
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
853 lines
44 KiB
JavaScript
853 lines
44 KiB
JavaScript
// 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);
|
||
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); });
|