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>
This commit is contained in:
ahmed mohammed
2026-09-22 08:40:57 +03:00
co-authored by Claude Opus 4.7
parent a5529ce542
commit 0db7b8140b
6 changed files with 110 additions and 2 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
+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"]
+4 -2
View File
@@ -23,13 +23,15 @@ Getting the XML: SSMS → **Ctrl+M** (include actual execution plan) → run the
## Run it ## Run it
This repo runs as a plain Node process. Containerization is written (`Dockerfile`, Task 12 of the plan) but is not This repo runs as a plain Node process. `Dockerfile` and `docker-compose.yml` are ready, but the image has **not
required for local work — `npm run dev` is the normal path on a small machine. 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 ```bash
npm install npm install
cp .env.example .env.local # then fill in APP_SECRET and DATA_DIR cp .env.example .env.local # then fill in APP_SECRET and DATA_DIR
npm run dev # http://localhost:3000 npm run dev # http://localhost:3000
bash scripts/smoke.sh # healthz, login page, signed-out redirect, analyze needs a session
``` ```
`.env.local`: `.env.local`:
+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**. 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) ## Actors (two models, strict roles)
- **Analyst** — any OpenAI-compatible endpoint the user configures (`base_url`, - **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 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 top pick is defensible because Jev's probabilities are on screen, and the winning
SQL is one click from the clipboard. 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.
+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; }