progmune-runtime 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp.json +11 -0
- package/.progmune_allowlist +50 -0
- package/.test_report/test_report.md +87 -0
- package/Dockerfile +2 -10
- package/FAQ.md +167 -0
- package/demo-project/auth.ts +55 -0
- package/demo-project/tsconfig.json +8 -0
- package/dist/ab-stats.js +11 -0
- package/dist/acl-breakdown.js +13 -0
- package/dist/all-sessions.js +11 -0
- package/dist/antibody-stats.js +11 -0
- package/dist/audit.js +218 -0
- package/dist/benchmark-count.js +7 -0
- package/dist/benchmark-full.js +17 -0
- package/dist/benchmark-pass-rate.js +54 -0
- package/dist/benchmark-report.js +67 -0
- package/dist/benchmark-save.js +62 -0
- package/dist/benchmark-status.js +15 -0
- package/dist/branch-ledger.js +365 -0
- package/dist/branch-tree-count.js +14 -0
- package/dist/check.js +506 -0
- package/dist/common-fixpath.js +12 -0
- package/dist/constraint-types.js +12 -0
- package/dist/deterministic-replay.js +277 -0
- package/dist/emitter.js +112 -12
- package/dist/exec-metrics.js +11 -0
- package/dist/execute.js +242 -0
- package/dist/extract-ir.js +550 -5
- package/dist/failure-collector.js +143 -0
- package/dist/failure-corpus.js +481 -35
- package/dist/failure-report.js +11 -0
- package/dist/failures.js +11 -0
- package/dist/fast-path-hits.js +13 -0
- package/dist/feedback.js +6 -3
- package/dist/file-lock.js +82 -0
- package/dist/find-session.js +10 -0
- package/dist/fingerprint-list.js +15 -0
- package/dist/gen-history-log.js +13 -0
- package/dist/generate.js +2 -1
- package/dist/generate_500.js +4 -2
- package/dist/genome.js +11 -0
- package/dist/heatmap-data.js +11 -0
- package/dist/heatmap.js +11 -0
- package/dist/immune-reporter.js +137 -0
- package/dist/learned.js +11 -0
- package/dist/ledger-registry.js +241 -0
- package/dist/llm.js +43 -2
- package/dist/load-benchmarks.js +47 -0
- package/dist/main.js +2 -1
- package/dist/mcp-server.mjs +446 -34
- package/dist/memory-layer.js +51 -13
- package/dist/metrics.js +11 -0
- package/dist/obs-web.js +561 -0
- package/dist/p0_ssg_demo.js +255 -40
- package/dist/planner.js +933 -65
- package/dist/protocol-registry.js +105 -0
- package/dist/recent-session.js +12 -0
- package/dist/repair-proposal.js +353 -0
- package/dist/report.js +32 -0
- package/dist/runtime-invariants.js +161 -0
- package/dist/runtime-types.js +117 -0
- package/dist/search-planner.js +38 -9
- package/dist/semantic-snapshot.js +155 -0
- package/dist/semantic-trace.js +1497 -0
- package/dist/semantic-validator.js +3 -2
- package/dist/semantic_guard_test.js +2 -1
- package/dist/sessions.js +11 -0
- package/dist/ssg-validator.js +658 -20
- package/dist/svl-distribution.js +11 -0
- package/dist/terminal-status.js +11 -0
- package/dist/test_failure_corpus.js +3 -1
- package/dist/token-savings.js +11 -0
- package/dist/total-repairs.js +12 -0
- package/dist/unresolved-count.js +12 -0
- package/dist/valid-fingerprints.js +13 -0
- package/dist/validator.js +116 -60
- package/dist/verify-fps.js +11 -0
- package/dist/verify-ledgers.js +11 -0
- package/docs/whitepaper-style.css +77 -0
- package/docs/whitepaper-v2.1.md +609 -0
- package/docs/whitepaper-v2.2.md +1064 -0
- package/docs/whitepaper-v2.2.pdf +0 -0
- package/fly.toml +1 -1
- package/package.json +13 -4
- package/protocols.json +136 -0
- package/public/dashboard.html +119 -0
- package/readme.md +829 -0
- package/server/hub.js +84 -12
- package/test/replay-golden/sess_1780063202050_mgeld.json +9 -0
- package/test/replay-golden/sess_1780064032560_gocld.json +354 -0
- package/test/replay-golden/sess_1780064413331_s2709.json +606 -0
- package/test/replay-golden/sess_1780064792710_y3avo.json +614 -0
- package/test/replay-golden.ts +84 -0
- package/test_benchmark.js +165 -0
- package/test_comprehensive.mjs +638 -0
- package/test_concurrency.js +129 -0
- package/test_ir_robustness.js +85 -0
- package/test_semantic_contracts.js +269 -0
- package/test_ssg_stress.js +156 -0
- package/test_svl3.js +58 -0
- package/tsconfig.json +1 -1
- package/.env.example +0 -3
- package/.progmune_memory/fingerprints.json +0 -7
- package/.progmune_memory/opt_in.json +0 -4
- package/README.md +0 -118
- package/dist/mcp-server.js +0 -55
- package/immune_hub_data/2026-05-14.json +0 -50
package/dist/obs-web.js
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Semantic Observatory — Web Dashboard
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* ts-node src/obs-web.ts → start server on default port 3100
|
|
7
|
+
* ts-node src/obs-web.ts --port 8080 → custom port
|
|
8
|
+
* PROGMUNE_OBS_PORT=4000 ts-node ... → env var override
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
const http = __importStar(require("http"));
|
|
45
|
+
const failure_corpus_1 = require("./failure-corpus");
|
|
46
|
+
const ssg_validator_1 = require("./ssg-validator");
|
|
47
|
+
const ledger_registry_1 = require("./ledger-registry");
|
|
48
|
+
const deterministic_replay_1 = require("./deterministic-replay");
|
|
49
|
+
const branch_ledger_1 = require("./branch-ledger");
|
|
50
|
+
const protocol_registry_1 = require("./protocol-registry");
|
|
51
|
+
const PORT = parseInt(process.env.PROGMUNE_OBS_PORT || process.argv[3] || "3100", 10);
|
|
52
|
+
// ── JSON API handlers ──
|
|
53
|
+
function jsonReply(res, data, code = 200) {
|
|
54
|
+
res.writeHead(code, {
|
|
55
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
56
|
+
"Access-Control-Allow-Origin": "*",
|
|
57
|
+
});
|
|
58
|
+
res.end(JSON.stringify(data));
|
|
59
|
+
}
|
|
60
|
+
const apiRoutes = {
|
|
61
|
+
"/api/sessions": () => (0, failure_corpus_1.getAllSessions)(),
|
|
62
|
+
"/api/genome": () => (0, failure_corpus_1.getFailureGenome)(),
|
|
63
|
+
"/api/heatmap": () => (0, failure_corpus_1.getSemanticHeatmap)(),
|
|
64
|
+
"/api/antibodies": () => (0, failure_corpus_1.getAntibodyStats)(),
|
|
65
|
+
"/api/learned": () => (0, failure_corpus_1.getLearnedPatterns)(),
|
|
66
|
+
};
|
|
67
|
+
function handleAPI(req, res) {
|
|
68
|
+
const url = new URL(req.url || "/", `http://localhost:${PORT}`);
|
|
69
|
+
const pathname = url.pathname;
|
|
70
|
+
// Single session lookup: /api/sessions/sess_...
|
|
71
|
+
if (pathname.startsWith("/api/sessions/")) {
|
|
72
|
+
const sessionId = pathname.slice("/api/sessions/".length);
|
|
73
|
+
if (sessionId) {
|
|
74
|
+
const sessions = (0, failure_corpus_1.getAllSessions)();
|
|
75
|
+
const session = sessions.find((s) => s.sessionId === sessionId || s.sessionId.startsWith(sessionId));
|
|
76
|
+
if (session)
|
|
77
|
+
return jsonReply(res, session);
|
|
78
|
+
return jsonReply(res, { error: "not found" }, 404);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Phase 4 Ledger APIs
|
|
82
|
+
if (pathname === "/api/ledger")
|
|
83
|
+
return handleLedgerAPI(res, url);
|
|
84
|
+
if (pathname === "/api/consistency")
|
|
85
|
+
return handleConsistencyAPI(res, url);
|
|
86
|
+
if (pathname === "/api/diff")
|
|
87
|
+
return handleDiffAPI(res, url);
|
|
88
|
+
if (pathname === "/api/replay")
|
|
89
|
+
return handleReplayAPI(res, url);
|
|
90
|
+
if (pathname === "/api/fingerprints")
|
|
91
|
+
return jsonReply(res, (0, ledger_registry_1.getFingerprintRegistry)());
|
|
92
|
+
const handler = apiRoutes[pathname];
|
|
93
|
+
if (handler) {
|
|
94
|
+
try {
|
|
95
|
+
return jsonReply(res, handler(url));
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
return jsonReply(res, { error: e.message }, 500);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return jsonReply(res, { error: "not found" }, 404);
|
|
102
|
+
}
|
|
103
|
+
// ── Phase 4 Ledger API handlers ──
|
|
104
|
+
function getSessionTransitions(session) {
|
|
105
|
+
if (session.branchTree && session.branchTree.length > 0) {
|
|
106
|
+
const map = (0, branch_ledger_1.buildBranchMap)(session.branchTree);
|
|
107
|
+
const root = (0, branch_ledger_1.findRootBranch)(session.branchTree);
|
|
108
|
+
if (root)
|
|
109
|
+
return (0, branch_ledger_1.flattenBranch)(root, map);
|
|
110
|
+
}
|
|
111
|
+
let tx = [];
|
|
112
|
+
for (const a of (session.attempts || [])) {
|
|
113
|
+
tx = tx.concat(a.transitions || []);
|
|
114
|
+
}
|
|
115
|
+
return tx;
|
|
116
|
+
}
|
|
117
|
+
function handleLedgerAPI(res, url) {
|
|
118
|
+
const sid = url.searchParams.get("sessionId");
|
|
119
|
+
if (!sid)
|
|
120
|
+
return jsonReply(res, { error: "missing sessionId" }, 400);
|
|
121
|
+
const sessions = (0, failure_corpus_1.getAllSessions)();
|
|
122
|
+
const s = sessions.find(x => x.sessionId === sid || x.sessionId.startsWith(sid));
|
|
123
|
+
if (!s)
|
|
124
|
+
return jsonReply(res, { error: "session not found" }, 404);
|
|
125
|
+
const tx = getSessionTransitions(s);
|
|
126
|
+
const consistency = (0, ssg_validator_1.checkLedgerConsistency)(tx, (0, protocol_registry_1.getNsInit)());
|
|
127
|
+
return jsonReply(res, {
|
|
128
|
+
sessionId: s.sessionId, intent: s.intent,
|
|
129
|
+
transitionCount: tx.length, hash: (0, ssg_validator_1.hashLedger)(tx),
|
|
130
|
+
consistency: { consistent: consistency.consistent, violationCount: consistency.violations.length },
|
|
131
|
+
transitions: tx,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function handleConsistencyAPI(res, url) {
|
|
135
|
+
const sid = url.searchParams.get("sessionId");
|
|
136
|
+
if (!sid)
|
|
137
|
+
return jsonReply(res, { error: "missing sessionId" }, 400);
|
|
138
|
+
const sessions = (0, failure_corpus_1.getAllSessions)();
|
|
139
|
+
const s = sessions.find(x => x.sessionId === sid || x.sessionId.startsWith(sid));
|
|
140
|
+
if (!s)
|
|
141
|
+
return jsonReply(res, { error: "session not found" }, 404);
|
|
142
|
+
return jsonReply(res, (0, ssg_validator_1.checkLedgerConsistency)(getSessionTransitions(s), (0, protocol_registry_1.getNsInit)()));
|
|
143
|
+
}
|
|
144
|
+
function handleDiffAPI(res, url) {
|
|
145
|
+
const aId = url.searchParams.get("sessionA"), bId = url.searchParams.get("sessionB");
|
|
146
|
+
if (!aId || !bId)
|
|
147
|
+
return jsonReply(res, { error: "missing sessionA or sessionB" }, 400);
|
|
148
|
+
const sessions = (0, failure_corpus_1.getAllSessions)();
|
|
149
|
+
const sA = sessions.find(x => x.sessionId === aId || x.sessionId.startsWith(aId));
|
|
150
|
+
const sB = sessions.find(x => x.sessionId === bId || x.sessionId.startsWith(bId));
|
|
151
|
+
if (!sA || !sB)
|
|
152
|
+
return jsonReply(res, { error: "session not found" }, 404);
|
|
153
|
+
const diff = (0, ssg_validator_1.diffLedgers)(getSessionTransitions(sA), getSessionTransitions(sB));
|
|
154
|
+
return jsonReply(res, { sessionA: { id: sA.sessionId, intent: sA.intent }, sessionB: { id: sB.sessionId, intent: sB.intent }, diff });
|
|
155
|
+
}
|
|
156
|
+
function handleReplayAPI(res, url) {
|
|
157
|
+
const sid = url.searchParams.get("sessionId");
|
|
158
|
+
if (!sid)
|
|
159
|
+
return jsonReply(res, { error: "missing sessionId" }, 400);
|
|
160
|
+
return jsonReply(res, (0, deterministic_replay_1.replaySession)(sid));
|
|
161
|
+
}
|
|
162
|
+
// ── Dashboard HTML ──
|
|
163
|
+
const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
164
|
+
<html lang="zh-CN">
|
|
165
|
+
<head>
|
|
166
|
+
<meta charset="utf-8">
|
|
167
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
168
|
+
<title>Progmune Observatory</title>
|
|
169
|
+
<style>
|
|
170
|
+
:root { --bg:#0d1117; --surface:#161b22; --border:#30363d; --text:#c9d1d9; --dim:#8b949e;
|
|
171
|
+
--green:#3fb950; --red:#f85149; --yellow:#d2991d; --cyan:#58a6ff; --purple:#bc8cff; }
|
|
172
|
+
* { box-sizing:border-box; margin:0; padding:0; }
|
|
173
|
+
body { font:14px/1.5 -apple-system,BlinkMacSystemFont,sans-serif; background:var(--bg); color:var(--text); padding:24px; }
|
|
174
|
+
h1 { font-size:20px; margin-bottom:20px; }
|
|
175
|
+
h2 { font-size:16px; margin:24px 0 12px; color:var(--cyan); }
|
|
176
|
+
.tab-bar { display:flex; gap:4px; margin-bottom:20px; flex-wrap:wrap; }
|
|
177
|
+
.tab { padding:6px 16px; border:1px solid var(--border); border-radius:6px; cursor:pointer; background:var(--surface); color:var(--dim); font-size:13px; }
|
|
178
|
+
.tab:hover { color:var(--text); border-color:var(--dim); }
|
|
179
|
+
.tab.active { color:var(--cyan); border-color:var(--cyan); }
|
|
180
|
+
.panel { display:none; }
|
|
181
|
+
.panel.active { display:block; }
|
|
182
|
+
.card { background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:16px; margin-bottom:12px; }
|
|
183
|
+
.flex { display:flex; gap:16px; flex-wrap:wrap; }
|
|
184
|
+
.stat { flex:1; min-width:140px; }
|
|
185
|
+
.stat .value { font-size:28px; font-weight:700; }
|
|
186
|
+
.stat .label { font-size:12px; color:var(--dim); }
|
|
187
|
+
table { width:100%; border-collapse:collapse; font-size:13px; }
|
|
188
|
+
th, td { text-align:left; padding:6px 10px; border-bottom:1px solid var(--border); }
|
|
189
|
+
th { color:var(--dim); font-weight:500; }
|
|
190
|
+
.badge { display:inline-block; padding:1px 8px; border-radius:10px; font-size:11px; font-weight:600; }
|
|
191
|
+
.badge-green { background:#1a4020; color:var(--green); }
|
|
192
|
+
.badge-red { background:#3d1f1f; color:var(--red); }
|
|
193
|
+
.badge-yellow { background:#3d351a; color:var(--yellow); }
|
|
194
|
+
.bar-wrap { background:var(--border); border-radius:4px; height:18px; overflow:hidden; min-width:60px; }
|
|
195
|
+
.bar-fill { height:100%; border-radius:4px; transition:width .4s; }
|
|
196
|
+
.loading { color:var(--dim); padding:40px; text-align:center; }
|
|
197
|
+
pre { font-size:12px; background:var(--bg); padding:12px; border-radius:6px; overflow-x:auto; max-height:400px; }
|
|
198
|
+
.timeline-step { padding:8px 0 8px 20px; border-left:2px solid var(--border); margin-left:8px; }
|
|
199
|
+
.timeline-step.ok { border-color:var(--green); }
|
|
200
|
+
.timeline-step.fail { border-color:var(--red); }
|
|
201
|
+
.fn { color:var(--cyan); font-family:monospace; }
|
|
202
|
+
.state-delta { font-size:12px; color:var(--dim); }
|
|
203
|
+
.state-add { color:var(--green); }
|
|
204
|
+
.state-rem { color:var(--red); }
|
|
205
|
+
</style>
|
|
206
|
+
</head>
|
|
207
|
+
<body>
|
|
208
|
+
<h1>Progmune Semantic Observatory</h1>
|
|
209
|
+
|
|
210
|
+
<div class="tab-bar">
|
|
211
|
+
<button class="tab active" data-panel="overview">Overview</button>
|
|
212
|
+
<button class="tab" data-panel="sessions">Sessions</button>
|
|
213
|
+
<button class="tab" data-panel="genome">Genome</button>
|
|
214
|
+
<button class="tab" data-panel="heatmap">Heatmap</button>
|
|
215
|
+
<button class="tab" data-panel="antibodies">Antibodies</button>
|
|
216
|
+
<button class="tab" data-panel="ledger">Ledger</button>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div id="overview" class="panel active"><div class="loading">Loading...</div></div>
|
|
220
|
+
<div id="sessions" class="panel"><div class="loading">Loading...</div></div>
|
|
221
|
+
<div id="genome" class="panel"><div class="loading">Loading...</div></div>
|
|
222
|
+
<div id="heatmap" class="panel"><div class="loading">Loading...</div></div>
|
|
223
|
+
<div id="antibodies" class="panel"><div class="loading">Loading...</div></div>
|
|
224
|
+
<div id="ledger" class="panel"><div class="loading">Loading...</div></div>
|
|
225
|
+
|
|
226
|
+
<script>
|
|
227
|
+
async function fetchJSON(u) {
|
|
228
|
+
const r = await fetch(u);
|
|
229
|
+
return r.json();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function loadOverview() {
|
|
233
|
+
const [sessions, antibodies] = await Promise.all([
|
|
234
|
+
fetchJSON('/api/sessions'),
|
|
235
|
+
fetchJSON('/api/antibodies'),
|
|
236
|
+
]);
|
|
237
|
+
const resolved = sessions.filter(s => s.resolved).length;
|
|
238
|
+
const totalAttempts = sessions.reduce((n,s) => n + s.attempts.length, 0);
|
|
239
|
+
const totalViolations = sessions.reduce((n,s) => n + s.attempts.reduce((m,a) => m + a.violations.length, 0), 0);
|
|
240
|
+
|
|
241
|
+
let html = '<div class="flex">' +
|
|
242
|
+
'<div class="card stat"><div class="value" style="color:var(--cyan)">' + sessions.length + '</div><div class="label">Total Sessions</div></div>' +
|
|
243
|
+
'<div class="card stat"><div class="value" style="color:var(--green)">' + resolved + '</div><div class="label">Resolved</div></div>' +
|
|
244
|
+
'<div class="card stat"><div class="value" style="color:var(--yellow)">' + totalAttempts + '</div><div class="label">Attempts</div></div>' +
|
|
245
|
+
'<div class="card stat"><div class="value" style="color:var(--red)">' + totalViolations + '</div><div class="label">Violations</div></div>' +
|
|
246
|
+
'<div class="card stat"><div class="value" style="color:var(--purple)">' + antibodies.totalLLMCallsSaved + '</div><div class="label">LLM Calls Saved</div></div>' +
|
|
247
|
+
'</div>';
|
|
248
|
+
|
|
249
|
+
if (sessions.length > 0) {
|
|
250
|
+
html += '<h2>Recent Sessions</h2>';
|
|
251
|
+
for (const s of sessions.slice(-5).reverse()) {
|
|
252
|
+
const failed = s.attempts.filter(a => a.outcome !== "success").length;
|
|
253
|
+
const icon = s.resolved ? '<span class="badge badge-green">resolved</span>' : '<span class="badge badge-red">unresolved</span>';
|
|
254
|
+
html += '<div class="card"><strong>' + esc(s.intent) + '</strong> ' + icon +
|
|
255
|
+
' <span style="color:var(--dim)">' + failed + ' retries</span>' +
|
|
256
|
+
' <span style="float:right;color:var(--dim);font-size:12px">' + s.sessionId.slice(0,13) + '...</span></div>';
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
document.getElementById('overview').innerHTML = html;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function loadSessions() {
|
|
263
|
+
const sessions = await fetchJSON('/api/sessions');
|
|
264
|
+
let html = '<div class="card"><table><tr><th>Session</th><th>Intent</th><th>Attempts</th><th>Violations</th><th>Status</th></tr>';
|
|
265
|
+
for (const s of sessions.reverse()) {
|
|
266
|
+
const failed = s.attempts.filter(a => a.outcome !== "success").length;
|
|
267
|
+
const viols = s.attempts.reduce((n,a) => n + a.violations.length, 0);
|
|
268
|
+
const icon = s.resolved ? '<span class="badge badge-green">resolved</span>' : '<span class="badge badge-red">failed</span>';
|
|
269
|
+
const abHit = s.attempts.some(a => a.antibodyHit) ? ' <span class="badge badge-yellow">Ab</span>' : '';
|
|
270
|
+
html += '<tr><td style="font-family:monospace;font-size:11px;color:var(--dim)">' + s.sessionId.slice(0,13) + '...</td>' +
|
|
271
|
+
'<td><a href="#" onclick="showSession(\'' + s.sessionId + '\');return false" style="color:var(--cyan)">' + esc(s.intent.slice(0,50)) + '</a></td>' +
|
|
272
|
+
'<td>' + s.attempts.length + '</td><td>' + viols + '</td><td>' + icon + abHit + '</td></tr>';
|
|
273
|
+
}
|
|
274
|
+
html += '</table></div>';
|
|
275
|
+
document.getElementById('sessions').innerHTML = html;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function showSession(id) {
|
|
279
|
+
const s = await fetchJSON('/api/sessions/' + id);
|
|
280
|
+
let html = '<div class="card"><h2>' + esc(s.intent) + '</h2>';
|
|
281
|
+
html += '<span style="color:var(--dim)">Session: ' + s.sessionId + ' | ';
|
|
282
|
+
html += 'Resolved: ' + (s.resolved ? '<span class="badge badge-green">yes</span>' : '<span class="badge badge-red">no</span>') + '</span></div>';
|
|
283
|
+
|
|
284
|
+
for (const a of s.attempts) {
|
|
285
|
+
const icon = a.outcome === "success" ? '✅' : '❌';
|
|
286
|
+
const cls = a.outcome === "success" ? 'ok' : 'fail';
|
|
287
|
+
html += '<div class="card"><strong>' + icon + ' Attempt ' + a.attemptNumber + '</strong>';
|
|
288
|
+
html += ' <span style="color:var(--dim)">(' + a.outcome + ')</span>';
|
|
289
|
+
|
|
290
|
+
if (a.antibodyHit) {
|
|
291
|
+
html += ' <span class="badge badge-yellow">Ab: ' + a.antibodyHit.level + ' ' + a.antibodyHit.action + '</span>';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Action sequence
|
|
295
|
+
const fns = (a.generatedActions || []).filter(x => x.kind === "call" && x.function);
|
|
296
|
+
if (fns.length > 0) {
|
|
297
|
+
html += '<div style="margin-top:8px">';
|
|
298
|
+
for (const fn of fns) {
|
|
299
|
+
html += '<span class="fn">' + esc(fn.function) + '()</span> → ';
|
|
300
|
+
}
|
|
301
|
+
html += '</div>';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Transitions
|
|
305
|
+
if (a.transitions && a.transitions.length > 0) {
|
|
306
|
+
html += '<div style="margin-top:8px">';
|
|
307
|
+
for (const t of a.transitions) {
|
|
308
|
+
const gained = (t.acquired||[]).map(s => '<span class="state-add">+' + s + '</span>').join(' ');
|
|
309
|
+
const lost = (t.invalidated||[]).map(s => '<span class="state-rem">-' + s + '</span>').join(' ');
|
|
310
|
+
html += '<div class="timeline-step ' + (t.valid ? 'ok' : 'fail') + '">' +
|
|
311
|
+
'<span class="fn">' + esc(t.function) + '()</span>' +
|
|
312
|
+
' <span class="state-delta">' + gained + ' ' + lost + '</span></div>';
|
|
313
|
+
}
|
|
314
|
+
html += '</div>';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Violations
|
|
318
|
+
for (const v of a.violations) {
|
|
319
|
+
html += '<div style="margin-top:6px;padding:8px;background:var(--bg);border-radius:4px">';
|
|
320
|
+
html += '<span class="badge badge-red">SVL-' + v.svl + '</span> ';
|
|
321
|
+
html += '<span style="color:var(--dim)">' + esc(v.violatedConstraint) + '</span>: ' + esc(v.description.slice(0, 120)) + '';
|
|
322
|
+
if (v.fixPath && v.fixPath.length) {
|
|
323
|
+
html += '<div style="font-size:12px;color:var(--yellow);margin-top:2px">Fix: ' + v.fixPath.join(' → ') + '</div>';
|
|
324
|
+
}
|
|
325
|
+
html += '</div>';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
html += '</div>';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
document.getElementById('sessions').innerHTML = html + '<br><a href="#" onclick="loadSessions();return false" style="color:var(--cyan)">← Back to list</a>';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function loadGenome() {
|
|
335
|
+
const g = await fetchJSON('/api/genome');
|
|
336
|
+
if (g.totalFailures === 0) {
|
|
337
|
+
document.getElementById('genome').innerHTML = '<div class="card">No failures recorded yet.</div>';
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const max = Math.max(g.bySVL['SVL-1']||0, g.bySVL['SVL-2']||0, g.bySVL['SVL-3']||0, g.bySVL['SVL-4']||0) || 1;
|
|
341
|
+
const bar = (n,color) => '<div class="bar-wrap"><div class="bar-fill" style="width:'+(n/max*100)+'%;background:'+color+'"></div></div>';
|
|
342
|
+
|
|
343
|
+
let html = '<div class="flex">' +
|
|
344
|
+
'<div class="card stat"><div class="value" style="color:var(--red)">'+g.totalFailures+'</div><div class="label">Total Failures</div></div>' +
|
|
345
|
+
'<div class="card stat"><div class="value" style="color:var(--yellow)">'+g.averageRetriesToSuccess+'</div><div class="label">Avg Retries</div></div>' +
|
|
346
|
+
'</div>';
|
|
347
|
+
|
|
348
|
+
html += '<h2>By SVL Level</h2><div class="card"><table>';
|
|
349
|
+
for (const [svl, count] of [['SVL-1',g.bySVL['SVL-1']],['SVL-2',g.bySVL['SVL-2']],['SVL-3',g.bySVL['SVL-3']],['SVL-4',g.bySVL['SVL-4']]]) {
|
|
350
|
+
html += '<tr><td>'+svl+'</td><td>'+bar(count, count===max?'var(--red)':'var(--yellow)')+'</td><td>'+count+'</td></tr>';
|
|
351
|
+
}
|
|
352
|
+
html += '</table></div>';
|
|
353
|
+
|
|
354
|
+
if (g.commonFixPaths.length) {
|
|
355
|
+
html += '<h2>Common Repair Paths</h2><div class="card"><table>';
|
|
356
|
+
for (const fp of g.commonFixPaths.slice(0,5)) {
|
|
357
|
+
html += '<tr><td style="color:var(--yellow)">'+fp.fixPath.join(' → ')+'</td><td>'+fp.count+'x</td></tr>';
|
|
358
|
+
}
|
|
359
|
+
html += '</table></div>';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
document.getElementById('genome').innerHTML = html;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function loadHeatmap() {
|
|
366
|
+
const h = await fetchJSON('/api/heatmap');
|
|
367
|
+
let html = '';
|
|
368
|
+
|
|
369
|
+
if (h.svlHotspots && h.svlHotspots.length) {
|
|
370
|
+
const maxC = Math.max(...h.svlHotspots.map(s=>s.count),1);
|
|
371
|
+
html += '<h2>SVL Hotspots</h2><div class="card"><table>';
|
|
372
|
+
for (const s of h.svlHotspots) {
|
|
373
|
+
const pct = s.percentage + '%';
|
|
374
|
+
html += '<tr><td>'+s.svl+'</td><td><div class="bar-wrap"><div class="bar-fill" style="width:'+(s.count/maxC*100)+'%;background:var(--red)"></div></div></td><td>'+s.count+' ('+pct+')</td></tr>';
|
|
375
|
+
}
|
|
376
|
+
html += '</table></div>';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (h.fragileProtocols && h.fragileProtocols.length) {
|
|
380
|
+
html += '<h2>Fragile Protocols</h2><div class="card"><table>';
|
|
381
|
+
for (const fp of h.fragileProtocols.slice(0,8)) {
|
|
382
|
+
html += '<tr><td class="fn">'+esc(fp.function)+'()</td><td>'+fp.violationCount+'x</td><td>'+fp.svl+'</td></tr>';
|
|
383
|
+
}
|
|
384
|
+
html += '</table></div>';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
document.getElementById('heatmap').innerHTML = html || '<div class="card">No heatmap data yet.</div>';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function loadAntibodies() {
|
|
391
|
+
const stats = await fetchJSON('/api/antibodies');
|
|
392
|
+
let html = '<div class="flex">' +
|
|
393
|
+
'<div class="card stat"><div class="value" style="color:var(--green)">'+stats.fastPathHits+'</div><div class="label">Fast-Path Hits</div></div>' +
|
|
394
|
+
'<div class="card stat"><div class="value" style="color:var(--yellow)">'+stats.injectedHintHits+'</div><div class="label">Hint Injections</div></div>' +
|
|
395
|
+
'<div class="card stat"><div class="value" style="color:var(--cyan)">'+stats.totalLLMCallsSaved+'</div><div class="label">LLM Calls Saved</div></div>' +
|
|
396
|
+
'<div class="card stat"><div class="value" style="color:var(--purple)">'+(stats.totalTokensSaved||0).toLocaleString()+'</div><div class="label">Tokens Saved</div></div>' +
|
|
397
|
+
'</div>';
|
|
398
|
+
|
|
399
|
+
if (Object.keys(stats.byLevel).length) {
|
|
400
|
+
html += '<h2>By Level</h2><div class="card"><table>';
|
|
401
|
+
for (const lvl of ['ACL-4','ACL-3','ACL-2','ACL-1']) {
|
|
402
|
+
const d = stats.byLevel[lvl];
|
|
403
|
+
if (!d) continue;
|
|
404
|
+
html += '<tr><td>'+lvl+'</td><td>'+d.hits+' hits</td><td>'+d.llmSaved+' LLM saved</td><td>'+(d.tokensSaved||0).toLocaleString()+' tokens</td></tr>';
|
|
405
|
+
}
|
|
406
|
+
html += '</table></div>';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
document.getElementById('antibodies').innerHTML = html || '<div class="card">No antibody activity yet.</div>';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function loadLedger() {
|
|
413
|
+
const sessions = await fetchJSON('/api/sessions');
|
|
414
|
+
// Session selector
|
|
415
|
+
let html = '<div class="card"><h2>Ledger Timeline</h2>';
|
|
416
|
+
html += '<select id="ledgerSession" style="background:var(--bg);color:var(--text);padding:6px;border:1px solid var(--border);border-radius:4px;margin-bottom:12px;width:100%">';
|
|
417
|
+
html += '<option value="">-- Select session --</option>';
|
|
418
|
+
for (const s of sessions.reverse()) {
|
|
419
|
+
html += '<option value="' + s.sessionId + '">' + s.sessionId.slice(0,13) + '... — ' + esc(s.intent.slice(0,60)) + '</option>';
|
|
420
|
+
}
|
|
421
|
+
html += '</select>';
|
|
422
|
+
html += '<div id="ledgerDetail"></div></div>';
|
|
423
|
+
|
|
424
|
+
// Session Diff
|
|
425
|
+
html += '<div class="card" style="margin-top:12px"><h2>Session Diff</h2>';
|
|
426
|
+
html += '<div style="display:flex;gap:8px">';
|
|
427
|
+
html += '<select id="diffSessionA" style="flex:1;background:var(--bg);color:var(--text);padding:6px;border:1px solid var(--border);border-radius:4px"><option value="">-- Session A --</option>';
|
|
428
|
+
for (const s of sessions) {
|
|
429
|
+
html += '<option value="' + s.sessionId + '">' + s.sessionId.slice(0,13) + '...</option>';
|
|
430
|
+
}
|
|
431
|
+
html += '</select>';
|
|
432
|
+
html += '<select id="diffSessionB" style="flex:1;background:var(--bg);color:var(--text);padding:6px;border:1px solid var(--border);border-radius:4px"><option value="">-- Session B --</option>';
|
|
433
|
+
for (const s of sessions) {
|
|
434
|
+
html += '<option value="' + s.sessionId + '">' + s.sessionId.slice(0,13) + '...</option>';
|
|
435
|
+
}
|
|
436
|
+
html += '</select>';
|
|
437
|
+
html += '<button onclick="showDiff()" style="background:var(--cyan);color:#000;border:none;padding:6px 16px;border-radius:4px;cursor:pointer;font-weight:600">Diff</button>';
|
|
438
|
+
html += '</div>';
|
|
439
|
+
html += '<div id="diffResult" style="margin-top:8px"></div></div>';
|
|
440
|
+
|
|
441
|
+
document.getElementById('ledger').innerHTML = html;
|
|
442
|
+
|
|
443
|
+
// Bind selector
|
|
444
|
+
document.getElementById('ledgerSession').addEventListener('change', function() {
|
|
445
|
+
if (this.value) showTimeline(this.value);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function showTimeline(sessionId) {
|
|
450
|
+
const data = await fetchJSON('/api/ledger?sessionId=' + sessionId);
|
|
451
|
+
const detail = document.getElementById('ledgerDetail');
|
|
452
|
+
|
|
453
|
+
let html = '<div style="color:var(--dim);margin-bottom:8px">' +
|
|
454
|
+
'Transitions: <strong style="color:var(--text)">' + data.transitionCount + '</strong> | ' +
|
|
455
|
+
'Hash: <code style="color:var(--cyan)">' + data.hash + '</code> | ' +
|
|
456
|
+
(data.consistency.consistent
|
|
457
|
+
? '<span class="badge badge-green">Consistent</span>'
|
|
458
|
+
: '<span class="badge badge-red">' + data.consistency.violationCount + ' violations</span>') +
|
|
459
|
+
'</div>';
|
|
460
|
+
|
|
461
|
+
// Timeline table
|
|
462
|
+
html += '<table><tr><th>#</th><th>Function</th><th>Namespace</th><th>Valid</th><th>Acquired</th><th>Invalidated</th></tr>';
|
|
463
|
+
for (const t of data.transitions) {
|
|
464
|
+
const validBadge = t.valid
|
|
465
|
+
? '<span class="badge badge-green">✓</span>'
|
|
466
|
+
: '<span class="badge badge-red">✗</span>';
|
|
467
|
+
const acquired = (t.acquired||[]).map(s => '<span style="color:var(--green)">+' + esc(s) + '</span>').join(' ') || '—';
|
|
468
|
+
const invalidated = (t.invalidated||[]).map(s => '<span style="color:var(--red)">-' + esc(s) + '</span>').join(' ') || '—';
|
|
469
|
+
html += '<tr>' +
|
|
470
|
+
'<td style="color:var(--dim)">' + t.actionIndex + '</td>' +
|
|
471
|
+
'<td><span class="fn">' + esc(t.function) + '()</span></td>' +
|
|
472
|
+
'<td style="color:var(--dim)">' + esc(t.namespace) + '</td>' +
|
|
473
|
+
'<td>' + validBadge + '</td>' +
|
|
474
|
+
'<td style="font-size:12px">' + acquired + '</td>' +
|
|
475
|
+
'<td style="font-size:12px">' + invalidated + '</td>' +
|
|
476
|
+
'</tr>';
|
|
477
|
+
}
|
|
478
|
+
html += '</table>';
|
|
479
|
+
|
|
480
|
+
// States before/after summary for last transition
|
|
481
|
+
if (data.transitions.length > 0) {
|
|
482
|
+
const last = data.transitions[data.transitions.length - 1];
|
|
483
|
+
html += '<div style="margin-top:12px;font-size:12px;color:var(--dim)">';
|
|
484
|
+
html += '<strong>Final State:</strong> ';
|
|
485
|
+
for (const [ns, states] of Object.entries(last.statesAfter || {})) {
|
|
486
|
+
html += '<span style="color:var(--cyan)">' + esc(ns) + '</span>=[<span style="color:var(--text)">' + (states as string[]).join(', ') + '</span>] ';
|
|
487
|
+
}
|
|
488
|
+
html += '</div>';
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
detail.innerHTML = html;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function showDiff() {
|
|
495
|
+
const a = document.getElementById('diffSessionA').value;
|
|
496
|
+
const b = document.getElementById('diffSessionB').value;
|
|
497
|
+
if (!a || !b) return;
|
|
498
|
+
|
|
499
|
+
const data = await fetchJSON('/api/diff?sessionA=' + a + '&sessionB=' + b);
|
|
500
|
+
const d = data.diff;
|
|
501
|
+
let html = '<div style="margin-top:8px">';
|
|
502
|
+
html += '<span style="color:var(--dim)">Unchanged: </span><strong>' + d.unchanged + '</strong> | ';
|
|
503
|
+
html += '<span style="color:var(--yellow)">Only A: </span><strong>' + d.onlyInA + '</strong> | ';
|
|
504
|
+
html += '<span style="color:var(--cyan)">Only B: </span><strong>' + d.onlyInB + '</strong> | ';
|
|
505
|
+
html += '<span style="color:var(--red)">Changed: </span><strong>' + d.changed + '</strong>';
|
|
506
|
+
if (d.identical) html += ' <span class="badge badge-green">Identical</span>';
|
|
507
|
+
html += '</div>';
|
|
508
|
+
document.getElementById('diffResult').innerHTML = html;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function esc(s) {
|
|
512
|
+
if (!s) return '';
|
|
513
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Tab switching
|
|
517
|
+
document.querySelectorAll('.tab').forEach(t => {
|
|
518
|
+
t.addEventListener('click', () => {
|
|
519
|
+
document.querySelectorAll('.tab').forEach(x => x.classList.remove('active'));
|
|
520
|
+
document.querySelectorAll('.panel').forEach(x => x.classList.remove('active'));
|
|
521
|
+
t.classList.add('active');
|
|
522
|
+
document.getElementById(t.dataset.panel).classList.add('active');
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Initial load
|
|
527
|
+
loadOverview();
|
|
528
|
+
loadSessions();
|
|
529
|
+
loadGenome();
|
|
530
|
+
loadHeatmap();
|
|
531
|
+
loadAntibodies();
|
|
532
|
+
loadLedger();
|
|
533
|
+
</script>
|
|
534
|
+
</body>
|
|
535
|
+
</html>`;
|
|
536
|
+
// ── HTTP Server ──
|
|
537
|
+
const server = http.createServer((req, res) => {
|
|
538
|
+
const url = new URL(req.url || "/", `http://localhost:${PORT}`);
|
|
539
|
+
if (url.pathname.startsWith("/api/")) {
|
|
540
|
+
return handleAPI(req, res);
|
|
541
|
+
}
|
|
542
|
+
// Serve dashboard
|
|
543
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
544
|
+
res.end(DASHBOARD_HTML);
|
|
545
|
+
});
|
|
546
|
+
server.listen(PORT, () => {
|
|
547
|
+
console.error(`Observatory web dashboard: http://localhost:${PORT}`);
|
|
548
|
+
console.error(`API endpoints:`);
|
|
549
|
+
console.error(` GET /api/sessions`);
|
|
550
|
+
console.error(` GET /api/sessions/:id`);
|
|
551
|
+
console.error(` GET /api/genome`);
|
|
552
|
+
console.error(` GET /api/heatmap`);
|
|
553
|
+
console.error(` GET /api/antibodies`);
|
|
554
|
+
console.error(` GET /api/learned`);
|
|
555
|
+
console.error(`Phase 4 Ledger APIs:`);
|
|
556
|
+
console.error(` GET /api/ledger?sessionId=`);
|
|
557
|
+
console.error(` GET /api/consistency?sessionId=`);
|
|
558
|
+
console.error(` GET /api/diff?sessionA=&sessionB=`);
|
|
559
|
+
console.error(` GET /api/replay?sessionId=`);
|
|
560
|
+
console.error(` GET /api/fingerprints`);
|
|
561
|
+
});
|