Skip to content

Commit 4088d6e

Browse files
hyperpolymathclaude
andcommitted
feat: E3 — wire frontend to V-lang backend, add strategy panel
Frontend now proxies real requests to proven-nesy-solver-api instead of the E1 mock handler: * /api/prove POSTs to NESY_BACKEND_URL/prove (default localhost:9000), preserving the full echidna + verisim-api response shape (valid, outcome, prover, duration, goals_remaining, tactics_used, attempt_id, obligation_id, recorded) * /api/strategy?class=X proxies to /strategy/:class for live per-class top-prover data from verisim-api * /api/health aggregates frontend + backend reachability When NESY_BACKEND_URL is unreachable, handlers degrade to mock responses with mock:true — frontend never 500s on users. UI additions: * Strategy panel now auto-loads on page load + class-select change + after each prove (since every prove adds an attempt to the data) * Result panel shows attempt_id, truncated obligation SHA-256, and a "recorded" badge (green) or "not recorded" (amber) based on whether verisim-api accepted the POST * CSS for .badge, .strategy-table, .strategy-class E2E smoke verified locally: user submits SMT-LIB → nesy-solver proxies to V-server → echidna verifies → V-server records in verisim-api → strategy panel refreshes with new attempt count. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2d6606e commit 4088d6e

3 files changed

Lines changed: 193 additions & 52 deletions

File tree

public/app.js

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,17 +120,63 @@ function renderResult(body) {
120120
}
121121
const verdictClass = body.valid === true ? "valid" : body.valid === false ? "invalid" : "unknown";
122122
const verdictText = body.valid === true ? "valid" : body.valid === false ? "invalid" : "unknown";
123+
const recordedBadge = body.recorded === true
124+
? `<span class="badge ok">recorded</span>`
125+
: body.recorded === false && !body.mock
126+
? `<span class="badge warn">not recorded</span>`
127+
: "";
123128
resultEl.innerHTML = `
124-
<p class="verdict ${verdictClass}">${verdictText}</p>
129+
<p class="verdict ${verdictClass}">${verdictText} ${recordedBadge}</p>
125130
<dl>
126131
<dt>prover</dt><dd>${escapeHtml(body.prover ?? "—")}</dd>
127132
<dt>duration</dt><dd>${body.duration_ms ?? "—"} ms</dd>
128133
<dt>goals remaining</dt><dd>${body.goals_remaining ?? "—"}</dd>
129134
<dt>tactics used</dt><dd>${body.tactics_used ?? "—"}</dd>
130135
<dt>strategy</dt><dd>${escapeHtml(body.strategy_tag ?? "—")}</dd>
136+
${body.attempt_id ? `<dt>attempt</dt><dd>${escapeHtml(body.attempt_id)}</dd>` : ""}
137+
${body.obligation_id ? `<dt>obligation</dt><dd class="truncate">${escapeHtml(body.obligation_id.slice(0, 16))}…</dd>` : ""}
131138
</dl>
132139
${body.prover_output ? `<pre>${escapeHtml(body.prover_output)}</pre>` : ""}
133-
${body.mock ? `<p class="placeholder">⚠ Mock response — E3 will wire real echidna backend.</p>` : ""}
140+
${body.mock ? `<p class="placeholder">⚠ Backend unreachable — showing mock response.</p>` : ""}
141+
`;
142+
}
143+
144+
async function loadStrategy(classValue) {
145+
const target = classValue === "auto" ? "safety" : classValue;
146+
try {
147+
const resp = await fetch(`/api/strategy?class=${encodeURIComponent(target)}`);
148+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
149+
const body = await resp.json();
150+
renderStrategy(body, target);
151+
} catch (err) {
152+
const strategyEl = document.getElementById("strategy");
153+
if (strategyEl) {
154+
strategyEl.innerHTML = `<p class="placeholder">Strategy data unavailable: ${escapeHtml(err.message)}</p>`;
155+
}
156+
}
157+
}
158+
159+
function renderStrategy(body, targetClass) {
160+
const strategyEl = document.getElementById("strategy");
161+
if (!strategyEl) return;
162+
const recs = body.recommendations ?? [];
163+
if (recs.length === 0) {
164+
strategyEl.innerHTML = `<p class="placeholder">No attempts recorded yet for class <code>${escapeHtml(targetClass)}</code>.</p>`;
165+
return;
166+
}
167+
const rows = recs.slice(0, 5).map((r) => `
168+
<tr>
169+
<td>${escapeHtml(r.prover)}</td>
170+
<td>${((r.success_rate ?? 0) * 100).toFixed(1)}%</td>
171+
<td>${(r.avg_duration_ms ?? 0).toFixed(0)} ms</td>
172+
<td>${r.total_attempts ?? 0}</td>
173+
</tr>`).join("");
174+
strategyEl.innerHTML = `
175+
<p class="strategy-class">class: <code>${escapeHtml(targetClass)}</code>${body.mock ? " (mock)" : ""}</p>
176+
<table class="strategy-table">
177+
<thead><tr><th>prover</th><th>success</th><th>avg</th><th>n</th></tr></thead>
178+
<tbody>${rows}</tbody>
179+
</table>
134180
`;
135181
}
136182

@@ -147,7 +193,8 @@ function setStatus(text, kind) {
147193

148194
// ── Events ────────────────────────────────────────────────────────────
149195
langSelect.addEventListener("change", () => mountEditor(langSelect.value));
150-
proveBtn.addEventListener("click", prove);
196+
classSelect.addEventListener("change", () => loadStrategy(classSelect.value));
197+
proveBtn.addEventListener("click", async () => { await prove(); loadStrategy(classSelect.value); });
151198
clearBtn.addEventListener("click", () => {
152199
if (editorView) editorView.dispatch({ changes: { from: 0, to: editorView.state.doc.length } });
153200
resultEl.innerHTML = `<p class="placeholder">Submit an obligation to see the prover's verdict.</p>`;
@@ -156,4 +203,5 @@ clearBtn.addEventListener("click", () => {
156203

157204
// ── Boot ──────────────────────────────────────────────────────────────
158205
mountEditor(langSelect.value);
206+
loadStrategy(classSelect.value);
159207
setStatus("ready", "ok");

public/styles.css

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,55 @@ body {
308308
max-height: 200px;
309309
}
310310
}
311+
312+
/* E3: strategy table + recorded badge */
313+
.badge {
314+
display: inline-block;
315+
font-family: var(--mono);
316+
font-size: 0.7rem;
317+
padding: 0.1rem 0.4rem;
318+
border-radius: 8px;
319+
margin-left: 0.5rem;
320+
font-weight: 500;
321+
text-transform: lowercase;
322+
}
323+
.badge.ok { background: #3fb95033; color: var(--success); }
324+
.badge.warn { background: #d2992233; color: var(--warn); }
325+
326+
.strategy-class {
327+
font-size: 0.8rem;
328+
color: var(--fg-muted);
329+
margin-bottom: 0.5rem;
330+
}
331+
.strategy-class code {
332+
color: var(--accent);
333+
background: var(--bg);
334+
padding: 0.1rem 0.3rem;
335+
border-radius: 3px;
336+
font-size: 0.75rem;
337+
}
338+
339+
.strategy-table {
340+
width: 100%;
341+
border-collapse: collapse;
342+
font-size: 0.8rem;
343+
font-family: var(--mono);
344+
}
345+
.strategy-table th {
346+
text-align: left;
347+
color: var(--fg-dim);
348+
font-weight: 500;
349+
padding: 0.25rem 0.5rem 0.25rem 0;
350+
border-bottom: 1px solid var(--border);
351+
}
352+
.strategy-table td {
353+
padding: 0.25rem 0.5rem 0.25rem 0;
354+
color: var(--fg);
355+
border-bottom: 1px solid var(--border);
356+
}
357+
.strategy-table tr:last-child td { border-bottom: none; }
358+
359+
.result dd.truncate {
360+
font-size: 0.75rem;
361+
color: var(--fg-muted);
362+
}

server.js

Lines changed: 90 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
// SPDX-License-Identifier: PMPL-1.0-or-later
22
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
33

4-
// nesy-solver dev server — Deno (NOT Node) serves static files + stub API.
5-
// E3 will replace /api/prove with a proxy to proven-server → echidna :8090.
4+
// nesy-solver dev server — Deno serves static files + proxies the API.
5+
// E3 wires /api/prove and /api/strategy to the V-lang backend
6+
// (proven-nesy-solver-api running on NESY_BACKEND_URL, default :9000),
7+
// which forwards to echidna (:8090) and verisim-api (:8080).
8+
//
9+
// When the backend is unreachable the handlers degrade to a mock response
10+
// with `mock: true` so the frontend never 500s.
611

712
import { serveDir } from "@std/http/file-server";
813
import { join } from "@std/path";
914

1015
// Default port 8787 to avoid collision with verisim-api (8080) on the dev host.
1116
const PORT = Number(Deno.env.get("PORT") ?? 8787);
17+
const BACKEND_URL = Deno.env.get("NESY_BACKEND_URL") ?? "http://localhost:9000";
1218
const ROOT = new URL(".", import.meta.url).pathname;
1319

14-
/**
15-
* Mock prove handler. Mirrors echidna /api/verify response shape so the
16-
* frontend contract is stable before E3 wires the real backend.
17-
*
18-
* Request shape: { language, obligationClass, prover, content }
19-
* Response shape: { valid, duration_ms, goals_remaining, tactics_used,
20-
* prover, strategy_tag, prover_output, mock: true }
21-
*/
20+
/** POST /api/prove — proxies to V backend /prove, degrades to mock on failure. */
2221
async function handleProve(req) {
2322
let body;
2423
try {
@@ -31,42 +30,100 @@ async function handleProve(req) {
3130
return json({ error: "content required" }, 400);
3231
}
3332

34-
// Deterministic mock: "valid" iff content references `check-sat`, `Qed`, `refl`, `Refl`, or `by`.
33+
try {
34+
const resp = await fetch(`${BACKEND_URL}/prove`, {
35+
method: "POST",
36+
headers: { "Content-Type": "application/json" },
37+
body: JSON.stringify({ language, obligationClass, prover, content }),
38+
signal: AbortSignal.timeout(30_000),
39+
});
40+
const bodyText = await resp.text();
41+
return new Response(bodyText, {
42+
status: resp.status,
43+
headers: {
44+
"Content-Type": "application/json; charset=utf-8",
45+
"Access-Control-Allow-Origin": "*",
46+
},
47+
});
48+
} catch (err) {
49+
console.warn(`backend unreachable: ${err.message} — returning mock`);
50+
return json(mockProveResponse({ language, obligationClass, prover, content }));
51+
}
52+
}
53+
54+
/** GET /api/strategy?class=safety — proxies to V backend /strategy/:class. */
55+
async function handleStrategy(req) {
56+
const url = new URL(req.url);
57+
const cls = url.searchParams.get("class") ?? "safety";
58+
try {
59+
const resp = await fetch(`${BACKEND_URL}/strategy/${encodeURIComponent(cls)}`, {
60+
signal: AbortSignal.timeout(10_000),
61+
});
62+
const bodyText = await resp.text();
63+
return new Response(bodyText, {
64+
status: resp.status,
65+
headers: {
66+
"Content-Type": "application/json; charset=utf-8",
67+
"Access-Control-Allow-Origin": "*",
68+
},
69+
});
70+
} catch (err) {
71+
console.warn(`backend unreachable: ${err.message} — returning mock strategy`);
72+
return json({
73+
mock: true,
74+
obligation_class: cls,
75+
recommendations: [{ prover: "z3", success_rate: 0, avg_duration_ms: 0, total_attempts: 0 }],
76+
});
77+
}
78+
}
79+
80+
/** GET /api/health — aggregates frontend + backend health. */
81+
async function handleHealth() {
82+
let backend = null;
83+
try {
84+
const resp = await fetch(`${BACKEND_URL}/health`, { signal: AbortSignal.timeout(3_000) });
85+
if (resp.ok) backend = await resp.json();
86+
} catch (_err) {
87+
backend = { reachable: false, url: BACKEND_URL };
88+
}
89+
return json({
90+
status: "ok",
91+
version: "0.1.0",
92+
frontend_port: PORT,
93+
backend_url: BACKEND_URL,
94+
backend,
95+
});
96+
}
97+
98+
/** Mock prove response used when the V backend is unreachable. */
99+
function mockProveResponse({ language, obligationClass, prover, content }) {
35100
const valid = /check-sat|Qed|refl|Refl|by\s/.test(content);
36101
const resolvedProver = prover === "auto" ? pickProver(obligationClass, language) : prover;
37102
const duration_ms = 20 + Math.floor(Math.random() * 80);
38-
39-
return json({
103+
return {
40104
valid,
105+
outcome: valid ? "success" : "failure",
106+
prover: resolvedProver,
41107
duration_ms,
42108
goals_remaining: valid ? 0 : 1,
43109
tactics_used: valid ? 3 : 0,
44-
prover: resolvedProver,
45110
obligation_class: obligationClass,
46111
language,
47112
strategy_tag: "mock-handler",
48113
prover_output: valid
49114
? `; ${resolvedProver} OK (mock)\n; ${content.split("\n").length} lines processed`
50-
: `; ${resolvedProver} could not dispatch (mock)\n; E3 will wire real echidna backend`,
115+
: `; ${resolvedProver} could not dispatch (mock)\n; backend ${BACKEND_URL} unreachable`,
116+
attempt_id: null,
117+
recorded: false,
51118
mock: true,
52-
});
119+
};
53120
}
54121

55-
/** Crude strategy fallback — replaced in E3 by verisim-api /strategy query. */
56122
function pickProver(obligationClass, language) {
57-
const byLang = {
58-
smtlib: "Z3",
59-
lean: "Lean",
60-
coq: "Coq",
61-
idris2: "Idris2",
62-
agda: "Agda",
63-
};
123+
const byLang = { smtlib: "Z3", lean: "Lean", coq: "Coq", idris2: "Idris2", agda: "Agda" };
64124
const byClass = {
65-
safety: "Z3",
66-
linearity: "Idris2",
67-
termination: "Agda",
68-
equiv: "Lean",
69-
correctness: "Coq",
125+
safety: "Z3", linearity: "Idris2", termination: "Agda",
126+
equiv: "Lean", correctness: "Coq",
70127
};
71128
return byClass[obligationClass] ?? byLang[language] ?? "Z3";
72129
}
@@ -81,29 +138,13 @@ function json(payload, status = 200) {
81138
});
82139
}
83140

84-
/** Serves index.html at root, static files for /public/*, and API for /api/*. */
85141
async function handler(req) {
86142
const url = new URL(req.url);
87143

88-
if (url.pathname === "/api/prove" && req.method === "POST") {
89-
return handleProve(req);
90-
}
91-
if (url.pathname === "/api/health" && req.method === "GET") {
92-
return json({ status: "ok", version: "0.1.0", mode: "mock" });
93-
}
94-
if (url.pathname === "/api/strategy" && req.method === "GET") {
95-
// Mock strategy data — E3 proxies verisim-api /api/v1/proof_attempts/strategy
96-
return json({
97-
mock: true,
98-
classes: {
99-
safety: { top: "Z3", success_rate: 0.92, n: 24 },
100-
linearity: { top: "Idris2", success_rate: 0.78, n: 9 },
101-
termination: { top: "Agda", success_rate: 0.71, n: 7 },
102-
},
103-
});
104-
}
144+
if (url.pathname === "/api/prove" && req.method === "POST") return handleProve(req);
145+
if (url.pathname === "/api/strategy" && req.method === "GET") return handleStrategy(req);
146+
if (url.pathname === "/api/health" && req.method === "GET") return handleHealth();
105147

106-
// Static files
107148
if (url.pathname === "/" || url.pathname === "/index.html") {
108149
const html = await Deno.readTextFile(join(ROOT, "index.html"));
109150
return new Response(html, {
@@ -114,5 +155,5 @@ async function handler(req) {
114155
}
115156

116157
console.log(`nesy-solver dev server listening on http://localhost:${PORT}`);
117-
console.log(` mode: mock (echidna backend wires in E3)`);
158+
console.log(` backend: ${BACKEND_URL}`);
118159
Deno.serve({ port: PORT }, handler);

0 commit comments

Comments
 (0)