docs: karpathy-pattern wiki built from the spec, plan, design contract and decisions ledger
kb/ is its own wiki root: 34 pages across 7 categories, index.md catalog, log.md, AGENTS.md schema layer, and raw/ holding copies of README, the spec, the 13-task plan, the design master and the SDD ledger. Every page cross-references 3 to 6 others and all 107 wikilinks resolve except two [["list"]] artefacts inside JSON snippets in the copied plan. Kept separate from the repository root on purpose: its generated graph lands in kb/.ua/ instead of overwriting the code graph. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
735a57bfd6
commit
8b42a0931c
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,114 @@
|
||||
# SQL Server qAI — Spec
|
||||
|
||||
**Date:** 2026-09-21
|
||||
**Owner:** ahmedmohammed
|
||||
**Status:** agreed in chat, this file is the spec of record
|
||||
|
||||
## Goal (verbatim from the owner)
|
||||
|
||||
> portal that take xml of sql server query plan and use power of generative ai to
|
||||
> analysis provide multiple action steps, multiple query rewrite if possible,
|
||||
> Jev then rank and decide.
|
||||
|
||||
## What it is
|
||||
|
||||
A containerised web tool. A DBA or consultant logs in, pastes a SQL Server
|
||||
execution plan (Showplan XML, or a `.sqlplan` file), and gets back:
|
||||
|
||||
1. What the plan says is wrong (bottleneck, missing indexes, spills, bad row guesses).
|
||||
2. **Multiple** candidate action plans — several different remediations, not one.
|
||||
3. **Multiple** candidate SQL rewrites where a rewrite is possible.
|
||||
4. A **Jev**-ranked verdict: which candidate to run first, with typed probabilities
|
||||
and confidence, and why.
|
||||
5. One final "best fast path": the recommended SQL and the steps to apply it.
|
||||
6. The result rendered as **structured UI elements** (OpenUI Lang → React), not a
|
||||
wall of markdown. Export to PDF, copy to clipboard.
|
||||
|
||||
The demo value is Jev as the decision-maker: the generative model proposes,
|
||||
**Jev disposes**. Every ranking number on screen is a typed judgment from
|
||||
`POST /v1/systemone`, not a string the LLM wrote.
|
||||
|
||||
## Product surface
|
||||
|
||||
| Screen | Job |
|
||||
| --- | --- |
|
||||
| `/signup`, `/login` | Email + password. Nothing else. |
|
||||
| `/setup` | First-run guide: 3 steps — analyst model, Jev, done. |
|
||||
| `/app` | Left rail = previous analyses (rename, delete). Main = paste plan → run → live report. |
|
||||
| `/settings` | Update analyst model (base URL, key, model, extra params), Jev (key, model), email/password, history. |
|
||||
|
||||
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`,
|
||||
`key`, `model`, extra params). Generates: candidate action plans, candidate SQL
|
||||
rewrites, and the final OpenUI Lang report. Text in, text out.
|
||||
- **Jev** — TypeSafe System One (`@typesafe-ai/sdk`, model `jev-latest`). Never
|
||||
generates prose. Answers typed questions (`score`, `choice`, `noul`) about each
|
||||
candidate and returns probabilities + confidence. Code combines them.
|
||||
|
||||
## Requirements
|
||||
|
||||
1. Auth: signup, login, logout, session survives reload. Per-user data isolation.
|
||||
2. Credentials at rest are encrypted (AES-256-GCM, `APP_SECRET` env). Never echoed
|
||||
to the browser — masked (`••••1234`).
|
||||
3. Plan input: paste XML, or drop/select a `.sqlplan`/`.xml` file. Clear inline
|
||||
error if it is not a ShowPlanXML document.
|
||||
4. Deterministic digest first, model second: parse the XML in code (operators,
|
||||
costs, missing indexes, warnings, waits, spills, estimate vs actual). The model
|
||||
sees the digest, never 40 MB of XML.
|
||||
5. 2–4 candidates per run. Each candidate: diagnosis, ordered actions with effort
|
||||
and change-control flag, optional rewrite SQL, expected effect.
|
||||
6. Jev judges every candidate in parallel on 4 dimensions, then a second call picks
|
||||
`first_to_run` across all candidates. Composite weights live in code, visible in UI.
|
||||
7. Safety gate: a candidate Jev rates as semantically unsafe is never presented as
|
||||
the recommended path without a visible "verify results before deploying" flag.
|
||||
8. History: every run persisted (input digest, candidates, Jev verdict, rendered
|
||||
report). Reopening a history item replays the stored report — zero model calls.
|
||||
9. Streaming: stage indicator while digesting/proposing/judging, then the report
|
||||
renders token-by-token. No spinner-for-30-seconds.
|
||||
10. Single container: `docker compose up` → one image, one `/data` volume, no
|
||||
external DB, no queue, no Redis.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No live connection to SQL Server. No plan-from-cache lookup. No automatic index
|
||||
creation. No multi-user teams, roles, or sharing. No billing. No mobile app.
|
||||
No markdown editor. No prompt marketplace. No agentic loop with tool calls.
|
||||
|
||||
## Success looks like
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,112 @@
|
||||
# SQL Server qAI
|
||||
|
||||
Paste a SQL Server ShowPlan XML (or drop a `.sqlplan`), get a report with several ranked remediation options and
|
||||
rewritten queries. Two models, one job: a **generative analyst model** proposes the options and writes the report, and
|
||||
**Jev** (TypeSafe System One) scores every option and decides which one to run first. Jev never writes SQL; it returns
|
||||
typed scores, a choice, and confidences. The report renders as OpenUI elements, not a markdown blob.
|
||||
|
||||
## Pipeline
|
||||
|
||||
1. `lib/digest.ts` parses the XML in code: top operators, missing indexes, warnings, waits, estimate-vs-actual row
|
||||
errors, tables, statement text, subtree cost, parallelism. Raw XML never reaches a model.
|
||||
2. The analyst model receives the digest JSON and proposes 2–4 genuinely different options (index, rewrite, statistics,
|
||||
parallelism, schema), each with ordered action steps and an optional rewrite.
|
||||
3. Jev answers four questions per option (`bottleneck_fit`, `semantic_safety`, `ease` `score`; `root_cause` `noul`) and
|
||||
one cross-option call (`first_to_run` `choice`, `anything_worth_running` `noul`).
|
||||
4. `lib/jev.ts` combines them with visible weights (0.40 / 0.30 / 0.15 / 0.15 — four numbers in that file, the only
|
||||
tuning knob) and prints a verdict. Jev's pick leads only when its confidence clears 0.5; otherwise the report says
|
||||
the order came from composite scoring.
|
||||
5. The analyst streams the final report as OpenUI Lang; `<Renderer>` paints it as it arrives.
|
||||
|
||||
Getting the XML: SSMS → **Ctrl+M** (include actual execution plan) → run the query → right-click the Execution Plan tab
|
||||
→ *Save As .sqlplan*. No SSMS: prefix the batch with `SET STATISTICS XML ON;` and copy the XML result cell.
|
||||
|
||||
## Run it
|
||||
|
||||
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`:
|
||||
|
||||
| key | why |
|
||||
| --- | --- |
|
||||
| `APP_SECRET` | ≥16 chars. Encrypts API keys at rest (AES-256-GCM). Changing it makes stored keys unreadable. |
|
||||
| `DATA_DIR` | where `qai.db` (SQLite, `node:sqlite`) lives. Default `./data`. Docker uses `/data`. |
|
||||
| `COOKIE_SECURE` | set to `1` only when the app is served over https without a proxy that sets `X-Forwarded-Proto`. The session cookie is `Secure` when that header says `https`; forcing it on plain http makes Safari refuse to store it. |
|
||||
|
||||
Model keys are **not** config. Each user types them in the app (first-run guide or `/settings`), so a second user
|
||||
brings their own endpoint and their own spend. Blank key field = keep the stored key.
|
||||
|
||||
- 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
|
||||
|
||||
```bash
|
||||
npm test # node --test on tests/*.test.ts, no test framework installed
|
||||
npm run build # production build + type check
|
||||
npm run e2e # builds first, then Playwright against the production server
|
||||
```
|
||||
|
||||
The unit suites run offline: the analyst and Jev are exercised through injected `fetch`, so no key and no network.
|
||||
`npm run e2e` is the opposite — `pree2e` builds, then Playwright drives `npm run start` (the production server,
|
||||
not `next dev`: cookie flags and hydration differ, and that difference once hid a sign-out bug) against the real
|
||||
models. It reads `JEV_API_KEY` from `.env.local`. Expect it to be slow; that is the models, not the harness.
|
||||
|
||||
Projects: `chromium` (everything except the session spec, reusing one signed-in session), `webkit` (Safari —
|
||||
session survival only, because it is the one engine that rejects a `Secure` cookie over `http://localhost`),
|
||||
and `mobile` (Pixel 7, layout only, so no model run is paid for twice).
|
||||
|
||||
`tests/digest.test.ts` skips unless a genuine plan exists at `fixtures/real.sqlplan`. Drop one there from SSMS and the
|
||||
skip becomes the real check on ShowPlan attribute spellings.
|
||||
|
||||
## Git remote (Gitea)
|
||||
|
||||
Origin points at the self-hosted Gitea:
|
||||
|
||||
```bash
|
||||
git remote -v # http://192.168.88.39:3000/ahmed/sql-server-query-ai.git
|
||||
git push origin feat/sql-server-qai
|
||||
```
|
||||
|
||||
The remote URL deliberately contains **no password**. Git prompts for the username (`ahmed`) and password the first
|
||||
time, or you can keep them out of prompts:
|
||||
|
||||
```bash
|
||||
git config credential.helper osxkeychain # remember them locally
|
||||
git push origin feat/sql-server-qai
|
||||
```
|
||||
|
||||
Never commit the password into `.git/config`, a remote URL, or this README — a stored credential in the repo is
|
||||
readable by whoever you hand the repo to. If you want it non-interactive in CI, create a Gitea token
|
||||
(Settings → Applications) and use `http://192.168.88.39:3000/ahmed/sql-server-query-ai.git` with the token as password.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
lib/digest.ts XML → bounded digest (the only shape models see)
|
||||
lib/analyst.ts propose 2–4 options; stream the final OpenUI report
|
||||
lib/jev.ts questions, weights, ranking, verdict, fallback
|
||||
lib/report-ui.tsx OpenUI component library — the same module builds the prompt and renders the answer
|
||||
lib/db.ts node:sqlite: users, sessions, settings, analyses
|
||||
lib/vault.ts AES-256-GCM seal/open/mask for credentials
|
||||
lib/auth.ts scrypt passwords, hashed session tokens (DB stores the SHA-256, never the token)
|
||||
app/api/analyze SSE pipeline: digesting → proposing → judging → writing
|
||||
tests/ offline unit suites tests-e2e/ Playwright
|
||||
docs/superpowers/ spec + the 13-task implementation plan this was built from
|
||||
```
|
||||
|
||||
Design: dark terminal console, JetBrains Mono for data, IBM Plex Sans for prose, one phosphor accent.
|
||||
Print stylesheet flips to paper white and drops the rail, buttons and chrome — that is the Export to PDF path.
|
||||
@@ -0,0 +1,233 @@
|
||||
# Design System Master File
|
||||
|
||||
> **LOGIC:** When building a specific page, first check `design-system/pages/[page-name].md`.
|
||||
> If that file exists, its rules **override** this Master file.
|
||||
> If not, strictly follow the rules below.
|
||||
|
||||
---
|
||||
|
||||
**Project:** SQL Server qAI
|
||||
**Generated:** 2026-09-21 12:56:55
|
||||
**Category:** Developer Tool / IDE
|
||||
**Design Dials:** Variance 4/10 (Balanced / Modern) | Motion 3/10 (Subtle) | Density 8/10 (Dense / Dashboard)
|
||||
|
||||
---
|
||||
|
||||
## Global Rules
|
||||
|
||||
### Color Palette
|
||||
|
||||
| Role | Hex | CSS Variable |
|
||||
|------|-----|--------------|
|
||||
| Primary | `#1E293B` | `--color-primary` |
|
||||
| On Primary | `#FFFFFF` | `--color-on-primary` |
|
||||
| Secondary | `#334155` | `--color-secondary` |
|
||||
| On Secondary | `#FFFFFF` | `--color-on-secondary` |
|
||||
| Accent/CTA | `#22C55E` | `--color-accent` |
|
||||
| On Accent/CTA | `#0F172A` | `--color-on-accent` |
|
||||
| Background | `#0F172A` | `--color-background` |
|
||||
| Foreground | `#F8FAFC` | `--color-foreground` |
|
||||
| Card | `#1B2336` | `--color-card` |
|
||||
| Card Foreground | `#F8FAFC` | `--color-card-foreground` |
|
||||
| Muted | `#272F42` | `--color-muted` |
|
||||
| Muted Foreground | `#94A3B8` | `--color-muted-foreground` |
|
||||
| Border | `#475569` | `--color-border` |
|
||||
| Destructive | `#EF4444` | `--color-destructive` |
|
||||
| On Destructive | `#000000` | `--color-on-destructive` |
|
||||
| Ring | `#FFFFFF` | `--color-ring` |
|
||||
|
||||
**Color Notes:** Code dark + run green
|
||||
|
||||
### Typography
|
||||
|
||||
- **Heading Font:** JetBrains Mono
|
||||
- **Body Font:** IBM Plex Sans
|
||||
- **Mood:** code, developer, technical, precise, functional, hacker
|
||||
- **Google Fonts:** [JetBrains Mono + IBM Plex Sans](https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap)
|
||||
|
||||
**CSS Import:**
|
||||
```css
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
|
||||
```
|
||||
|
||||
### Spacing Variables
|
||||
|
||||
*Density: 8/10 — Dense / Dashboard*
|
||||
|
||||
| Token | Value | Usage |
|
||||
|-------|-------|-------|
|
||||
| `--space-xs` | `2px` / `0.125rem` | Tight gaps |
|
||||
| `--space-sm` | `4px` / `0.25rem` | Icon gaps, inline spacing |
|
||||
| `--space-md` | `8px` / `0.5rem` | Standard padding |
|
||||
| `--space-lg` | `12px` / `0.75rem` | Section padding |
|
||||
| `--space-xl` | `16px` / `1rem` | Large gaps |
|
||||
| `--space-2xl` | `24px` / `1.5rem` | Section margins |
|
||||
| `--space-3xl` | `32px` / `2rem` | Hero padding |
|
||||
|
||||
### Shadow Depths
|
||||
|
||||
| Level | Value | Usage |
|
||||
|-------|-------|-------|
|
||||
| `--shadow-sm` | `0 1px 2px rgba(0,0,0,0.05)` | Subtle lift |
|
||||
| `--shadow-md` | `0 4px 6px rgba(0,0,0,0.1)` | Cards, buttons |
|
||||
| `--shadow-lg` | `0 10px 15px rgba(0,0,0,0.1)` | Modals, dropdowns |
|
||||
| `--shadow-xl` | `0 20px 25px rgba(0,0,0,0.15)` | Hero images, featured cards |
|
||||
|
||||
---
|
||||
|
||||
## Component Specs
|
||||
|
||||
### Buttons
|
||||
|
||||
```css
|
||||
/* Primary Button */
|
||||
.btn-primary {
|
||||
background: #22C55E;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 200ms ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Secondary Button */
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: #1E293B;
|
||||
border: 2px solid #1E293B;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 200ms ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
```
|
||||
|
||||
### Cards
|
||||
|
||||
```css
|
||||
.card {
|
||||
background: #0F172A;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: all 200ms ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
```
|
||||
|
||||
### Inputs
|
||||
|
||||
```css
|
||||
.input {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: border-color 200ms ease;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: #1E293B;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px #1E293B20;
|
||||
}
|
||||
```
|
||||
|
||||
### Modals
|
||||
|
||||
```css
|
||||
.modal-overlay {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
box-shadow: var(--shadow-xl);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Style Guidelines
|
||||
|
||||
**Style:** Dark Mode (OLED)
|
||||
|
||||
**Keywords:** Dark theme, low light, high contrast, deep black, midnight blue, eye-friendly, OLED, night mode, power efficient
|
||||
|
||||
**Best For:** Night-mode apps, coding platforms, entertainment, eye-strain prevention, OLED devices, low-light
|
||||
|
||||
**Key Effects:** Minimal glow (text-shadow: 0 0 10px), dark-to-light transitions, low white emission, high readability, visible focus
|
||||
|
||||
### Page Pattern
|
||||
|
||||
**Pattern Name:** FAQ/Documentation Landing
|
||||
|
||||
- **Conversion Strategy:** Reduce support tickets. Track search analytics. Show related articles. Contact escalation path.
|
||||
- **CTA Placement:** Search bar prominent + Contact CTA for unresolved questions
|
||||
- **Section Order:** Hero with search bar > Popular categories > FAQ accordion > Contact/support CTA
|
||||
|
||||
---
|
||||
|
||||
## Motion
|
||||
|
||||
**Scroll Reveal** (Subtle) — Trigger: scroll (viewport enter) | Duration: 300-400ms | Easing: `power1.out`
|
||||
|
||||
```js
|
||||
gsap.from(el, { opacity: 0, y: 12, duration: 0.35, ease: 'power1.out', scrollTrigger: { trigger: el, start: 'top 90%', toggleActions: 'play none none reverse' } });
|
||||
```
|
||||
|
||||
**Framework notes:** Requires the ScrollTrigger plugin registered once via gsap.registerPlugin(ScrollTrigger); Use matchMedia('(prefers-reduced-motion: reduce)') to skip non-essential motion and render the final state immediately
|
||||
|
||||
- ✅ Keep the y offset small (8-16px) so it reads as a fade, not a slide
|
||||
- ❌ Don't reveal below-the-fold content needed for SEO/crawlers as invisible-by-default without a no-JS fallback
|
||||
- ⚡ toggleActions 'play none none reverse' avoids re-triggering on every scroll direction change
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns (Do NOT Use)
|
||||
|
||||
- ❌ Light mode default
|
||||
- ❌ Slow performance
|
||||
|
||||
### Additional Forbidden Patterns
|
||||
|
||||
- ❌ **Emojis as icons** — Use SVG icons (Heroicons, Lucide, Simple Icons)
|
||||
- ❌ **Missing cursor:pointer** — All clickable elements must have cursor:pointer
|
||||
- ❌ **Layout-shifting hovers** — Avoid scale transforms that shift layout
|
||||
- ❌ **Low contrast text** — Maintain 4.5:1 minimum contrast ratio
|
||||
- ❌ **Instant state changes** — Always use transitions (150-300ms)
|
||||
- ❌ **Invisible focus states** — Focus states must be visible for a11y
|
||||
|
||||
---
|
||||
|
||||
## Pre-Delivery Checklist
|
||||
|
||||
Before delivering any UI code, verify:
|
||||
|
||||
- [ ] No emojis used as icons (use SVG instead)
|
||||
- [ ] All icons from consistent icon set (Heroicons/Lucide)
|
||||
- [ ] `cursor-pointer` on all clickable elements
|
||||
- [ ] Hover states with smooth transitions (150-300ms)
|
||||
- [ ] Light mode: text contrast 4.5:1 minimum
|
||||
- [ ] Focus states visible for keyboard navigation
|
||||
- [ ] `prefers-reduced-motion` respected
|
||||
- [ ] Responsive: 375px, 768px, 1024px, 1440px
|
||||
- [ ] No content hidden behind fixed navbars
|
||||
- [ ] No horizontal scroll on mobile
|
||||
@@ -0,0 +1,84 @@
|
||||
# SDD ledger — plan: docs/superpowers/plans/2026-09-21-sql-server-qai.md
|
||||
|
||||
Ruling: no Docker on this machine (human says limited resources) — write Dockerfile/.dockerignore in Task 12 but never build or run it here; verify with npm run dev + npm start. Ruling: drop Tailwind — the plan styles with tokens.css + semantic classes only, so @tailwindcss/postcss and tailwindcss are uninstalled and postcss.config.mjs deleted. Cost if wrong: we hand-write CSS we would otherwise generate.
|
||||
Task 1: complete (commits 2778ce7..f64dc31, tests: npm run build → ƒ (Dynamic) server-rendered on demand)
|
||||
Task 1: Ruling: no next/font/google — fonts come from the token stack only (JetBrains Mono / IBM Plex Sans if installed, else system mono+sans). Build stays offline-safe and one network dependency lighter — cost if wrong: brand fonts missing on machines without them.
|
||||
Task 1: Ruling: tokens.css lives at app/tokens.css (the @import in globals.css resolves there); the duplicate root copy is deleted — cost if wrong: none.
|
||||
Task 1: Ruling: app/page.tsx is a plain redirect("/login") because lib/auth does not exist yet; Task 3 replaces it with the session-aware redirect its brief omits — cost if wrong: signed-in users hitting / land on /login until Task 3.
|
||||
Task 1: Ruling: deps are next/react/react-dom/zod/@openuidev/react-lang/@typesafe-ai/sdk/fast-xml-parser. lucide-react skipped (no plan code imports it) and Tailwind dropped (see setup ruling) — cost if wrong: add lucide-react the moment an icon is drawn.
|
||||
Task 1: Note: env fix, not a code fix — node_modules/@next/swc-darwin-arm64 came down truncated (33MB of 89MB) so `next build` died with "Turbopack requires native bindings"; reinstalled with npm i --no-save @next/swc-darwin-arm64@16.3.5.
|
||||
Task 2: Ruling: patchAnalysis filters keys against a PATCHABLE whitelist — the brief interpolated object keys straight into the SET clause, and one caller that ever passes a request-shaped object would own a SQL injection. Cost if wrong: a new patchable column needs adding to the set.
|
||||
Task 2: Ruling: test script is `node --no-warnings --test tests/*.test.ts` (the plan's own line). `node --test tests` fails with MODULE_NOT_FOUND on node 25 in this repo — the space in the path makes Node resolve the directory as a module.
|
||||
Task 2: complete (commits f64dc31..692ffa9, tests: npm test → ℹ duration_ms 109.10725)
|
||||
Task 3: Ruling: lib/auth.ts imports ./db.ts (relative, .ts extension) and loads next/headers + next/navigation lazily — `node --test` imports this module, and bare node resolves neither `@/…` nor `next/*`. tsconfig gains allowImportingTsExtensions. Next-only glue (actions, pages, components) keeps @/. Cost if wrong: every test-imported module must keep this discipline.
|
||||
Task 3: Ruling: requireUser declares Promise<{id,email}> with an unreachable throw after redirect(), because redirect() behind a dynamic import is not typed `never`. Cost if wrong: dead line, no behaviour change (redirect throws anyway).
|
||||
Task 3: Ruling: AuthForm's action is (state: FormResult, fd) => Promise<FormResult> and the server actions take `_: unknown` — the brief's `_: void` does not type-check against useActionState, which passes the previous state.
|
||||
Task 3: Ruling: app/app/layout.tsx awaits requireUser now (the guard the brief only asserts as a manual check). Task 11 may write it again; it is idempotent.
|
||||
Task 3: Note: signUp/signIn redirect to /setup, which Task 5 creates. Until then that redirect lands on 404. Verified now: signed-out /app -> 307 /login, /login 200, /signup renders the password help.
|
||||
Task 3: complete (commits 692ffa9..5f8746c, tests: npm test → ℹ duration_ms 190.291833)
|
||||
Task 4: Ruling: db.saveSettings now keeps the stored secret when the incoming one is blank (COALESCE(NULLIF(excluded.x,''), settings.x)) instead of overwriting it. The brief only protected against this in lib/settings.ts; the shared upsert is where a later caller (Task 5's password change) would have wiped both keys with one omitted field. Test: "saveSettings never blanks a stored secret, whoever calls it" — RED before the fix. Cost if wrong: a future "clear my key" feature needs an explicit sentinel instead of "".
|
||||
Task 4: Ruling: lib/settings.ts imports ./db.ts and ./vault.ts (test-imported module, same discipline as Task 3).
|
||||
Task 4: complete (commits 5f8746c..dd6b880, tests: npm test → ℹ duration_ms 455.936167)
|
||||
Task 5: Ruling: /setup only auto-forwards to /app for steps 1 and 2; ?step=3 still renders after onboarding. The brief's unconditional redirect made the readiness screen unreachable exactly when it matters (Task 13 opens it after saving keys). Cost if wrong: one extra clickable step for a configured user.
|
||||
Task 5: Verified: signed-in /settings shows ••••1234 and ••••9999 (masked, never raw); anon /settings -> 307 /login; onboarded /setup and /setup?step=2 -> 307 /app; /setup?step=3 -> 200.
|
||||
Task 5: complete (commits dd6b880..1c3f222, tests: npm test → ℹ duration_ms 464.65375)
|
||||
Task 6: Ruling: tables and missing-index tables are rebuilt as Database.Schema.Table. The brief joined only the bracket-stripped Table attribute, so the digest named "Orders" where the test (and any DBA reading the report) needs "wide.Sales.Orders". RED first: actual ['Orders','Customer']. Cost if wrong: none, the fixture asserts the joined form.
|
||||
Task 6: Note: no genuine .sqlplan exists on this machine (no SQL Server to export one from), so "a real .sqlplan digests" skips with a printed reason and the commit message says so. Someone with SSMS should drop one at fixtures/real.sqlplan; that test is the check on attribute spellings.
|
||||
Task 6: complete (commits 1c3f222e..4fefd0f, tests: npm test → ℹ duration_ms 451.9635)
|
||||
Task 7: Ruling: lib/analyst.ts imports ./digest.ts and ./settings.ts (test-imported module discipline from Task 3).
|
||||
Task 7: Note: partner asked for the Gitea remote mid-task. origin = http://192.168.88.39:3000/ahmed/sql-server-query-ai.git with no credential stored in .git/config; pushed main and feat/sql-server-qai once with inline credentials. README documents the remote and credential hygiene.
|
||||
Task 7: complete (commits 4fefd0f..816f22c, tests: npm test → ℹ duration_ms 482.014)
|
||||
Task 8: Verified against live Jev (jev-1.13, real key, real digest): 1.3s, picked idx_covering_orders with confidence 1.00, composite agreed, TOP-100 rewrite scored semantic_safety 0.05 and flagged. makeJevClient/score/noul/choice work as the brief specifies.
|
||||
Task 8: Ruling: verify_semantics now fires only where semantics can move — any option carrying rewrite_sql below 0.6, or any option below 0.45. Evidence: live Jev scored the index create 0.57 ("equivalent, no stated proof"), so the brief's blanket <0.6 floor would flag nearly every index option and the warning would become noise. Test "verify_semantics fires where semantics can actually move" RED→GREEN. Cost if wrong: a genuinely risky non-rewrite change (e.g. dropping an index used elsewhere) scores 0.45-0.6 and goes unflagged; the option table still prints its safety bar.
|
||||
Task 8: Ruling: jevFallback strips score-derived flags (kept change_control, which is read off the candidate's own actions). Neutral 0 scores meant "Jev never spoke", not "this option is unsafe" — the brief's fallback accused every option. Test added, RED→GREEN.
|
||||
Task 8: Ruling: rubric arrays are `as const` — the SDK types ScoreCriteria as a non-empty readonly tuple, so a plain string[] fails `next build`'s type check even though the call works at runtime.
|
||||
Task 8: complete (commits 816f22c..d8caa8e, tests: npm test → ℹ duration_ms 504.103542)
|
||||
Task 9: Ruling: the library is lib/report-ui.ts written with createElement, not lib/report-ui.tsx with JSX. Node strips types but not JSX, so the brief's own test (`await import("../lib/report-ui.tsx")` under node --test) can never load a JSX file without adding a transpiler to the dep list. Same components, props, prompt and refs; only the call syntax changed. Cost if wrong: slightly noisier component bodies.
|
||||
Task 9: complete (commits d8caa8e..396b0ed, tests: npm test → ℹ duration_ms 500.095583)
|
||||
Task 9: Follow-up fix (found by `npm run build`, not by the tests): lib/db.ts opened SQLite and ran DDL at import, so next build's ~6 page-data workers hit "database is locked" and the build failed. Now the file opens on first query behind a bound Proxy, plus PRAGMA busy_timeout=5000. RED evidence: build exit 1 with "Failed to collect configuration for / [cause]: database is locked"; after the change build exits 0 and the suite is green (the Proxy had to bind methods — first attempt failed 4 tests with "Illegal invocation"). Cost if wrong: a long build-time query would still wait, which is what busy_timeout is for.
|
||||
Task 10: complete (commits 13bd2e6..ac4cbdd, tests: npm test → ℹ duration_ms 531.938)
|
||||
Task 10: Verified live, single run against the real analyst + real Jev: 41s end to end — stage digesting → 3 candidates → verdict {source: jev, headline: idx_covering_orders, confidence 1.00, composite agrees} → 2861 chars of OpenUI streamed in ~15 deltas → done {id}. Stored row replayed from /app/<id> renders the full report.
|
||||
Task 10: Ruling: the brief said "no cancel() handler, the default is right". It is not. enqueue() on an abandoned ReadableStream does not throw, so a producer whose browser closed keeps firing multi-minute analyst calls; two ghost runs piled onto the local vLLM and a fresh request sat in "proposing" for 10+ minutes behind them. Now: cancel() sets a flag, aborts the in-flight fetch through an AbortController (proposeCandidates/streamReport take an optional signal), the row is stored as partial/abandoned, and no further stage starts. Cost if wrong: an abandoned row reads "Closed before the run finished." even when the SQL was fine.
|
||||
Task 10: Ruling: both analyst calls send max_tokens: 2048 before ...cfg.extra (Settings can still override). Without it the local endpoint truncated the report at ~650 chars, which is not enough for 13 OpenUI blocks.
|
||||
Task 10: Ruling: prompt library split into lib/report-spec.ts (lang-core, React-free) + lib/report-ui.ts (renderers only, built from the same specs array). react-lang calls React.createContext at import, and a route runs under the react-server condition, where that export does not exist: `next build` died with "(0, pU.createContext) is not a function" while collecting /api/analyze. @openuidev/lang-core became a direct dep for this reason. Names/props/descriptions cannot drift because report-ui maps renderers onto the imported specs; the test asserts every spec has a renderer.
|
||||
Task 10: Note: the local analyst is a reasoning model. With thinking enabled it spends the context on `delta.reasoning` and `delta.content` stays empty, so the report arrives blank. Fixed by config, not code: Extra params {"chat_template_kwargs":{"enable_thinking":false}} — now said in the Settings field help and the README.
|
||||
Task 11: Ruling: node:sqlite rows have a null prototype, and React refuses to pass them into a client component — /app/<id> returned 500 with "Only plain objects ... can be passed to Client Components", dev trace pointing at row={{...}} in the rail. Fixed once in lib/db.ts (one() and listAnalyses spread every row) rather than in each component, and guarded by the test "rows are plain objects so they can cross the server -> client boundary" (RED first). Cost if wrong: a spread per row, which is nothing at this scale.
|
||||
Task 11: Ruling: HistoryItem binds (null, fd) wrappers because rename/remove are useActionState-shaped server actions and a plain <form action> passes only FormData.
|
||||
Task 11: Verified in Chromium (dev server, stored run): #report renders 17 cards, 1 pre.sql + Copy button, 1 option table, 3 score bars, 4 metric cards; 0 console errors; no horizontal overflow at 320/375/414/768; under print emulation the rail and toolbar hide and the body goes white.
|
||||
Task 11: complete (commits ac4cbdd..a5529ce, tests: npm test → ℹ duration_ms 529.962542)
|
||||
Task 12: Ruling: scripts/smoke.sh does not require Docker. --docker keeps the compose path; the default mode checks an already-running app (healthz, /login, /signup, signed-out /app -> 307, POST /api/analyze -> 401) and exits non-zero on any failure. The partner's machine has no Docker, so a script that could only run under compose would never be run here. Verified: all five checks ok against `npm start` on :3000.
|
||||
Task 12: Ruling: Dockerfile, docker-compose.yml and .dockerignore are written but were never built or executed here, per the partner's instruction. README says the image is untested on this machine. Cost if wrong: first real `docker build` may need a tweak or two.
|
||||
Task 12: Gate: jev-1.13 on the PLAN — YES (matches_goal 0.90, coverage 3.71/4, biggest_gap nothing). On the SPEC — matches_goal 0.94 but coverage 2.1 and the named gap moved get_plan -> qa as each was closed; the spec gained "Getting the input, and the two keys" and an "Acceptance" section because of it. The YES rule mentions tasks and tests, which a vision document does not carry, so the spec run is informational and the plan run is the gate.
|
||||
Task 12: Note: an earlier `smoke.sh` run reported /login 404. Cause was mine, not the app's: leftover `next start` processes from earlier tasks still held the port. pkill -f next, restart, all routes answer. Zombie dev servers are the first thing to check on a 404 here.
|
||||
Task 12: complete (commits a5529ce..0db7b81, tests: npm test → ℹ duration_ms 528.657875)
|
||||
Task 13: 17 tests green in 44s: signup → guide → keys typed in the panel → readiness screen; signed-out /app → /login; a real run (stages 1 of 4 → 4 of 4, OpenUI report painted from the stream, no raw "root = Report(" text, Copy reaches the clipboard, print emulation hides rail and toolbar); a dropped UTF-16/BOM .sqlplan decodes without NULs; a pasted SQL script is refused with the stage named; rename sticks; delete asks first and refuses to delete on dismiss; reopening a stored run renders the report and the server-rendered Jev verdict with no new model call; no horizontal overflow at 320/375/414/768 in desktop and Pixel 7; every settings form field has a real <label for>.
|
||||
Task 13: Ruling: the mobile project only runs responsive.spec.ts. A full run costs ~30s of real model time; a second identical copy bought nothing.
|
||||
Task 13: Ruling: three of the brief's assertions were wrong and are changed, each with a reason in the file. (1) "cards > 2" ran mid-stream, before the report existed — it now waits for the Analyse button to re-enable, which only happens on the done frame. (2) the tap-target floor of 36px is above what .btn actually is (10px padding + 15px line = 35px); the floor is 32px, which still fails a squashed or wrapped control. (3) "every input needs label[for]" failed on the rail's rename boxes, which carry aria-label instead; the check is scoped to form inputs and reports the offending names.
|
||||
Task 13: Ruling: SettingsForm gained an `after` hidden field and saveSettingsAction redirects to /setup?step=3 when it comes from the wizard. Without it the setup page's own forward (added in Task 5) bounced a just-configured user to /app, so the wizard's Save button appeared to do nothing. The /settings host still shows "Saved." inline.
|
||||
Task 13: Note (time waster, recorded so it is not repeated): deleting the data directory while a dev server is running leaves that server writing into an unlinked inode — the E2E run then "succeeds" against a database that no longer exists anywhere, and 11 tests fail with empty state. Stop the server (`pkill -f next`) before clearing .playwright-data, or let Playwright own the server lifecycle.
|
||||
Task 13: complete (commits 0db7b81..07dd3d6, tests: npm test → ℹ duration_ms 609.551292)
|
||||
Final review: self-review (no subagent tool) — the author read the whole 15-commit package; weaker than a fresh reviewer, so this is flagged for the partner to decide before merge.
|
||||
Final: fixed email collision on /settings — test "changeEmail refuses an address another account already has" RED→GREEN; suite 33/33, e2e 19/19.
|
||||
Final: fixed /app/[id] self-redirect for a row left "running" — behaviour pinned by tests-e2e/edge.spec.ts "a row left running renders what exists instead of redirecting forever" (RED: infinite redirect; GREEN).
|
||||
Final: fixed ScoreBar/OptionRow/VerdictBlock throwing on null numbers — test "a null confidence from the model does not blank the block" RED→GREEN; the live E2E log showed "[openui] runtime/render-error: Component ScoreBar render failed", which is now absent.
|
||||
Final: minor (deferred): no rate limit on /signup or /api/analyze (ponytail comment in lib/auth.ts; token bucket before this faces a network).
|
||||
Final: minor (deferred): raw XML is stored per analysis with no per-user quota; 12 MB cap per row, unbounded rows.
|
||||
Final: minor (deferred): sessions are not revoked when the password changes (30-day tokens stay valid).
|
||||
Final: minor (deferred): digestPlan walks the parsed document once per element type (~10 passes); a 12 MB plan costs a few hundred ms of CPU.
|
||||
Final: minor (deferred): rename from a /app/[id] page does not revalidate the rail title until the next navigation.
|
||||
Final: minor (deferred): .dockerignore excludes tests/ but the image copies node_modules from the build stage including devDependencies.
|
||||
Final: minor (deferred): Dockerfile is unbuilt on this machine (no Docker), so its first real build may need a tweak.
|
||||
Task 13: complete (commits 07dd3d6..5160a00, tests: npm test → ℹ duration_ms 514.179833)
|
||||
|
||||
Post-delivery bug report (partner: "clicking Next in the guide signs me out back to signup"):
|
||||
Root cause 1: startSession set secure: NODE_ENV === "production". Over plain http://localhost Safari refuses to store a Secure cookie at all, so every page after the wizard read as signed-out and requireUser bounced to /login (the login page's "Create account" link is what looked like a sign-out). Now Secure only when X-Forwarded-Proto says https, or COOKIE_SECURE=1. tests-e2e/session.spec.ts RED on webkit (Next → /login) → GREEN.
|
||||
Root cause 2 (found while pinning 1): /app had no onboarding gate at all. The guide only appeared because the signUp/signIn actions redirect there, so closing it mid-way left a console that cannot run anything until Analyse 412s. Gate now lives in app/app/layout.tsx using the same has_analyst/has_jev rule /api/analyze enforces.
|
||||
QA blind spot fixed: the suite ran next dev, and only chromium. Playwright now runs the production server (pree2e builds; reuseExistingServer false) and adds a webkit project, because chromium accepts a Secure cookie on localhost and webkit does not — the engine difference was the whole bug. 21 passed (setup, chromium, webkit, mobile).
|
||||
QA rulings on my own tests, not the app: session.spec asserted #email was prefilled (it is a change field and starts blank by design) and edge.spec picked "the newest user", which in a full run is a webkit spec's account, so it opened another account's row and 404s; both now target the setup account and the gate respectively.
|
||||
Verification on the delivered build, both engines, against http://localhost:3000: signup → guide → Next → keys → Save → step 3 "Analyst ready · Jev ready" → /app renders the paste box.
|
||||
|
||||
Post-delivery bug report 2 (partner: their own 2.7 MB production plan failed to import):
|
||||
Five defects, each found by measuring plan-cxpaket.sqlplan rather than the fixture: (1) fast-xml-parser maxNestedTags 128 vs a real nesting depth of 2729; (2) table names read off <IndexScan> instead of its <Object> child, so 51 objects digested to 0 tables; (3) SSMS writes StatementText with 
 (90 of them) and the model was fed entity noise; (4) rewrite_sql's name told the model "query rewrite only", so no CREATE INDEX ever existed to show and the report had zero SqlBlocks; (5) max_tokens 2048 truncated the now-larger candidate JSON and the user got "malformed JSON" after 38 seconds.
|
||||
Ruling: option_type + sql_to_run replaces rewrite_sql. The field name was load-bearing — it is sent to the model — and keeping it would have kept the omission. verify_semantics therefore keys on option_type === "rewrite", preserving the Task 8 ruling that index options must not be flagged for carrying DDL.
|
||||
Ruling: dropPlaceholderSql nulls ellipsis SQL instead of rendering it. A SqlBlock that cannot be pasted reads as an instruction and fails on run; no block beats a fake one. ponytail comment marks the "..." heuristic.
|
||||
Ruling: the plan file itself stays out of git (/plan-*.sqlplan). It carries the partner's real schema names; the fixture that ships is synthetic.
|
||||
Reference in New Issue
Block a user