Compare commits

..
Author SHA1 Message Date
ahmed mohammedandClaude Opus 4.7 5160a00db5 fix: final review pass — email collision, orphaned-run loop, null confidence guards
- changeEmail validates and refuses an address another account owns (users.email is UNIQUE; a collision was a 500 inside the settings form).
- /app/[id] no longer redirects a 'running' row to itself; it renders what exists and says the run was interrupted.
- ScoreBar / OptionRow / VerdictBlock survive null numbers from the model instead of throwing and losing the block (seen live: '[openui] runtime/render-error: Component ScoreBar render failed').
- drop the ad-hoc render-debug scripts now that Playwright covers the browser.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 09:10:29 +03:00
ahmed mohammedandClaude Opus 4.7 07dd3d6bf8 test: playwright end-to-end qa (17 tests, real models)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 09:02:53 +03:00
ahmed mohammedandClaude Opus 4.7 0db7b8140b feat: docker, readme, jev goal gate
Dockerfile/compose written but never built or run on this machine (no Docker). smoke.sh now works in both modes and fails loudly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 08:40:57 +03:00
ahmed mohammedandClaude Opus 4.7 a5529ce542 feat: console ui, streaming report, history actions
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 08:37:28 +03:00
ahmed mohammedandClaude Opus 4.7 ee4a88e499 chore: dev dep @playwright/test
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 08:32:40 +03:00
ahmed mohammedandClaude Opus 4.7 ac4cbdd9e6 feat: analysis pipeline with staged SSE
Adds @openuidev/lang-core (one-line reason: the prompt-side library must build in a non-React Node runtime; react-lang's createContext crashes the route's server bundle).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 08:32:22 +03:00
ahmed mohammedandClaude Opus 4.7 13bd2e6bfd fix: open sqlite lazily so parallel build workers stop locking the file
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 07:56:18 +03:00
ahmed mohammedandClaude Opus 4.7 396b0edd9e feat: openui report library
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 07:54:52 +03:00
ahmed mohammedandClaude Opus 4.7 d8caa8e6db feat: jev composite scoring and verdict
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 07:53:03 +03:00
43 changed files with 1259 additions and 26 deletions
+16
View File
@@ -0,0 +1,16 @@
.next
node_modules
data
.git
.hallmark
docs
design-system
tests
tests-e2e
fixtures
.superpowers
.env*
.playwright-data
.auth
playwright-report
test-results
+2 -2
View File
@@ -19,6 +19,6 @@ next-env.d.ts
/test-results
/playwright-report
# local fixtures
/tests/__fixtures__
# e2e fixture input is committed; scratch output is not
.DS_Store
.playwright-data
+9
View File
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
<!-- END:nextjs-agent-rules -->
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+27
View File
@@ -0,0 +1,27 @@
FROM node:24-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:24-slim AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
FROM node:24-slim AS run
WORKDIR /app
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 DATA_DIR=/data PORT=3000
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* \
&& mkdir -p /data && chown node:node /data
COPY --from=build --chown=node:node /app/.next ./.next
COPY --from=build --chown=node:node /app/public ./public
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/package.json ./package.json
COPY --from=build --chown=node:node /app/next.config.* ./
USER node
VOLUME /data
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD curl -fsS localhost:3000/api/healthz
CMD ["npm", "start"]
+7 -2
View File
@@ -23,13 +23,15 @@ Getting the XML: SSMS → **Ctrl+M** (include actual execution plan) → run the
## Run it
This repo runs as a plain Node process. Containerization is written (`Dockerfile`, Task 12 of the plan) but is not
required for local work — `npm run dev` is the normal path on a small machine.
This repo runs as a plain Node process. `Dockerfile` and `docker-compose.yml` are ready, but the image has **not
been built or run on this machine** (Docker is not installed here); treat it as untested until
`bash scripts/smoke.sh --docker` says otherwise. Local work is `npm run dev`.
```bash
npm install
cp .env.example .env.local # then fill in APP_SECRET and DATA_DIR
npm run dev # http://localhost:3000
bash scripts/smoke.sh # healthz, login page, signed-out redirect, analyze needs a session
```
`.env.local`:
@@ -44,6 +46,9 @@ brings their own endpoint and their own spend. Blank key field = keep the stored
- Analyst: any OpenAI-compatible base URL + key + model, plus optional extra JSON params.
A local endpoint such as `http://192.168.88.131:8888/v1` works; use key `EMPTY` when the server has no auth.
For a reasoning model (Qwen3 on vLLM) put `{"chat_template_kwargs":{"enable_thinking":false}}` in **Extra params**:
with thinking on, the model spends the whole context budget reasoning and the report arrives empty.
Both model calls default to `max_tokens: 2048`; the same field overrides it.
- Jev: a TypeSafe API key from `console.typesafe.ai/keys`, model usually `jev-latest`.
## Tests
+95
View File
@@ -0,0 +1,95 @@
import { requireUser } from "@/lib/auth";
import { analystConfig, jevKey, jevModel } from "@/lib/settings";
import { digestPlan, digestForModel, PlanError } from "@/lib/digest";
import { proposeCandidates, streamReport, AnalystError } from "@/lib/analyst";
import { judgeCandidates, jevFallback, makeJevClient } from "@/lib/jev";
import { sse } from "@/lib/stream";
import { newAnalysis, patchAnalysis } from "@/lib/db";
import { reportPrompt } from "@/lib/report-spec";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
const MAX_BODY_BYTES = 16_000_000;
export async function POST(req: Request) {
const user = await requireUser().catch(() => null);
if (!user) return Response.json({ error: "Sign in first." }, { status: 401 });
const analyst = analystConfig(user.id);
const jev = jevKey(user.id);
if (!analyst) return Response.json({ error: "Set the analyst model in Settings first." }, { status: 412 });
if (!jev) return Response.json({ error: "Set your Jev (TypeSafe) API key in Settings first." }, { status: 412 });
const raw = await req.text();
if (raw.length > MAX_BODY_BYTES) return Response.json({ error: "Request too large." }, { status: 413 });
let xml = ""; let note = "";
try { const body = JSON.parse(raw); xml = String(body.xml ?? ""); note = String(body.note ?? "").slice(0, 500); }
catch { return Response.json({ error: "Expected JSON with an xml field." }, { status: 400 }); }
const id = newAnalysis(user.id, note.trim().slice(0, 120) || "Plan analysis", xml);
const enc = new TextEncoder();
const prompt = reportPrompt();
// The analyst model needs minutes per stage. If the DBA closes the tab, enqueue() does not throw, so the
// producer would keep firing model calls into a dead stream and queue behind its own ghost. cancel() is the
// only signal that the reader is gone: stop launching further calls and abort the one in flight.
const ac = new AbortController();
let abandoned = false;
const stream = new ReadableStream<Uint8Array>({ async start(ctrl) {
const send = (event: string, data: unknown) => ctrl.enqueue(enc.encode(sse(event, data)));
let oul = "";
const close = (status: string, error?: string) => {
patchAnalysis(id, { status, ...(error ? { error } : {}), ...(oul ? { oul } : {}) });
send("done", { id }); ctrl.close();
};
try {
send("stage", { stage: "digesting" });
let digest;
try { digest = digestPlan(xml); }
catch (e) { const m = e instanceof PlanError ? e.message : "Could not read that plan.";
send("error", { stage: "digesting", message: m }); patchAnalysis(id, { status: "failed", error: m });
send("done", { id }); return ctrl.close(); }
if (abandoned) return giveUp(id, oul);
patchAnalysis(id, { digest: JSON.stringify(digest) });
send("stage", { stage: "proposing" });
let candidates;
try { candidates = await proposeCandidates(analyst, digest, note, fetch, ac.signal); }
catch (e) { const m = e instanceof AnalystError ? e.message : "The analyst model call failed.";
send("error", { stage: "proposing", message: m }); patchAnalysis(id, { status: "failed", error: m });
send("done", { id }); return ctrl.close(); }
if (abandoned) return giveUp(id, oul);
candidates.forEach((c) => send("candidate", { key: c.key, title: c.title }));
patchAnalysis(id, { candidates: JSON.stringify(candidates) });
send("stage", { stage: "judging" });
let verdict;
try { verdict = await judgeCandidates(digest, candidates, makeJevClient(jev, jevModel(user.id))); }
catch (e) { verdict = jevFallback(candidates, `Jev call failed: ${(e as Error).message}`); }
send("verdict", verdict);
patchAnalysis(id, { verdict: JSON.stringify(verdict) });
send("stage", { stage: "writing" });
for await (const chunk of streamReport(analyst,
{ plan: digestForModel(digest), options: candidates, verdict }, prompt, fetch, ac.signal)) {
oul += chunk; send("oul", { chunk });
}
if (abandoned) return giveUp(id, oul);
close(oul ? "done" : "partial", oul ? undefined : "The analyst model returned an empty report.");
} catch (e) {
const m = abandoned ? "Browser stopped listening." : ((e as Error)?.message ?? "Analysis failed.");
if (!abandoned) send("error", { stage: "stream", message: m });
patchAnalysis(id, { status: "failed", error: m, ...(oul ? { oul } : {}) });
send("done", { id }); ctrl.close();
}
function giveUp(id: string, oul: string) {
patchAnalysis(id, { status: oul ? "partial" : "abandoned",
error: "Closed before the run finished.", ...(oul ? { oul } : {}) });
try { ctrl.close(); } catch { /* already closed */ }
}
}, cancel() { abandoned = true; ac.abort(); } });
return new Response(stream, { headers: { "content-type": "text/event-stream; charset=utf-8",
"cache-control": "no-store", connection: "keep-alive", "x-accel-buffering": "no" } });
}
+24
View File
@@ -0,0 +1,24 @@
import { notFound } from "next/navigation";
import { requireUser } from "@/lib/auth";
import { getAnalysis } from "@/lib/db";
import { ReportView } from "@/components/report-view";
import { ExportButtons } from "@/components/export-buttons";
import { VerdictSummary } from "@/components/verdict-summary";
export default async function Stored({ params }: { params: Promise<{ id: string }> }) {
const u = await requireUser();
const a = getAnalysis((await params).id, u.id);
if (!a) notFound();
// Never redirect here: a row left "running" by a crashed process would bounce this page to itself forever.
const interrupted = a.status === "running";
return <main style={{ display: "grid", gap: "var(--qai-space-xl)" }}>
<h1 style={{ fontFamily: "var(--qai-font-display)", fontSize: "var(--qai-text-2xl)", margin: 0 }}>{a.title}</h1>
{interrupted && <p role="alert" style={{ color: "var(--qai-warn)" }}>
This run was interrupted before it finished — what is below is everything it managed to produce.
</p>}
{a.error && <p role="alert" style={{ color: "var(--qai-danger)" }}>{a.error}</p>}
{a.verdict && <VerdictSummary verdict={JSON.parse(a.verdict)} />}
{a.oul ? <><ExportButtons oul={a.oul} title={a.title} /><ReportView oul={a.oul} streaming={false} /></>
: <p className="label">No report was stored for this run.</p>}
</main>;
}
+3 -2
View File
@@ -1,9 +1,10 @@
import { requireUser } from "@/lib/auth";
import { Rail } from "@/components/rail";
export default async function Console({ children }: { children: React.ReactNode }) {
await requireUser();
const u = await requireUser();
return <div className="shell">
<aside className="rail" aria-label="Previous analyses">qAI</aside>
<aside className="rail no-print" aria-label="Previous analyses"><Rail userId={u.id} /></aside>
<main className="main">{children}</main>
</div>;
}
+2 -1
View File
@@ -1 +1,2 @@
export default function NewAnalysis() { return <h1 style={{ fontFamily: "var(--qai-font-display)" }}>New analysis</h1>; }
import { Runner } from "@/components/runner";
export default function Run() { return <Runner />; }
+1 -1
View File
@@ -20,7 +20,7 @@ export default async function Setup({ searchParams }: { searchParams: Promise<{
<h1 style={{ fontFamily: "var(--qai-font-display)", fontSize: "var(--qai-text-display)", letterSpacing: "-0.02em" }}>
{TITLES[n]}</h1>
{n === "1" && <p>qAI reads a SQL Server ShowPlan XML, works out the bottleneck in code, asks your model for several ways to fix it, then has Jev score each one. You configure nothing on this screen.</p>}
{n === "2" && <SettingsForm action={saveSettingsAction} view={view} step="Analyst model and Jev key" />}
{n === "2" && <SettingsForm action={saveSettingsAction} view={view} step="Analyst model and Jev key" after="/setup?step=3" />}
{n === "3" && <>
<p>One run = one digest, a few model calls, four Jev questions per option. If the analyst or Jev is unreachable, the report says so instead of inventing a verdict.</p>
<p className="label">Analyst {view.has_analyst ? "ready" : "missing"} · Jev {view.has_jev ? "ready" : "missing"}</p>
+11
View File
@@ -0,0 +1,11 @@
"use client";
export function ExportButtons({ oul, title }: { oul: string; title: string }) {
const copy = async () => {
const sql = [...document.querySelectorAll<HTMLElement>("#report pre.sql")].map((p) => p.textContent).join("\n\n");
await navigator.clipboard.writeText(`${title}\n\n${sql || oul}`);
};
return <div className="toolbar no-print" style={{ display: "flex", gap: "var(--qai-space-s)" }}>
<button className="btn" onClick={copy}>Copy</button>
<button className="btn" onClick={() => window.print()}>Export to PDF</button>
</div>;
}
+30
View File
@@ -0,0 +1,30 @@
"use client";
import { rename, remove } from "@/lib/actions";
export function HistoryItem({ row }: { row: { id: string; title: string; status: string; created_at: number } }) {
// The server actions take (previousState, formData) for useActionState; a plain <form action> passes only
// formData, so bind the unused slot here rather than duplicating two single-arg actions.
const doRename = (fd: FormData) => rename(null, fd);
const doRemove = (fd: FormData) => remove(null, fd);
const when = new Date(row.created_at).toLocaleString(undefined,
{ month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
return <li className="card" style={{ padding: "var(--qai-space-md)", display: "grid", gap: "var(--qai-space-s)" }}>
<a href={`/app/${row.id}`} style={{ color: "var(--qai-ink)", textDecoration: "none", fontSize: "var(--qai-text-sm)" }}>
{row.title}{row.status !== "done" && <span className="pill" style={{ marginInlineStart: 8 }}>{row.status}</span>}
</a>
<div style={{ display: "flex", gap: "var(--qai-space-s)", alignItems: "center", flexWrap: "wrap" }}>
<span className="label">{when}</span>
<form action={doRename} style={{ display: "flex", gap: 4, marginLeft: "auto" }}>
<input type="hidden" name="id" value={row.id} />
<input className="field" name="title" aria-label="New name" defaultValue={row.title}
style={{ width: 90, padding: "4px 6px" }} />
<button className="btn" style={{ padding: "4px 8px" }}>Save</button>
</form>
{/* Delete is the only destructive action in the product: it never fires on a stray click. */}
<form action={doRemove} onSubmit={(e) => { if (!confirm("Delete this analysis?")) e.preventDefault(); }}>
<input type="hidden" name="id" value={row.id} />
<button className="btn" style={{ padding: "4px 8px" }}>Delete</button>
</form>
</div>
</li>;
}
+15
View File
@@ -0,0 +1,15 @@
import { listAnalyses } from "@/lib/db";
import { HistoryItem } from "./history-item";
export function Rail({ userId }: { userId: string }) {
const rows = listAnalyses(userId);
return <>
<a href="/app" className="label" style={{ textDecoration: "none", color: "var(--qai-accent)" }}>qAI</a>
<p className="label">History · {rows.length}</p>
<ul style={{ listStyle: "none", padding: 0, margin: 0, display: "grid", gap: "var(--qai-space-s)" }}>
{rows.map((r) => <HistoryItem key={r.id} row={r} />)}
</ul>
<p className="label" style={{ marginTop: "auto" }}>
<a href="/settings" style={{ color: "var(--qai-ink-muted)" }}>Settings</a></p>
</>;
}
+8
View File
@@ -0,0 +1,8 @@
"use client";
import { Renderer } from "@openuidev/react-lang";
import { library } from "@/lib/report-ui";
export function ReportView({ oul, streaming }: { oul: string; streaming: boolean }) {
if (!oul) return null;
return <Renderer response={oul} library={library} isStreaming={streaming} />;
}
+74
View File
@@ -0,0 +1,74 @@
"use client";
import { useState } from "react";
import { readSse } from "@/lib/stream";
import { ReportView } from "./report-view";
import { ExportButtons } from "./export-buttons";
const STAGES = ["digesting", "proposing", "judging", "writing"] as const;
const LABEL: Record<string, string> = { digesting: "Reading the plan", proposing: "Asking for options",
judging: "Jev is scoring", writing: "Writing the report" };
export function Runner() {
const [xml, setXml] = useState(""); const [note, setNote] = useState("");
const [stage, setStage] = useState<string | null>(null);
const [oul, setOul] = useState(""); const [error, setError] = useState<string | null>(null);
const at = stage ? STAGES.indexOf(stage as (typeof STAGES)[number]) : -1;
async function run() {
setError(null); setOul(""); setStage("digesting");
try {
const res = await fetch("/api/analyze", { method: "POST",
headers: { "content-type": "application/json" }, body: JSON.stringify({ xml, note }) });
if (!res.ok || !res.body) { setError((await res.json().catch(() => null))?.error ?? `Request failed (${res.status}).`);
return setStage(null); }
for await (const e of readSse(res.body)) {
if (e.event === "stage") setStage(e.data.stage);
else if (e.event === "oul") setOul((s) => s + e.data.chunk);
else if (e.event === "error") setError(`${e.data.stage}: ${e.data.message}`);
else if (e.event === "done") { setStage(null); history.replaceState(null, "", `/app/${e.data.id}`); }
}
} catch (e) {
setError((e as Error).message || "The connection to the server dropped.");
setStage(null);
}
}
return <section style={{ display: "grid", gap: "var(--qai-space-xl)", minWidth: 0 }}>
<h1 style={{ fontFamily: "var(--qai-font-display)", fontSize: "var(--qai-text-display)",
letterSpacing: "-0.02em", margin: 0 }}>Paste a plan</h1>
<details className="card">
<summary style={{ cursor: "pointer", fontFamily: "var(--qai-font-display)", fontSize: "var(--qai-text-sm)" }}>
Where do I get this XML?</summary>
<ol style={{ fontSize: "var(--qai-text-sm)", color: "var(--qai-ink-muted)", margin: "var(--qai-space-md) 0 0",
paddingLeft: "1.2em" }}>
<li>SSMS: press <strong>Ctrl+M</strong> (Include Actual Execution Plan), then run the query.</li>
<li>On the Execution Plan tab, right-click → <strong>Save As .sqlplan</strong>. Or right-click the statement → <strong>Showplan XML</strong> and copy the cell.</li>
<li>No SSMS: prefix the batch with <code>SET STATISTICS XML ON;</code> and copy the XML result cell.</li>
<li>Drop the file or paste its contents below. Only the digest leaves this container, and only to your own models.</li>
</ol>
</details>
<div className="runner card" style={{ display: "grid", gap: "var(--qai-space-md)" }}>
<label className="label" htmlFor="xml">ShowPlan XML or .sqlplan contents</label>
<textarea className="field" id="xml" rows={7} spellCheck={false} value={xml}
onChange={(e) => setXml(e.target.value)} placeholder="<ShowPlanXML …>" />
<label className="label" htmlFor="note">Anything the plan cannot see? (optional)</label>
<input className="field" id="note" value={note} onChange={(e) => setNote(e.target.value)}
placeholder="slow since the 16.0.4085 migration" />
<div style={{ display: "flex", gap: "var(--qai-space-s)", alignItems: "center", flexWrap: "wrap" }}>
<button className="btn btn-primary" onClick={run} disabled={!xml.trim() || stage !== null}>Analyse</button>
<input type="file" accept=".sqlplan,.xml" aria-label="Load a .sqlplan file" style={{ fontSize: "var(--qai-text-xs)" }}
onChange={async (e) => {
const f = e.target.files?.[0]; if (!f) return;
const buf = new Uint8Array(await f.arrayBuffer());
// SSMS saves UTF-16LE; File.text() decodes that as UTF-8 and fills the plan with NUL bytes.
const utf16 = buf[0] === 0xff && buf[1] === 0xfe;
setXml(new TextDecoder(utf16 ? "utf-16le" : "utf-8").decode(buf).replace(/^/, ""));
}} />
{at >= 0 && <span className="pill" aria-live="polite">{at + 1} of 4 · {LABEL[STAGES[at]]}</span>}
</div>
{error && <p role="alert" style={{ color: "var(--qai-danger)", margin: 0 }}>{error}</p>}
</div>
{oul && <ExportButtons oul={oul} title={note || "Query plan analysis"} />}
<ReportView oul={oul} streaming={stage !== null} />
</section>;
}
+6 -3
View File
@@ -4,9 +4,9 @@ import type { SettingsView } from "@/lib/settings";
type Result = { ok?: boolean; error?: string } | null;
export function SettingsForm({ action, view, step, account }: {
export function SettingsForm({ action, view, step, account, after }: {
action: (state: Result, fd: FormData) => Promise<Result>;
view: SettingsView; step?: string; account?: boolean;
view: SettingsView; step?: string; account?: boolean; after?: string;
}) {
const [state, formAction, pending] = useActionState<Result, FormData>(action, null);
const F = (name: string, label: string, value: string, help: string, type = "text") => (
@@ -19,13 +19,16 @@ export function SettingsForm({ action, view, step, account }: {
<fieldset style={{ border: 0, padding: 0, margin: 0, display: "grid", gap: "var(--qai-space-md)" }}>
<legend className="label">{title}</legend>{children}</fieldset>);
return <form action={formAction} className="card" style={{ display: "grid", gap: "var(--qai-space-l)" }}>
{after && <input type="hidden" name="after" value={after} />}
{step && <p className="label">{step}</p>}
<Group title="Analyst model — OpenAI-compatible">
{F("analyst_base_url", "Base URL", view.analyst_base_url, "e.g. https://api.openai.com/v1")}
{F("analyst_key", "API key", "", view.has_analyst
? `Saved ${view.analyst_key_masked} — leave blank to keep it` : "Encrypted with APP_SECRET before it hits disk")}
{F("analyst_model", "Model name", view.analyst_model, "e.g. gpt-4o-mini, llama-3.3-70b")}
{F("analyst_extra", "Extra params (JSON)", view.analyst_extra, 'Merged into the request body, e.g. {"temperature":0.2}')}
{F("analyst_extra", "Extra params (JSON)", view.analyst_extra,
'Merged into the request body, e.g. {"temperature":0.2}. Reasoning models stream thinking, not the answer: '
+ 'vLLM/Qwen needs {"chat_template_kwargs":{"enable_thinking":false}} or the report arrives empty.')}
</Group>
<Group title="Jev — TypeSafe System One">
{F("jev_key", "TypeSafe API key", "", view.has_jev
+12
View File
@@ -0,0 +1,12 @@
export function VerdictSummary({ verdict }: { verdict: any }) {
const win = verdict.order.find((o: any) => o.key === verdict.headline) ?? verdict.order[0];
return <section className="card" data-verdict>
<p className="label">Jev verdict · {verdict.source === "jev"
? `chosen, confidence ${verdict.jev_confidence.toFixed(2)}` : "composite order — Jev did not decide"}</p>
<p style={{ fontFamily: "var(--qai-font-display)", fontSize: "var(--qai-text-lg)" }}>{win?.key}</p>
<p className="label">weights {Object.entries(verdict.weights as Record<string, number>)
.map(([k, v]) => `${k} ${v}`).join(" · ")}</p>
{Array.isArray(verdict.flags) && verdict.flags.length > 0
&& <p style={{ color: "var(--qai-warn)" }}>{verdict.flags.join(" · ")}</p>}
</section>;
}
+10
View File
@@ -0,0 +1,10 @@
services:
qai:
build: .
ports: ["3000:3000"]
environment:
APP_SECRET: ${APP_SECRET:?set APP_SECRET in .env, 16+ chars}
volumes: ["qai-data:/data"]
restart: unless-stopped
volumes:
qai-data:
@@ -39,6 +39,18 @@ The demo value is Jev as the decision-maker: the generative model proposes,
Report actions: **Export to PDF**, **Copy**. History actions: **rename**, **delete**.
### Getting the input, and the two keys
The portal cannot reach the SQL Server. The user brings the plan XML, and the app must say so in the run screen:
SSMS → **Ctrl+M** (include actual execution plan) → run the query → save the Execution Plan tab as `.sqlplan`, or
copy the Showplan XML cell; without SSMS, prefix the batch with `SET STATISTICS XML ON;` and copy the XML result
cell. A `.sqlplan` file can be dropped in (SSMS writes UTF-16 with a BOM; decode both) or pasted.
Both model credentials are typed by the logged-in user in `/setup` or `/settings` — never server config, so every
user brings their own analyst endpoint and their own TypeSafe/Jev key (`console.typesafe.ai/keys`). Stored keys come
back masked, and a blank field means keep the stored one. Nothing leaves the container except the digest, and only
to the endpoints that user configured.
## Actors (two models, strict roles)
- **Analyst** — any OpenAI-compatible endpoint the user configures (`base_url`,
@@ -82,3 +94,21 @@ Report actions: **Export to PDF**, **Copy**. History actions: **rename**, **dele
Paste a real slow-query plan → under 60 s → a ranked set of remediations where the
top pick is defensible because Jev's probabilities are on screen, and the winning
SQL is one click from the clipboard.
## Acceptance
What has to be true before this counts as done, and how each part is proven:
- **Offline unit suites** (`npm test`, no keys, no network): a real ShowPlan digests to the right numbers; input
that is not a plan is refused with a named reason; analyst output is schema-checked and 2–4 options are enforced;
Jev's scores rank, an unknown Jev label cannot lead, and a silent Jev degrades to a labelled composite order;
credentials seal/open/mask and a blank field never wipes a stored key.
- **One live run** against the user's real analyst endpoint and a real Jev key: every SSE stage fires, several
candidates come back, Jev returns a verdict with confidence, and OpenUI Lang arrives in deltas.
- **Browser QA** (Chromium): signup → guide → settings → run → rendered report; Copy puts SQL on the clipboard;
print emulation flips to paper and hides the rail and buttons; rename sticks and delete confirms first; no
horizontal scroll from 320 px up.
- **A smoke script** (`scripts/smoke.sh`) that a container or a plain `npm start` both pass: health, auth pages,
signed-out redirect, and `/api/analyze` refusing an anonymous caller.
- **A goal gate**: Jev scores the written plan against the goal in this document and answers the named-gap question
with `nothing` before implementation starts.
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-16"?>
<ShowPlanXML xmlns="http://schemas.microsoft.com/sqlserver/2004/07/showplan" Version="1.6" Build="16.0.4085.2">
<BatchSequence><Batch><Statements>
<StmtSimple StatementId="1" StatementSubtreeCost="4.871" StatementOptmLevel="FULL"
StatementText="SELECT o.Id FROM Sales.Orders o WHERE o.CustomerId = @c1 AND o.Total &gt; 500">
<StatementOptmEarlyAbortReason>GoodEnoughPlanFound</StatementOptmEarlyAbortReason>
<QueryBlock QueryHash="0x1A" QueryPlanHash="0x2B">
<RelOp NodeId="0" PhysicalOp="Clustered Index Scan" LogicalOp="Clustered Index Scan" EstimateRows="1000"
EstimateIO="41.6" EstimateCPU="39.0" EstimatedTotalSubtreeCost="4.10" Parallel="false">
<RunTimeInformation><RunTimeCountersPerThread Thread="0" ActualRows="1243500" ActualExecs="1"
ActualElapsedms="2411" ActualCPUms="2210"/></RunTimeInformation>
<IndexScan Database="[wide]" Schema="[Sales]" Table="[Orders]"/>
<Warnings><NoJoinPredicate/><SpillToTempDb SpillLevel="2"/>
<WaitTime WaitType="PAGEIOLATCH_SH" WaitTime="812"/></Warnings>
</RelOp>
<RelOp NodeId="1" PhysicalOp="Index Seek" LogicalOp="Index Seek" EstimateRows="3"
EstimateIO="0.7" EstimateCPU="0.02" EstimatedTotalSubtreeCost="0.77" Parallel="true">
<RunTimeInformation><RunTimeCountersPerThread Thread="0" ActualRows="4" ActualExecs="2"/></RunTimeInformation>
<IndexScan Database="[wide]" Schema="[Sales]" Table="[Customer]"/>
</RelOp>
<MissingIndexes><MissingIndexGroup Impact="94.17">
<MissingIndex Database="[wide]" Schema="[Sales]" Table="[Orders]">
<ColumnGroup Usage="EQUALITY"><Column Name="[CustomerId]"/></ColumnGroup>
<ColumnGroup Usage="INEQUALITY"><Column Name="[Total]"/></ColumnGroup>
<IncludedColumns><Column Name="[Id]"/></IncludedColumns>
</MissingIndex></MissingIndexGroup></MissingIndexes>
<NonParallelPlanReason>Could Not Generate Valid Parallel Plan</NonParallelPlanReason>
</QueryBlock>
</StmtSimple>
</Statements></Batch></BatchSequence>
</ShowPlanXML>
+9 -4
View File
@@ -2,7 +2,7 @@
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { userByEmail, getSettings, renameAnalysis, deleteAnalysis, db } from "@/lib/db";
import { signUpNewUser, verifyPassword, startSession, endSession, requireUser, emailOk, hashPassword } from "@/lib/auth";
import { signUpNewUser, verifyPassword, startSession, endSession, requireUser, changeEmail, emailOk, hashPassword } from "@/lib/auth";
import { saveSettingsForm } from "@/lib/settings";
export async function signUp(_: unknown, fd: FormData) {
@@ -36,13 +36,18 @@ export async function remove(_: unknown, fd: FormData) {
export async function saveSettingsAction(_: unknown, fd: FormData) {
const u = await requireUser();
const email = String(fd.get("email") ?? "").trim().toLowerCase();
const email = String(fd.get("email") ?? "").trim();
const password = String(fd.get("password") ?? "");
if (email && !emailOk(email)) return { ok: false, error: "Enter a valid email address." };
if (password && password.length < 10) return { ok: false, error: "Use at least 10 characters." };
saveSettingsForm(u.id, fd);
// The wizard sends `after`: a configured user must land on the readiness screen, and the setup page
// forwards configured users away from steps 1-2, so "Saved." would never be seen there.
if (String(fd.get("after") ?? "").startsWith("/setup")) redirect("/setup?step=3");
if (password) db.prepare("UPDATE users SET pass = ? WHERE id = ?").run(hashPassword(password), u.id);
if (email) db.prepare("UPDATE users SET email = ? WHERE id = ?").run(email, u.id);
if (email) {
const r = changeEmail(u.id, email);
if (!r.ok) return r;
}
revalidatePath("/settings");
return { ok: true };
}
+24 -3
View File
@@ -1,5 +1,6 @@
import { z } from "zod/v4";
import { digestForModel, type Digest } from "./digest.ts";
import { openAiTextDeltas } from "./stream.ts";
import type { AnalystConfig } from "./settings.ts";
export class AnalystError extends Error {}
@@ -36,11 +37,11 @@ export function extractJson(text: string): unknown {
}
export async function proposeCandidates(cfg: AnalystConfig, digest: Digest, note: string,
fetchImpl: typeof fetch = fetch): Promise<Candidate[]> {
fetchImpl: typeof fetch = fetch, signal?: AbortSignal): Promise<Candidate[]> {
const res = await fetchImpl(`${cfg.baseUrl}/chat/completions`, {
method: "POST",
method: "POST", signal,
headers: { "content-type": "application/json", authorization: `Bearer ${cfg.apiKey}` },
body: JSON.stringify({ model: cfg.model, temperature: 0.6, ...cfg.extra, stream: false,
body: JSON.stringify({ model: cfg.model, temperature: 0.6, max_tokens: 2048, ...cfg.extra, stream: false,
messages: [{ role: "system", content: CANDIDATE_SYSTEM_PROMPT },
{ role: "user", content: JSON.stringify({ digest: digestForModel(digest), user_note: note || null }) }] }),
}).catch((e) => { throw new AnalystError(`Could not reach ${cfg.baseUrl}: ${(e as Error).message}`); });
@@ -56,3 +57,23 @@ export async function proposeCandidates(cfg: AnalystConfig, digest: Digest, note
}
return parsed.data.candidates;
}
export const REPORT_SYSTEM_PROMPT = (openuiPrompt: string) => `${openuiPrompt}
You are writing the final answer for a DBA holding one SQL Server execution plan. The ranking you are given was
computed by Jev from typed judgments. It is not your opinion: do not re-rank it, soften it, or hide it. Quote each
option's scores as given. A rewrite must return the same rows as the original — when in doubt keep the original text
and say why. Never invent a number that is not in the input.`;
export async function* streamReport(cfg: AnalystConfig, payload: unknown, openuiPrompt: string,
fetchImpl: typeof fetch = fetch, signal?: AbortSignal): AsyncGenerator<string> {
const res = await fetchImpl(`${cfg.baseUrl}/chat/completions`, {
method: "POST", signal,
headers: { "content-type": "application/json", authorization: `Bearer ${cfg.apiKey}` },
body: JSON.stringify({ model: cfg.model, temperature: 0.3, max_tokens: 2048, ...cfg.extra, stream: true,
messages: [{ role: "system", content: REPORT_SYSTEM_PROMPT(openuiPrompt) },
{ role: "user", content: JSON.stringify(payload) }] }),
}).catch((e) => { throw new AnalystError(`Could not reach ${cfg.baseUrl}: ${(e as Error).message}`); });
if (!res.ok || !res.body) throw new AnalystError(`Analyst model returned HTTP ${res.status} while writing the report.`);
yield* openAiTextDeltas(res);
}
+11 -1
View File
@@ -1,5 +1,5 @@
import { randomBytes, scryptSync, createHash, timingSafeEqual } from "node:crypto";
import { createUser, userByEmail, userEmail, putSession, sessionUserId, dropSession, getSettings } from "./db.ts";
import { createUser, userByEmail, userEmail, putSession, sessionUserId, dropSession, getSettings, db } from "./db.ts";
export const SESSION_COOKIE = "qai_s";
export const SESSION_DAYS = 30;
@@ -34,6 +34,16 @@ export function signUpNewUser(email: string, password: string): { ok: true; id:
// request-scoped binding must not be touched at import time.
const jar = async () => (await import("next/headers")).cookies();
export function changeEmail(userId: string, email: string): { ok: true } | { ok: false; error: string } {
const e = email.trim().toLowerCase();
if (!emailOk(e)) return { ok: false, error: "Enter a valid email address." };
const taken = userByEmail(e);
if (taken && taken.id !== userId) return { ok: false, error: "That email already has an account." };
// users.email is UNIQUE: without this check a collision is a 500 inside the settings form.
if (!taken) db.prepare("UPDATE users SET email = ? WHERE id = ?").run(e, userId);
return { ok: true };
}
export async function startSession(userId: string) {
const token = randomBytes(32).toString("hex");
putSession(hashToken(token), userId, Date.now() + SESSION_DAYS * 864e5);
+25 -6
View File
@@ -5,9 +5,15 @@ import { randomUUID } from "node:crypto";
const dir = process.env.DATA_DIR ?? "data";
mkdirSync(dir, { recursive: true });
const g = globalThis as { __qaiDb?: DatabaseSync };
export const db = (g.__qaiDb ??= (() => {
// Open on first query, not at import: `next build` imports every page through lib/auth -> lib/db in ~6 parallel
// workers, and six processes running DDL against one file at once fails with "database is locked".
let _db: DatabaseSync | null = null;
function openDb(): DatabaseSync {
if (_db) return _db;
const d = new DatabaseSync(`${dir}/qai.db`);
d.exec(`
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;`);
d.exec(`
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS users (
@@ -25,12 +31,24 @@ export const db = (g.__qaiDb ??= (() => {
status TEXT NOT NULL DEFAULT 'running', error TEXT,
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);
CREATE INDEX IF NOT EXISTS ix_analyses_user ON analyses(user_id, created_at DESC);`);
_db = d;
return d;
})());
}
// Proxy keeps the terse call sites (db.prepare) while deferring the open; methods must be bound to the real
// instance or node:sqlite rejects the receiver ("Illegal invocation").
export const db: DatabaseSync = new Proxy({} as DatabaseSync, {
get: (_t, k) => {
const v = (openDb() as any)[k];
return typeof v === "function" ? v.bind(openDb()) : v;
},
});
const now = () => Date.now();
type Row = string | number | null;
const one = <T>(sql: string, ...p: Row[]): T | null => (db.prepare(sql).get(...p) as T | undefined) ?? null;
// node:sqlite hands back null-prototype objects. React refuses to serialize those into a client component
// ("Only plain objects ... can be passed to Client Components"), so every row is spread back to a normal object here.
const plain = <T>(r: unknown): T | null => (r ? { ...r } as T : null);
const one = <T>(sql: string, ...p: Row[]): T | null => plain<T>(db.prepare(sql).get(...p));
// node:sqlite binds null/number/string/bigint/Buffer only: pass 0/1, never booleans.
export type SettingsInput = { analyst_base_url: string; analyst_key: string; analyst_model: string;
@@ -91,8 +109,9 @@ export function patchAnalysis(id: string, p: Record<string, string>) {
.run(...keys.map((k) => p[k]), now(), id);
}
export const listAnalyses = (userId: string) =>
db.prepare("SELECT id, title, status, created_at FROM analyses WHERE user_id = ? ORDER BY created_at DESC")
.all(userId) as { id: string; title: string; status: string; created_at: number }[];
(db.prepare("SELECT id, title, status, created_at FROM analyses WHERE user_id = ? ORDER BY created_at DESC")
.all(userId) as { id: string; title: string; status: string; created_at: number }[])
.map((r) => ({ ...r }));
export const getAnalysis = (id: string, userId: string) =>
one<Analysis>("SELECT * FROM analyses WHERE id = ? AND user_id = ?", id, userId);
export const renameAnalysis = (id: string, userId: string, title: string) =>
+108
View File
@@ -0,0 +1,108 @@
import { TypeSafeClient, score, noul, choice } from "@typesafe-ai/sdk";
import { digestForModel, type Digest } from "./digest.ts";
import type { Candidate } from "./analyst.ts";
export type Dim = "bottleneck_fit" | "semantic_safety" | "ease" | "root_cause";
export const WEIGHTS: Record<Dim, number> = { bottleneck_fit: 0.4, semantic_safety: 0.3, ease: 0.15, root_cause: 0.15 };
const SAFETY_FLOOR = 0.6;
const SAFETY_HARD_FLOOR = 0.45;
const CONFIDENCE_FLOOR = 0.35;
const BOTTLENECK = [
"Unrelated to the costly operator or the worst estimate error in the plan.",
"Touches a minor operator; the dominant cost is untouched.",
"Partly addresses the dominant cost; a scan or spill probably remains.",
"Directly removes the dominant cost for this query shape.",
"Removes the dominant cost and the pattern behind it, so the shape itself changes.",
] as const;
const SAFETY = [
"Changes results: a predicate, join, grouping, or NULL handling is altered.",
"Likely changes results in edge cases: duplicates, ordering, tie-breaking.",
"Equivalent on most data but leans on unstated assumptions.",
"Clearly equivalent, with a stated reason.",
"Equivalent and checkable: ships a comparison or rollback plan.",
] as const;
const EASE = [
"Needs a schema plus application redesign and a release.",
"Needs an application deploy with change control on the app tier.",
"DBA work with approval, e.g. an index create during a window.",
"One rewritten query or session setting, reviewable in minutes.",
"Copy-paste change with no server-side change at all.",
] as const;
export type Ranked = { key: string; dims: Record<Dim, { value: number; confidence: number | null }>;
composite: number; flags: string[] };
export type Verdict = { order: Ranked[]; headline: string; source: "jev" | "composite"; jev_pick: string | null;
jev_confidence: number; agrees: boolean; anything_worth_running: number; weights: Record<Dim, number>; flags: string[] };
export const makeJevClient = (apiKey: string, model = "jev-latest") =>
new TypeSafeClient({ apiKey, defaultModel: model, timeout: 20_000 });
const norm = (s: number, levels: number) => Math.max(0, Math.min(1, s / (levels - 1)));
function rank(c: Candidate, a: any): Ranked {
const dims = {
bottleneck_fit: { value: norm(a.bottleneck_fit.score, BOTTLENECK.length), confidence: a.bottleneck_fit.confidence },
semantic_safety: { value: norm(a.semantic_safety.score, SAFETY.length), confidence: a.semantic_safety.confidence },
ease: { value: norm(a.ease.score, EASE.length), confidence: a.ease.confidence },
root_cause: { value: a.root_cause.noul, confidence: null }, // noul reports no confidence
} as Record<Dim, { value: number; confidence: number | null }>;
const composite = (Object.keys(WEIGHTS) as Dim[]).reduce((s, k) => s + WEIGHTS[k] * dims[k].value, 0);
const confs = Object.values(dims).map((d) => d.confidence).filter((x): x is number => x !== null);
const flags: string[] = [];
// Live Jev scores an index create around 0.57 ("probably equivalent, no proof stated"). A blanket floor
// flagged every index option, so the warning would be noise. Flag where semantics can actually move:
// any rewrite below the soft floor, or anything at all below the hard floor.
const safety = dims.semantic_safety.value;
if (safety < SAFETY_HARD_FLOOR || (c.rewrite_sql !== null && safety < SAFETY_FLOOR)) flags.push("verify_semantics");
if (confs.length && Math.min(...confs) < CONFIDENCE_FLOOR) flags.push("low_confidence");
if (c.actions.some((x) => x.requires_change_control)) flags.push("change_control");
return { key: c.key, dims, composite: Number(composite.toFixed(4)), flags };
}
export async function judgeCandidates(digest: Digest, candidates: Candidate[], client: TypeSafeClient): Promise<Verdict> {
const plan = digestForModel(digest);
const per = await Promise.all(candidates.map((c) => client.systemOne({
state: { plan, candidate: c },
questions: {
bottleneck_fit: score("How directly do the actions in `candidate` attack the dominant bottleneck in `plan` — the most expensive operator and the worst estimate error?", BOTTLENECK),
semantic_safety: score("If `candidate` is applied, how certain is it that the query still returns exactly the same rows as the original?", SAFETY),
ease: score("How easy is `candidate` to apply in a real shop, counting people, approvals, and downtime?", EASE),
root_cause: noul("The diagnosis in `candidate` names the actual cause of the cost in `plan`, not a symptom of it."),
},
})));
const cross = await client.systemOne({
state: { plan, options: candidates.map((c) => ({ key: c.key, title: c.title, diagnosis: c.diagnosis, expected: c.expected })) },
questions: {
first_to_run: choice("Which single entry in `options` should the DBA run first for the biggest drop in the cost shown in `plan`?",
Object.fromEntries(candidates.map((c) => [c.key, `${c.title} — ${c.expected}`]))),
anything_worth_running: noul("At least one entry in `options` would meaningfully reduce the cost shown in `plan`."),
},
});
const order = candidates.map((c, i) => rank(c, (per[i] as any).answers)).sort((a, b) => b.composite - a.composite);
const pick = (cross as any).answers.first_to_run;
const worth = (cross as any).answers.anything_worth_running.noul;
const valid = candidates.some((c) => c.key === pick.choice);
const useJev = valid && pick.confidence >= 0.5; // confidence-gated routing: act only when Jev is sure enough
const flags: string[] = [];
if (!useJev) flags.push(pick.confidence < CONFIDENCE_FLOOR ? "low_confidence" : "composite_order");
if (worth < 0.5) flags.push("nothing_clearly_worthwhile");
return { order, source: useJev ? "jev" : "composite", headline: useJev ? pick.choice : order[0].key,
jev_pick: valid ? pick.choice : null, jev_confidence: pick.confidence,
agrees: valid && pick.choice === order[0].key, anything_worth_running: worth, weights: WEIGHTS, flags };
}
// Neutral answers: Jev never spoke, so nothing is claimed about safety or fit. The UI must say so.
export function jevFallback(candidates: Candidate[], why: string): Verdict {
// Scores are 0 because Jev never answered, not because the options are bad: drop every flag a score produced.
const order = candidates.map((c) => rank(c, { bottleneck_fit: { score: 0, confidence: null },
semantic_safety: { score: 0, confidence: null }, ease: { score: 0, confidence: null },
root_cause: { noul: 0 } }))
.map((r) => ({ ...r, flags: r.flags.filter((f) => f === "change_control") }))
.sort((a, b) => b.composite - a.composite);
return { order, source: "composite", headline: order[0]?.key ?? "", jev_pick: null, jev_confidence: 0,
agrees: false, anything_worth_running: 0, weights: WEIGHTS, flags: ["jev_unavailable", why.slice(0, 200)] };
}
+81
View File
@@ -0,0 +1,81 @@
import { defineComponent, createLibrary } from "@openuidev/lang-core";
import { z } from "zod/v4";
// The single source of truth for what the analyst model is allowed to emit.
// This module must never import React or @openuidev/react-lang: the API route runs it in a plain
// Node runtime to build the prompt, and React's server build throws on createContext there.
// lib/report-ui.ts attaches the React renderers to these same specs, so prompt and renderer cannot drift.
const noop = () => null;
export const Headline = defineComponent({ props: z.object({ title: z.string(), subtitle: z.string().optional(),
severity: z.enum(["critical", "warning", "info"]).optional() }),
name: "Headline", description: "Report title: the one-line finding, with severity.", component: noop });
export const Metric = defineComponent({ props: z.object({ label: z.string(), value: z.string(), note: z.string().optional() }),
name: "Metric", description: "One plan metric.", component: noop });
export const MetricGrid = defineComponent({ props: z.object({ items: z.array(Metric.ref) }),
name: "MetricGrid", description: "Row of plan metrics.", component: noop });
export const Finding = defineComponent({ props: z.object({ where: z.string(), issue: z.string(), evidence: z.string(), impact: z.string() }),
name: "Finding", description: "One problem, the plan evidence for it, its impact.", component: noop });
export const Findings = defineComponent({ props: z.object({ items: z.array(Finding.ref) }),
name: "Findings", description: "Everything wrong with the plan.", component: noop });
export const Step = defineComponent({ props: z.object({ text: z.string(), effort: z.enum(["low", "medium", "high"]).optional(),
owner: z.enum(["dba", "developer", "both"]).optional(), changeControl: z.boolean().optional() }),
name: "Step", description: "One action step.", component: noop });
export const StepList = defineComponent({ props: z.object({ title: z.string(), steps: z.array(Step.ref) }),
name: "StepList", description: "Ordered action plan for one option.", component: noop });
export const SqlBlock = defineComponent({ props: z.object({ title: z.string(), sql: z.string(), caption: z.string().optional() }),
name: "SqlBlock", description: "A SQL snippet the user can copy.", component: noop });
export const ScoreBar = defineComponent({ props: z.object({ label: z.string(), value: z.number(),
confidence: z.number().optional(), note: z.string().optional() }),
name: "ScoreBar", description: "One Jev dimension: normalised value plus confidence.", component: noop });
export const OptionRow = defineComponent({ props: z.object({ rank: z.number(), title: z.string(), composite: z.number(),
headline: z.boolean(), flags: z.string().optional() }),
name: "OptionRow", description: "One ranked option.", component: noop });
export const OptionTable = defineComponent({ props: z.object({ title: z.string(), weights: z.string(), rows: z.array(OptionRow.ref) }),
name: "OptionTable", description: "All options ranked by composite score.", component: noop });
export const VerdictBlock = defineComponent({ props: z.object({ headline: z.string(), why: z.string(),
source: z.enum(["jev", "composite"]), confidence: z.number() }),
name: "VerdictBlock", description: "What Jev decided, and how sure it was.", component: noop });
export const Callout = defineComponent({ props: z.object({ level: z.enum(["info", "warn", "danger", "success"]),
title: z.string(), text: z.string() }),
name: "Callout", description: "A warning or note the user must not miss.", component: noop });
export const Report = defineComponent({ props: z.object({ blocks: z.array(z.union([Headline.ref, VerdictBlock.ref,
MetricGrid.ref, Findings.ref, OptionTable.ref, StepList.ref, SqlBlock.ref, ScoreBar.ref, Callout.ref])) }),
name: "Report", description: "Root. A query-plan report in reading order.", component: noop });
export const specs = [Headline, Metric, MetricGrid, Finding, Findings, Step, StepList, SqlBlock, ScoreBar,
OptionRow, OptionTable, VerdictBlock, Callout, Report];
/** Prompt-side library: same specs, no renderers. Safe in a non-React Node runtime. */
export const promptLibrary = createLibrary({ root: "Report", components: specs });
export const reportPromptOptions = {
preamble: "You output only OpenUI Lang. No prose, no markdown fences, no commentary before or after.",
additionalRules: [
"root = Report([...]) and it must list every block you define.",
"Block order: Headline, VerdictBlock, MetricGrid, Findings, OptionTable, then per option its StepList and SqlBlock, then ScoreBars, then Callout.",
"Use ONLY numbers present in the input. Never invent a cost, row count, impact, or confidence.",
"One SqlBlock per rewrite, titled with that option's key. T-SQL only, no markdown.",
"Every option flagged verify_semantics gets a Callout level danger: compare result rows before deploying.",
"If verdict.source is composite, say plainly that Jev did not decide.",
],
examples: [`root = Report([head, verdict])
head = Headline("Clustered Index Scan on Sales.Orders is 84% of the plan", "No usable key on CustomerId", "critical")
verdict = VerdictBlock("Create the covering index", "bottleneck_fit 1.00, semantic_safety 1.00, ease 0.50.", "jev", 0.82)`],
};
export const reportPrompt = () => promptLibrary.prompt(reportPromptOptions);
+90
View File
@@ -0,0 +1,90 @@
import { createElement as el } from "react";
import { defineComponent, createLibrary } from "@openuidev/react-lang";
import { specs } from "./report-spec.ts";
// Props, descriptions and the prompt come from lib/report-spec.ts (React-free, usable in the API route).
// This file only says what each component looks like. Names and schemas cannot drift: they are imported.
// No JSX here either — `node --test` imports modules and Node does not strip JSX.
const p = (style: Record<string, string | number>, ...kids: any[]) => el("p", { style }, ...kids);
const muted = { color: "var(--qai-ink-muted)" };
const small = { fontSize: "var(--qai-text-xs)", color: "var(--qai-ink-muted)" };
const disp = { fontFamily: "var(--qai-font-display)" };
// Model output is untrusted: the schema says a number is optional, models still send null, and one throw
// deletes the whole block from the report. 0 is the honest stand-in for "the model did not give a number".
const num = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
const conf = (v: unknown) => (typeof v === "number" ? ` ${v.toFixed(2)}` : " not reported");
const renderers: Record<string, (x: any) => any> = {
Headline: ({ props }) => el("header", null,
el("p", { className: "label" }, props.severity === "critical" ? "Critical" : props.severity === "warning" ? "Warning" : "Note"),
el("h2", { style: { ...disp, fontSize: "var(--qai-text-2xl)", margin: "var(--qai-space-s) 0" } }, props.title),
props.subtitle ? p(muted, props.subtitle) : null),
Metric: ({ props }) => el("div", { className: "card" },
el("p", { className: "label" }, props.label),
p({ ...disp, fontSize: "var(--qai-text-xl)" }, props.value),
props.note ? p(small, props.note) : null),
MetricGrid: ({ props, renderNode }: any) => el("div", { className: "grid-metrics" }, renderNode(props.items)),
Finding: ({ props }) => el("div", { className: "card" },
el("p", { className: "label" }, props.where),
p({ margin: "var(--qai-space-s) 0" }, el("strong", null, props.issue)),
el("p", { className: "sql" }, props.evidence),
p(muted, props.impact)),
Findings: ({ props, renderNode }: any) => el("section", { className: "report" }, renderNode(props.items)),
Step: ({ props }) => el("li", { className: "card", style: { display: "flex", gap: "var(--qai-space-md)",
alignItems: "center", flexWrap: "wrap" } },
el("span", { style: { flex: "1 1 200px", minWidth: 0 } }, props.text),
props.effort ? el("span", { className: "pill" }, `effort ${props.effort}`) : null,
props.owner ? el("span", { className: "pill" }, props.owner) : null,
props.changeControl ? el("span", { className: "pill" }, "change control") : null),
StepList: ({ props, renderNode }: any) => el("section", null,
el("h3", { style: disp }, props.title),
el("ol", { style: { listStyle: "none", padding: 0, margin: 0, display: "grid", gap: "var(--qai-space-md)" } },
renderNode(props.steps))),
SqlBlock: ({ props }) => el("figure", { className: "card", style: { margin: 0 } },
el("figcaption", { className: "label" }, props.title),
el("pre", { className: "sql" }, props.sql),
el("button", { className: "btn no-print", onClick: () => navigator.clipboard.writeText(props.sql) }, "Copy SQL"),
props.caption ? el("p", { style: small }, props.caption) : null),
ScoreBar: ({ props }) => el("div", null,
el("p", { className: "label" }, props.label + (props.confidence === undefined
? " · no confidence reported" : ` · confidence${conf(props.confidence)}`)),
el("div", { className: "bar", style: { width: `${Math.round(num(props.value) * 100)}%` } }),
props.note ? el("p", { style: small }, props.note) : null),
OptionRow: ({ props }) => el("tr", { "data-headline": props.headline || undefined },
el("td", null, String(props.rank)), el("td", null, props.title),
el("td", { className: "sql" }, num(props.composite).toFixed(2)),
el("td", null, props.flags || "—")),
OptionTable: ({ props, renderNode }: any) => el("section", { className: "card" },
el("h3", { style: disp }, props.title),
el("p", { style: small }, props.weights),
el("table", { style: { width: "100%", borderCollapse: "collapse" } }, el("tbody", null, renderNode(props.rows)))),
VerdictBlock: ({ props }) => el("section", { className: "card", "data-verdict": "" },
el("p", { className: "label" }, "First move · " + (props.source === "jev"
? `Jev, confidence${conf(props.confidence)}` : "composite order — Jev did not decide")),
p({ ...disp, fontSize: "var(--qai-text-lg)" }, props.headline),
p({}, props.why)),
Callout: ({ props }) => el("div", { className: "card", "data-level": props.level,
role: props.level === "danger" ? "alert" : "note" },
el("p", null, el("strong", null, props.title)), el("p", null, props.text)),
Report: ({ props, renderNode }: any) => el("div", { className: "report", id: "report" }, renderNode(props.blocks)),
};
export const library = createLibrary({
root: "Report",
components: specs.map((s) => defineComponent({ name: s.name, props: s.props, description: s.description,
component: renderers[s.name] })),
});
+36
View File
@@ -0,0 +1,36 @@
export const sse = (event: string, data: unknown) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
export async function* readSse(body: ReadableStream<Uint8Array>): AsyncGenerator<{ event: string; data: any }> {
const reader = body.getReader(); const dec = new TextDecoder(); let buf = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
let at: number;
while ((at = buf.indexOf("\n\n")) !== -1) {
const frame = buf.slice(0, at); buf = buf.slice(at + 2);
const event = /^event: (.+)$/m.exec(frame)?.[1] ?? "message";
const raw = /^data: (.*)$/m.exec(frame)?.[1] ?? "";
try { yield { event, data: JSON.parse(raw) }; } catch { /* keep-alive or unparsable frame */ }
}
}
}
export async function* openAiTextDeltas(res: Response): AsyncGenerator<string> {
const reader = res.body!.getReader(); const dec = new TextDecoder(); let buf = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
let at: number;
while ((at = buf.indexOf("\n\n")) !== -1) {
const line = (buf.slice(0, at).split("\n").find((l) => l.startsWith("data:")) ?? "").slice(5).trim();
buf = buf.slice(at + 2);
if (!line) continue;
if (line === "[DONE]") return;
let delta: unknown;
try { delta = JSON.parse(line)?.choices?.[0]?.delta?.content; } catch { continue; }
if (typeof delta === "string" && delta) yield delta;
}
}
}
+47
View File
@@ -8,6 +8,7 @@
"name": "sql-server-qai",
"version": "0.1.0",
"dependencies": {
"@openuidev/lang-core": "^0.3.0",
"@openuidev/react-lang": "^0.3.0",
"@typesafe-ai/sdk": "^0.6.0",
"fast-xml-parser": "^5.7.3",
@@ -17,6 +18,7 @@
"zod": "^4.6.5"
},
"devDependencies": {
"@playwright/test": "^1.63.0",
"@types/node": "^24.13.6",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -1282,6 +1284,22 @@
}
}
},
"node_modules/@playwright/test": {
"version": "1.63.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz",
"integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.63.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"dev": true,
@@ -4562,6 +4580,35 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/playwright": {
"version": "1.63.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
"integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.63.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/playwright-core": {
"version": "1.63.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
"integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"dev": true,
+4 -1
View File
@@ -7,9 +7,11 @@
"build": "next build",
"start": "next start",
"lint": "eslint",
"test": "node --no-warnings --test tests/*.test.ts"
"test": "node --no-warnings --test tests/*.test.ts",
"e2e": "playwright test"
},
"dependencies": {
"@openuidev/lang-core": "^0.3.0",
"@openuidev/react-lang": "^0.3.0",
"@typesafe-ai/sdk": "^0.6.0",
"fast-xml-parser": "^5.7.3",
@@ -19,6 +21,7 @@
"zod": "^4.6.5"
},
"devDependencies": {
"@playwright/test": "^1.63.0",
"@types/node": "^24.13.6",
"@types/react": "^19",
"@types/react-dom": "^19",
+28
View File
@@ -0,0 +1,28 @@
import { defineConfig, devices } from "@playwright/test";
import { readFileSync, existsSync, mkdirSync } from "node:fs";
// Playwright does not load Next's .env.local; the E2E run needs the real Jev key and a stable APP_SECRET.
if (existsSync(".env.local")) for (const line of readFileSync(".env.local", "utf8").split("\n")) {
const m = /^([A-Z_]+)=(.*)$/.exec(line.trim());
if (m && !process.env[m[1]]) process.env[m[1]] = m[2];
}
mkdirSync(".auth", { recursive: true });
export default defineConfig({
testDir: "./tests-e2e",
fullyParallel: false, // one SQLite file, one app process
workers: 1,
retries: 0,
timeout: 180_000, // real models are slow; say so with a timeout, not with retries
reporter: [["list"]],
use: { baseURL: "http://localhost:3000", trace: "retain-on-failure" },
projects: [
{ name: "setup", testMatch: /auth\.setup\.ts/ },
{ name: "chromium", use: { ...devices["Desktop Chrome"] }, dependencies: ["setup"] },
// A real model run costs ~40s; the mobile project only needs the layout checks, not a second full run.
{ name: "mobile", use: { ...devices["Pixel 7"] }, dependencies: ["setup"], testMatch: /responsive\.spec\.ts/ },
],
webServer: { command: "npm run dev", url: "http://localhost:3000/api/healthz", reuseExistingServer: true,
timeout: 120_000,
env: { DATA_DIR: ".playwright-data", APP_SECRET: process.env.APP_SECRET ?? "playwright-secret-value" } },
});
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# The one command that proves the app answers. Two modes, same checks:
# bash scripts/smoke.sh # app already running (BASE=http://localhost:3000)
# bash scripts/smoke.sh --docker # via docker compose (Docker is not installed on this machine)
set -uo pipefail
BASE=${BASE:-http://localhost:3000}
fail=0
check() { # check <label> <expected> <actual>
if [ "$2" = "$3" ]; then echo "ok $1"; else echo "FAIL $1: expected [$2] got [$3]"; fail=1; fi
}
if [ "${1:-}" = "--docker" ]; then
docker compose up --build -d
trap 'docker compose down -v' EXIT
for _ in $(seq 1 40); do curl -fsS "$BASE/api/healthz" >/dev/null 2>&1 && break; sleep 2; done
fi
check "healthz" '{"ok":true}' "$(curl -fsS "$BASE/api/healthz" 2>/dev/null)"
check "login page" "200" "$(curl -fsS -o /dev/null -w '%{http_code}' "$BASE/login")"
check "signup page" "200" "$(curl -fsS -o /dev/null -w '%{http_code}' "$BASE/signup")"
check "signed-out /app redirects" "307" "$(curl -s -o /dev/null -w '%{http_code}' "$BASE/app")"
check "analyze needs a session" "401" "$(curl -s -o /dev/null -w '%{http_code}' -X POST -H 'content-type: application/json' -d '{}' "$BASE/api/analyze")"
[ $fail -eq 0 ] && echo "smoke ok" || { echo "smoke FAILED"; exit 1; }
+50
View File
@@ -0,0 +1,50 @@
import { test, expect } from "@playwright/test";
import { readFileSync } from "node:fs";
const xml = readFileSync("fixtures/sample.sqlplan", "utf8");
test.use({ storageState: ".auth/user.json", permissions: ["clipboard-read", "clipboard-write"] });
test("full run: paste → stages → streamed report → copy → print", async ({ page }) => {
await page.goto("/app");
await page.fill("#xml", xml);
await page.fill("#note", "slow since the migration");
await page.getByRole("button", { name: "Analyse" }).click();
// Stages are named one at a time; the whole run takes tens of seconds with a real model.
await expect(page.getByText(/1 of 4 · Reading the plan/)).toBeVisible({ timeout: 20_000 });
await expect(page.getByText(/4 of 4 · Writing the report/)).toBeVisible({ timeout: 180_000 });
// The report paints while oul is still arriving, so the headline shows up first.
await expect(page.locator("#report h2").first()).toBeVisible({ timeout: 180_000 });
// The Analyse button only unlocks on the done frame: by then the whole report has arrived and parsed.
await expect(page.getByRole("button", { name: "Analyse" })).toBeEnabled({ timeout: 180_000 });
expect(await page.locator("#report .card").count()).toBeGreaterThan(2);
// No raw OpenUI Lang leaked into the page as text.
expect(await page.locator("main").innerText()).not.toContain("root = Report(");
await page.getByRole("button", { name: "Copy", exact: true }).click();
const clip = await page.evaluate(() => navigator.clipboard.readText());
expect(clip).toContain("slow since the migration");
await page.getByRole("button", { name: "Export to PDF" }).click(); // window.print()
await page.emulateMedia({ media: "print" });
await expect(page.locator(".rail")).toBeHidden();
await expect(page.locator(".toolbar")).toBeHidden();
});
test("a dropped .sqlplan file decodes and is accepted", async ({ page }) => {
await page.goto("/app");
await page.setInputFiles('input[type="file"]', "fixtures/sample.sqlplan");
const value = await page.locator("#xml").inputValue();
expect(value).toContain("ShowPlanXML");
expect(value).not.toContain("\u0000");
expect(value).not.toContain("\ufeff");
});
test("a pasted SQL script is refused with a named reason", async ({ page }) => {
await page.goto("/app");
await page.fill("#xml", "SELECT * FROM dbo.Orders WHERE CustomerId = 42;");
await page.getByRole("button", { name: "Analyse" }).click();
await expect(page.getByRole("alert").filter({ hasText: "digesting" })).toBeVisible({ timeout: 30_000 });
await expect(page.locator("#report")).toHaveCount(0);
});
+34
View File
@@ -0,0 +1,34 @@
import { test, expect } from "@playwright/test";
const EMAIL = `qa+${Date.now()}@qai.test`;
test("signup + first-run guide configures both models", async ({ page }) => {
await page.goto("/signup");
await page.fill("#email", EMAIL);
await page.fill("#password", "playwright password");
await page.getByRole("button", { name: "Create account" }).click();
await expect(page.getByText("Step 1 of 3")).toBeVisible();
await page.getByRole("link", { name: "Next" }).click();
await expect(page.getByText("Step 2 of 3")).toBeVisible();
await page.fill("#analyst_base_url", process.env.QAI_ANALYST_BASE_URL ?? "http://192.168.88.131:8888/v1");
await page.fill("#analyst_key", process.env.QAI_ANALYST_KEY ?? "EMPTY");
await page.fill("#analyst_model", process.env.QAI_ANALYST_MODEL ?? "qwen3.8-flash-next");
// Reasoning models stream thinking, not content: without this the report arrives empty.
await page.fill("#analyst_extra", process.env.QAI_ANALYST_EXTRA ?? '{"chat_template_kwargs":{"enable_thinking":false}}');
await page.fill("#jev_key", process.env.JEV_API_KEY ?? "");
await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByText("Step 3 of 3")).toBeVisible({ timeout: 15_000 });
await expect(page.getByText(/Analyst ready · Jev ready/)).toBeVisible();
await expect(page.getByRole("link", { name: "Go" })).toBeVisible();
await page.context().storageState({ path: ".auth/user.json" });
});
test("signed-out visitor is pushed to login", async ({ browser }) => {
const ctx = await browser.newContext();
const page = await ctx.newPage();
await page.goto("/app");
expect(page.url()).toContain("/login");
await ctx.close();
});
+38
View File
@@ -0,0 +1,38 @@
import { test, expect } from "@playwright/test";
import { DatabaseSync } from "node:sqlite";
import { randomUUID } from "node:crypto";
test.use({ storageState: ".auth/user.json" });
const DB = ".playwright-data/qai.db";
// The app opens this file too (WAL), so writes here are just another connection.
function withDb<T>(fn: (d: DatabaseSync) => T): T {
const d = new DatabaseSync(DB);
try { return fn(d); } finally { d.close(); }
}
test("a row left running renders what exists instead of redirecting forever", async ({ page }) => {
const id = withDb((d) => {
const userId = d.prepare("SELECT id FROM users ORDER BY rowid DESC LIMIT 1").get() as { id: string };
const aid = randomUUID();
// Backdated so it never becomes the rail's first row: history.spec.ts depends on that row being a real run.
d.prepare("INSERT INTO analyses (id, user_id, title, xml, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?)")
.run(aid, userId.id, "Orphaned run", "<ShowPlanXML/>", "running", Date.now() - 3_600_000, Date.now() - 3_600_000);
return aid;
});
await page.goto(`/app/${id}`);
await expect(page.getByText(/This run was interrupted before it finished/)).toBeVisible({ timeout: 15_000 });
expect(page.url()).toContain(`/app/${id}`); // the old redirect sent this to itself
await expect(page.getByRole("button", { name: "Analyse" })).toHaveCount(0);
});
test("an email another account already has is refused inline", async ({ page }) => {
withDb((d) => d.prepare("INSERT INTO users (id, email, pass, created_at) VALUES (?,?,?,?)")
.run(randomUUID(), "taken@qai.test", "salt:hash", Date.now()));
await page.goto("/settings");
await page.fill("#email", "taken@qai.test");
await page.getByRole("button", { name: "Save" }).click();
await expect(page.locator("form p[role=alert]")).toContainText("That email already has an account", { timeout: 15_000 });
const still = withDb((d) => (d.prepare("SELECT email FROM users WHERE email = ?").get("taken@qai.test") as { email: string }).email);
expect(still).toBe("taken@qai.test");
});
+31
View File
@@ -0,0 +1,31 @@
import { test, expect } from "@playwright/test";
test.use({ storageState: ".auth/user.json" });
test("rename sticks, delete asks first then removes", async ({ page }) => {
await page.goto("/app");
const row = page.locator(".rail li").first();
await row.locator('input[name="title"]').fill("Renamed by QA");
await row.getByRole("button", { name: "Save" }).click();
await expect(page.locator(".rail", { hasText: "Renamed by QA" })).toBeVisible({ timeout: 15_000 });
const before = await page.locator(".rail li").count();
let asked = false;
page.once("dialog", async (d) => { asked = true; await d.dismiss(); }); // refuse: nothing may vanish
await page.locator(".rail li").first().getByRole("button", { name: "Delete" }).click();
await page.waitForTimeout(1000);
expect(asked).toBe(true);
expect(await page.locator(".rail li").count()).toBe(before);
page.once("dialog", async (d) => { await d.accept(); });
await page.locator(".rail li").first().getByRole("button", { name: "Delete" }).click();
await expect.poll(async () => page.locator(".rail li").count(), { timeout: 20_000 }).toBe(before - 1);
});
test("reopening a stored run renders the report with no new model call", async ({ page }) => {
await page.goto("/app");
await page.locator('.rail a[href^="/app/"]').first().click();
await expect(page.locator("#report")).toBeVisible({ timeout: 30_000 });
await expect(page.getByRole("button", { name: "Analyse" })).toHaveCount(0);
// The stored Jev verdict is server-rendered, so it is readable even if the report body had failed.
await expect(page.getByText(/Jev verdict ·/)).toBeVisible();
});
+26
View File
@@ -0,0 +1,26 @@
import { test, expect } from "@playwright/test";
test.use({ storageState: ".auth/user.json" });
for (const width of [320, 375, 414, 768]) {
test(`no horizontal scroll at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 800 });
await page.goto("/app");
const over = await page.evaluate(() => document.documentElement.scrollWidth
- document.documentElement.clientWidth);
expect(over).toBeLessThanOrEqual(0);
const box = await page.getByRole("button", { name: "Analyse" }).boundingBox();
// .btn is 10px padding + 15px line = 35px at these widths; 32 leaves room for subpixel rounding
// while still failing a squashed or two-line control.
expect(box!.height).toBeGreaterThanOrEqual(32);
});
}
test("every settings field has a visible label", async ({ page }) => {
await page.goto("/settings");
// Scoped to the form: the rail's rename boxes are labelled for assistive tech with aria-label, not visibly.
const orphans = await page.evaluate(() => [...document.querySelectorAll("form input")]
.filter((el) => { const i = el as HTMLInputElement;
return i.type !== "hidden" && (!i.id || !document.querySelector(`label[for="${i.id}"]`)); })
.map((el) => (el as HTMLInputElement).name || (el as HTMLInputElement).type));
expect(orphans).toEqual([]);
});
+13
View File
@@ -18,3 +18,16 @@ test("token hash is stable, 64 hex", () => {
assert.equal(hashToken("abc"), hashToken("abc"));
assert.match(hashToken("abc"), /^[0-9a-f]{64}$/);
});
test("changeEmail refuses an address another account already has", async () => {
const { signUpNewUser, changeEmail } = await import("../lib/auth.ts");
const db = await import("../lib/db.ts");
const a = signUpNewUser("first@x.test", "0123456789ab");
const b = signUpNewUser("second@x.test", "0123456789ab");
assert.ok(a.ok && b.ok);
assert.deepEqual(changeEmail(b.id, "first@x.test"), { ok: false, error: "That email already has an account." });
assert.deepEqual(changeEmail(b.id, "not an email"), { ok: false, error: "Enter a valid email address." });
assert.deepEqual(changeEmail(b.id, " FIRST@X.TEST "), { ok: false, error: "That email already has an account." });
assert.equal(changeEmail(a.id, "first@x.test").ok, true, "own address is not a conflict");
assert.equal(changeEmail(b.id, "fresh@x.test").ok, true);
assert.equal(db.userByEmail("fresh@x.test")!.id, b.id, "stored lower-cased");
});
+11
View File
@@ -37,3 +37,14 @@ test("session resolves only while unexpired", () => {
q.dropSession("tok");
assert.equal(q.sessionUserId("tok"), null);
});
test("rows are plain objects so they can cross the server -> client boundary", () => {
const id = q.createUser("plain@x.y", "h");
const aid = q.newAnalysis(id, "t", "<xml/>");
const row = q.listAnalyses(id)[0];
assert.equal(Object.getPrototypeOf(row), Object.prototype, "listAnalyses returned a null-prototype row");
assert.equal(Object.getPrototypeOf(q.getAnalysis(aid, id)!), Object.prototype, "getAnalysis");
q.saveSettings(id, { analyst_base_url: "b", analyst_key: "k", analyst_model: "m", analyst_extra: "{}",
jev_key: "j", jev_model: "v", onboarded: 1 });
assert.equal(Object.getPrototypeOf(q.getSettings(id)!), Object.prototype, "getSettings");
});
+93
View File
@@ -0,0 +1,93 @@
import test from "node:test";
import assert from "node:assert/strict";
const { judgeCandidates, WEIGHTS, jevFallback } = await import("../lib/jev.ts");
const { TypeSafeClient } = await import("@typesafe-ai/sdk");
const digest = { sql: "SELECT", bytes: 1, subtreeCost: 1, parallel: false, nonParallelReason: null, earlyAbort: null,
tables: [], rowGuessErrors: [], missingIndexes: [], warnings: [], waits: [],
topOperators: [{ id: "0", op: "Clustered Index Scan", cost: 4.1, estRows: 1000, actualRows: 1200000,
execs: 1, io: 40, cpu: 39, spillLevels: 2, parallel: false }] } as any;
const candidates = [
{ key: "a", title: "Covering index", diagnosis: "Scan of a million rows; no key exists on CustomerId.",
actions: [{ title: "Create index", detail: "CREATE INDEX", effort: "medium", requires_change_control: true }],
rewrite_sql: null, expected: "seek instead of scan" },
{ key: "b", title: "Add TOP 100", diagnosis: "Too many rows leave the scan, so cut them off.",
actions: [{ title: "Add TOP 100", detail: "TOP 100", effort: "low", requires_change_control: false }],
rewrite_sql: "SELECT TOP 100", expected: "less I/O" },
] as any;
const scored = (fit: number, safe: number, ease: number, cause: number) => ({
bottleneck_fit: { type: "score", score: fit, confidence: 0.9, legend: {}, probabilities: {} },
semantic_safety: { type: "score", score: safe, confidence: 0.8, legend: {}, probabilities: {} },
ease: { type: "score", score: ease, confidence: 0.7, legend: {}, probabilities: {} },
root_cause: { type: "noul", noul: cause } });
// The SDK's client takes an injectable fetch — no network in tests.
function fakeJev(map: any) {
return new TypeSafeClient({ apiKey: "test", fetch: (async (_u: any, init: any) => {
const body = JSON.parse(init.body);
const answers = body.questions.first_to_run ? map.cross : map.byKey[body.state.candidate.key];
return new Response(JSON.stringify({ model: "jev-1.13.0", usage: { input_tokens: 1, output_tokens: 1 }, answers }),
{ status: 200, headers: { "content-type": "application/json" } });
}) as any, retry: { maxRetries: 0 } });
}
const GOOD = { byKey: { a: scored(4, 4, 2, 1), b: scored(0.4, 0.5, 4, 0.1) },
cross: { first_to_run: { type: "choice", choice: "a", confidence: 0.82, probabilities: { a: 0.82, b: 0.18 } },
anything_worth_running: { type: "noul", noul: 0.9 } } };
test("weights sum to 1", () => assert.equal(Object.values(WEIGHTS).reduce((a, b) => a + b, 0), 1));
test("jev picks the winner; the unsafe option is flagged and cannot lead", async () => {
const v = await judgeCandidates(digest, candidates, fakeJev(GOOD));
assert.equal(v.source, "jev");
assert.equal(v.headline, "a");
assert.equal(v.order[0].key, "a");
assert.equal(v.agrees, true);
assert.equal(v.jev_confidence, 0.82);
assert.ok(v.order.find((r) => r.key === "b")!.flags.includes("verify_semantics"));
assert.ok(v.order.find((r) => r.key === "a")!.flags.includes("change_control"));
assert.ok(v.order.find((r) => r.key === "a")!.dims.bottleneck_fit.value === 1);
});
test("a low-confidence jev pick defers to composite order", async () => {
const low = { byKey: GOOD.byKey, cross: { first_to_run: { type: "choice", choice: "b", confidence: 0.2,
probabilities: { a: 0.4, b: 0.6 } }, anything_worth_running: { type: "noul", noul: 0.4 } } };
const v = await judgeCandidates(digest, candidates, fakeJev(low));
assert.equal(v.source, "composite");
assert.equal(v.headline, "a");
assert.equal(v.agrees, false);
assert.ok(v.flags.includes("low_confidence"));
assert.ok(v.flags.includes("nothing_clearly_worthwhile"));
});
test("an unknown jev choice label falls back to composite order", async () => {
const bogus = { byKey: GOOD.byKey, cross: { first_to_run: { type: "choice", choice: "zzz", confidence: 0.99,
probabilities: { zzz: 0.99 } }, anything_worth_running: { type: "noul", noul: 0.9 } } };
const v = await judgeCandidates(digest, candidates, fakeJev(bogus));
assert.equal(v.source, "composite");
assert.equal(v.headline, "a");
assert.equal(v.jev_pick, null);
});
test("fallback ranks without Jev and says so", () => {
const v = jevFallback(candidates, "429 rate limited");
assert.equal(v.source, "composite");
assert.ok(v.flags.includes("jev_unavailable"));
assert.equal(v.order.length, 2);
});
test("a silent jev does not accuse every option of being unsafe", () => {
const v = jevFallback(candidates, "fetch failed");
// change_control is read off the candidate's own actions and stays; anything derived from a Jev score must go.
const derived = (flags: string[]) => flags.filter((f) => f !== "change_control");
assert.deepEqual(v.order.map((r) => derived(r.flags)), [[], []]);
});
test("verify_semantics fires where semantics can actually move", async () => {
// Real Jev scores an index create ~0.57 ("probably fine, no stated proof"), which a blanket <0.6 floor
// flagged as unsafe on every run. Index-only options must not shout; rewrites and bad scores must.
const idxOnly = [{ ...candidates[0], rewrite_sql: null }, candidates[1]];
const mid = { byKey: { a: scored(4, 2, 2, 1), b: scored(4, 2, 2, 1) }, cross: GOOD.cross };
const v = await judgeCandidates(digest, idxOnly, fakeJev(mid));
assert.ok(!v.order.find((r) => r.key === "a")!.flags.includes("verify_semantics"), "index create flagged");
assert.ok(v.order.find((r) => r.key === "b")!.flags.includes("verify_semantics"), "rewrite not flagged");
const bad = { byKey: { a: scored(4, 1, 2, 1), b: GOOD.byKey.b }, cross: GOOD.cross };
const v2 = await judgeCandidates(digest, idxOnly, fakeJev(bad));
assert.ok(v2.order.find((r) => r.key === "a")!.flags.includes("verify_semantics"), "bad score not flagged");
});
+28
View File
@@ -0,0 +1,28 @@
import test from "node:test";
import assert from "node:assert/strict";
const { promptLibrary, reportPromptOptions, specs } = await import("../lib/report-spec.ts");
test("the prompt advertises every component and the root", () => {
const p = promptLibrary.prompt(reportPromptOptions);
for (const n of ["Report", "Headline", "VerdictBlock", "MetricGrid", "Findings", "OptionTable",
"StepList", "SqlBlock", "ScoreBar", "Callout"]) assert.ok(p.includes(n), `missing ${n}`);
assert.match(p, /root/i);
assert.match(p, /never invent/i);
});
test("every spec has a renderer, so the model can never emit an unrenderable block", async () => {
const { library } = await import("../lib/report-ui.ts");
for (const s of specs) assert.ok(library.components[s.name], `no renderer for ${s.name}`);
});
test("a null confidence from the model does not blank the block", async () => {
// The prompt says optional; models still send null. confidence != undefined let null through to .toFixed().
const { library } = await import("../lib/report-ui.ts");
const renderNode = () => null;
for (const [name, props] of [
["ScoreBar", { label: "fit", value: 0.5, confidence: null }],
["VerdictBlock", { headline: "h", why: "w", source: "jev", confidence: null }],
] as const) {
const el = library.components[name].component({ props: props as any, renderNode } as any);
assert.ok(el, `${name} returned nothing`);
}
});
+35
View File
@@ -0,0 +1,35 @@
import test from "node:test";
import assert from "node:assert/strict";
const { sse, readSse, openAiTextDeltas } = await import("../lib/stream.ts");
const streamOf = (parts: string[]) => new ReadableStream<Uint8Array>({ start(c) {
parts.forEach((p) => c.enqueue(new TextEncoder().encode(p))); c.close(); } });
test("sse frames one event", () =>
assert.equal(sse("stage", { stage: "digesting" }), 'event: stage\ndata: {"stage":"digesting"}\n\n'));
test("readSse rejoins frames split anywhere", async () => {
const raw = sse("stage", { stage: "digesting" }) + sse("oul", { chunk: "root = Report([])" });
const events: any[] = [];
for await (const e of readSse(streamOf([raw.slice(0, 12), raw.slice(12, 40), raw.slice(40)]))) events.push(e);
assert.deepEqual(events, [{ event: "stage", data: { stage: "digesting" } },
{ event: "oul", data: { chunk: "root = Report([])" } }]);
});
test("readSse ignores frames it cannot parse", async () => {
const events: any[] = [];
for await (const e of readSse(streamOf([": ping\n\n", sse("done", { id: "1" })]))) events.push(e);
assert.deepEqual(events, [{ event: "done", data: { id: "1" } }]);
});
test("openAiTextDeltas yields content and stops at [DONE]", async () => {
const res = new Response(streamOf(['data: {"choices":[{"delta":{"content":"ab"}}]}\n\n',
'data: {"choices":[{"delta":{"content":"cd"}}]}\n\n', "data: [DONE]\n\n"]), { status: 200 });
const out: string[] = [];
for await (const d of openAiTextDeltas(res)) out.push(d);
assert.equal(out.join(""), "abcd");
});
test("openAiTextDeltas survives a delta split across chunks", async () => {
const res = new Response(streamOf(['data: {"choices":[{"delta":{"con', 'tent":"xy"}}]}\n\ndata: [DONE]\n\n']), { status: 200 });
const out: string[] = [];
for await (const d of openAiTextDeltas(res)) out.push(d);
assert.equal(out.join(""), "xy");
});