Compare commits

...
2 Commits
Author SHA1 Message Date
ahmed mohammedandClaude Opus 4.7 8b42a0931c 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>
2026-09-22 12:14:05 +03:00
ahmed mohammedandClaude Opus 4.7 735a57bfd6 chore: untrack QA database, the local production plan, and a stray image
- .qa-data/qai.db{,-shm,-wal} was committed by an add -A during the cookie bug hunt; it holds throwaway accounts and sealed key blobs.
- plan-cxpaket.sqlplan (2.7 MB, real schema names) was committed one commit before the /plan-*.sqlplan ignore rule existed.
- an unrelated upscaled PNG from the working tree went in with the same add -A.
All four stay on disk and out of git; .ua/ is ignored too so generated graph data never sneaks in the same way.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-22 11:48:47 +03:00
48 changed files with 3538 additions and 0 deletions
+4
View File
@@ -13,6 +13,7 @@ next-env.d.ts
# runtime data (sqlite) # runtime data (sqlite)
/data /data
/.playwright-data /.playwright-data
.qa-data/
# e2e artifacts # e2e artifacts
/.auth /.auth
@@ -25,3 +26,6 @@ next-env.d.ts
# real production plans dropped in for debugging stay out of git # real production plans dropped in for debugging stay out of git
/plan-*.sqlplan /plan-*.sqlplan
# generated knowledge graph (regenerate with /understand)
.ua/
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 MiB

+26
View File
@@ -0,0 +1,26 @@
# Wiki conventions (schema layer)
This directory is a Karpathy-pattern wiki: raw sources, wiki pages, and this schema file.
## Layout
- `raw/` — immutable sources copied from the repository. Never edited here.
- `index.md` — the catalog, grouped by category. Every page appears exactly once.
- `log.md` — chronological record of what was added, and why.
- `<Page Name>.md` — one page per concept. File name is the link target.
## Categories
- Pipeline
- Data and storage
- Security and credentials
- Interface
- Verification
- Operations
- Decisions
## Page rules
1. Start with `# <Page Name>`, then 120 to 250 words of prose.
2. Cross-reference with `[[Page Name]]`. Every page links 3 to 6 others, and every link must resolve.
3. State measured facts (numbers, timings, error strings) instead of adjectives. Note the measurement when it
came from a specific artifact.
4. A page about a decision says: what the old behaviour was, what it cost, what replaced it, what it broke.
5. No code blocks longer than 5 lines. Long code belongs in `raw/`.
+13
View File
@@ -0,0 +1,13 @@
# Analysis row lifecycle
A row is created before the plan is validated, so every attempt is visible in the rail. Status moves through
`running` → `done` | `failed` | `partial` | `abandoned`.
`partial` means some stages produced output before the run stopped. `abandoned` means the browser went away
mid-run. Both surface as pills in the history rail rather than being hidden, and `/app/[id]` renders whatever
exists with a "this run was interrupted before it finished" note.
`/app/[id]` never redirects a `running` row. An earlier version did, which turned a row orphaned by a crashed
process into an infinite self-redirect.
Related: [[SSE stream]], [[SQLite schema]], [[History rail]].
+20
View File
@@ -0,0 +1,20 @@
# Analyst model
`lib/analyst.ts` talks to any OpenAI-compatible endpoint the user pastes in settings. Two calls, two jobs.
`proposeCandidates` asks for 2 to 4 genuinely different remediation options and validates them against a zod
schema: `key`, `title`, `diagnosis`, 1-4 `actions`, `option_type`, `sql_to_run`, `expected`. Fewer than two options is
an error, because [[Jev scoring]] cannot rank a single option.
`streamReport` writes the final answer as OpenUI Lang, streamed token by token into [[OpenUI report language]].
Its system prompt carries hard requirements: every option with `sql_to_run` gets a SqlBlock containing exactly that
T-SQL, and every option gets a ScoreBar.
Both calls spread the user's extra JSON parameters *after* `max_tokens`, which is how the output ceiling is raised
from the settings screen without a rebuild. `extractJson` recovers an object from prose or fenced output and, when
the response was cut off, names the character count instead of blaming the model.
The local QA endpoint is `http://192.168.88.131:8888/v1` running `qwen3.8-flash-next`; it streams reasoning
instead of content unless extra params include `{"chat_template_kwargs":{"enable_thinking":false}}`.
Related: [[Jev scoring]], [[Option type and sql to run]], [[Placeholder SQL is dropped]], [[Output token ceiling]].
+13
View File
@@ -0,0 +1,13 @@
# Console runner
`components/runner.tsx` is the whole console: file drop with encoding-aware decode, the paste box, stage pills,
SSE reading, the live report, a cancel button, and the error line.
The Analyse button is disabled while a stage is active and re-enabled only on the `done` frame — which is what the
end-to-end test waits on before asserting report contents.
Design follows the [[Design tokens]] contract: dark workbench, JetBrains Mono for plan data, IBM Plex Sans for
prose, one phosphor accent. The layout holds at 320 px without horizontal scroll, and every settings field has a
real `<label for>`.
Related: [[SSE stream]], [[OpenUI report language]], [[Export and print]], [[History rail]].
+12
View File
@@ -0,0 +1,12 @@
# Container plan
`Dockerfile` is a multi-stage Node 25 build — Node 25 because `node:sqlite` is what the data layer uses — and
`docker-compose.yml` mounts a named volume at `/data` and reads `APP_SECRET` from the host environment.
The image is written but unbuilt on the development machine, which has no Docker. The build is therefore the
first thing to verify on a machine that has it: `bash scripts/smoke.sh --docker`.
Known rough edges carried forward deliberately: `.dockerignore` excludes tests but the image copies `node_modules`
including devDependencies, and a first real build may need a tweak.
Related: [[Running locally]], [[Smoke script]], [[Environment variables]].
+13
View File
@@ -0,0 +1,13 @@
# Credential vault
Model keys are pasted by the user into the settings screen. They are never stored as server environment
configuration and never appear in the DOM of a subsequent page.
`lib/vault.ts` seals with AES-256-GCM under a key derived from `APP_SECRET`, and renders secrets as bullets plus the
last four characters (`••••1234`). `open` returns null instead of throwing when `APP_SECRET` changed, so the app
asks for the key again instead of crashing.
`settingsView` is the only shape the forms ever receive, which is what makes "never echo a stored secret back"
structural rather than a per-field discipline.
Related: [[Session cookie rules]], [[Settings form]], [[SQLite schema]].
+9
View File
@@ -0,0 +1,9 @@
# Design contract
`design-system/sql-server-qai/MASTER.md` is the contract the UI is built against: dark workbench palette, type
scale, spacing, component rules, and the reason the accent is reserved for action and state rather than decoration.
The Hallmark audit log in `.hallmark/log.json` records the decisions taken against it. Two of them shaped the
code: no utility CSS framework, and no network font loader.
Related: [[Design tokens]], [[Console runner]].
+13
View File
@@ -0,0 +1,13 @@
# Design tokens
`app/tokens.css` defines the palette and scale: dark console background, phosphor-green accent reserved for
action and state, JetBrains Mono for anything numeric or machine-written, IBM Plex Sans for prose, and a spacing
scale used everywhere instead of literal margins.
`app/ui.css` turns the tokens into semantic classes — `card`, `field`, `btn`, `pill`, `bar`, `rail` — plus the print
stylesheet. Tailwind was installed by the scaffold and removed by decision: the design contract is small enough to
own directly, and utility classes were duplicating tokens rather than expressing them.
Fonts come from the token stack rather than a network font loader, keeping the build offline-light.
Related: [[Export and print]], [[Console runner]], [[Design contract]].
+13
View File
@@ -0,0 +1,13 @@
# Decision: email collision check
Changing your email in settings wrote straight to a UNIQUE column. A collision surfaced as a raw constraint error
inside a server action — the form looked like it did nothing.
`changeEmail` in `lib/auth.ts` now validates the address, compares it case-insensitively after trimming, and
returns "That email already has an account." when another user owns it. It returns ok when the address is already
yours.
A deferred neighbour: sessions are not revoked when the password changes, so a 30-day token survives a password
rotation.
Related: [[First-run gate]], [[Session cookie rules]], [[SQLite schema]].
+11
View File
@@ -0,0 +1,11 @@
# Environment variables
Deliberately short. `APP_SECRET` seals stored keys; `DATA_DIR` locates the SQLite file; `COOKIE_SECURE=1` forces the
Secure flag for a direct https deploy with no proxy setting `X-Forwarded-Proto`.
Nothing about models lives here. Base URL, key, model name, extra JSON parameters, Jev key and Jev model are
per-user settings encrypted at rest, because the deployment is expected to be self-hosted and shared.
The full file list, with the reasoning for each default, is in the repository README.
Related: [[Credential vault]], [[Session cookie rules]], [[Running locally]].
+9
View File
@@ -0,0 +1,9 @@
# Export and print
Two export actions, both client-side, no PDF library: a Copy button that pulls the SQL blocks out of the rendered
report, and Print, which opens the browser print dialog against a print stylesheet.
`app/ui.css` carries the print rules — the rail, the toolbar and the runner all hide, so the printed page is the
report. Saving as PDF from the dialog is the export path.
Related: [[Console runner]], [[OpenUI report language]], [[Design tokens]].
+14
View File
@@ -0,0 +1,14 @@
# First-run gate
The console requires two things before it renders: an analyst base URL with model and key, and a Jev key. The gate
lives in `app/app/layout.tsx`, so it covers `/app` and `/app/[id]` in one place, and it matches the rule
`POST /api/analyze` already enforces with a 412.
Before the gate existed, the guide was only reachable because the signup and sign-in actions redirect there. Close
the wizard halfway and the console rendered anyway, then failed only when Analyse turned red.
The three-step guide is reused as the destination after a save from settings, via a hidden `after` field on
`SettingsForm`. Without it, a save from the wizard bounced the user straight to the console and the Save button
looked dead.
Related: [[Session cookie rules]], [[Settings form]], [[Email collision check]].
+10
View File
@@ -0,0 +1,10 @@
# Gitea remote
The repository is pushed to a self-hosted Gitea at `http://192.168.88.39:3000/ahmed/sql-server-query-ai.git`, branch
`main`. Credentials were used inline for the push and are deliberately absent from `.git/config`, the README and
every tracked file; the README documents using a keychain entry or a token instead.
Tracked artifacts that must stay out: the generated `.ua/` graph directory, the QA databases under `.qa-data/`,
`.playwright-data`, and any production plan file matching `/plan-*.sqlplan` — real plans carry real schema names.
Related: [[Running locally]], [[Wiki conventions]].
+10
View File
@@ -0,0 +1,10 @@
# History rail
The rail lists the signed-in user's runs newest first, with the settings link pinned at the bottom. Each row is a
card holding the title link, an inline rename field, a delete button, and a status pill that appears only when
status is not `done`.
Delete never fires on a stray click: the form asks for confirmation and does nothing when it is dismissed. Rename
and delete are server actions scoped by owner id, so another user's id in a hidden field deletes nothing.
Related: [[Analysis row lifecycle]], [[Console runner]], [[SQLite schema]].
+13
View File
@@ -0,0 +1,13 @@
# Jev goal gate
`scripts/jev-gate.mjs` asks Jev whether a document actually builds the stated goal, before code is written from it.
It asks for a yes/no, a coverage score, coverage confidence, and the biggest gap as a typed choice rather than an
unfalsifiable "is something missing".
Both the plan and the spec pass: verdict YES, goal match 0.90, coverage 3.71 of 4, biggest gap `nothing`.
Two earlier failures were measurement artefacts, not model verdicts: sending a raw 60k truncation hid the tail of
the plan, and replacing the text with a heading-and-checkbox skeleton stripped too much prose. The gate now strips
fenced code blocks, keeps prose, and sends up to 90k.
Related: [[Jev scoring]], [[Smoke script]].
+18
View File
@@ -0,0 +1,18 @@
# Jev scoring
Jev is a separate typed judgment model, not a second opinion in prose. `lib/jev.ts` asks it four questions per
option, gets a value plus a confidence for each, and combines them through `WEIGHTS` into one composite score.
Three flags come out of the scores:
- `verify_semantics` when semantic safety falls below 0.45, or below 0.6 for an option whose `option_type` is
`rewrite`. Live Jev scores an index create around 0.57 ("probably equivalent, no stated proof"), so keying the
soft floor on the option type rather than on "has SQL" is what keeps the warning useful.
- `low_confidence` when any dimension's confidence drops under the floor.
- `change_control` when an action is marked as needing change control.
When Jev is unreachable or abstains, `jevFallback` returns the composite order and marks the source
`jev_unavailable`, so the report prints "composite order — Jev did not decide" rather than implying Jev chose.
A real run on the 2.7 MB production plan hit exactly this path and stayed honest.
Related: [[Analyst model]], [[Option type and sql to run]], [[Jev goal gate]].
+13
View File
@@ -0,0 +1,13 @@
# Lazy database open
Opening SQLite at module import time broke `npm run build` with "database is locked": Next collects page data in
parallel workers, each importing the module and running DDL at once.
The fix is a bound Proxy. Nothing opens until a property is first touched; then `DatabaseSync` is constructed,
`journal_mode = WAL` and `busy_timeout = 5000` are set, and the schema DDL runs once. The Proxy methods are bound to
the real instance — an unbound Proxy made every call fail with "TypeError: Illegal invocation".
A second trap: SQLite rows arrive as null-prototype objects, which Next refuses to serialize from a server
component to a client component. Rows are normalised on read.
Related: [[SQLite schema]], [[Running locally]].
+13
View File
@@ -0,0 +1,13 @@
# Decision: max_nested_tags for real plans
A 2.7 MB production plan refused to import with "Maximum nested tags exceeded", which read like a corrupt export.
The plan was fine: fast-xml-parser's default `maxNestedTags` is 128 and that plan nested **2729 tags deep**. Spaghettified
nested-loop plans do that.
The cap is now 20 000, bounded in practice by the 12 MB input limit. The same plan digests in 56 ms.
Two more real-plan defects were fixed in the same pass, both invisible to the synthetic fixture: table names are
read from the `<Object>` child rather than the `<IndexScan>` element itself, and numeric character references such
as `&#xD;&#xA;` (90 of them in that plan's statement text) are decoded before the text reaches a model.
Related: [[Table names live on Object]], [[ShowPlan digest]].
+16
View File
@@ -0,0 +1,16 @@
# OpenUI report language
The report is elements, not markdown. `lib/report-spec.ts` defines the component vocabulary and the library prompt
with `@openuidev/lang-core`; `lib/report-ui.ts` attaches the React renderers.
The split exists because `@openuidev/react-lang` calls `React.createContext` at module scope, which breaks the API
route's server-side collection. A route that needs the prompt must not import a React-backed module.
Components: Headline, MetricGrid, Finding, OptionTable, ActionSteps, SqlBlock, ScoreBar, Callout, VerdictBlock.
The report renders while `oul` is still arriving, so the headline appears before the option table.
Every numeric the model supplies passes through `num` and `conf` guards. Before that, a `null` confidence inside
ScoreBar threw and OpenUI dropped the whole block — visible live as "runtime/render-error: Component ScoreBar
render failed".
Related: [[SSE stream]], [[Analyst model]], [[Console runner]].
+15
View File
@@ -0,0 +1,15 @@
# Decision: option_type and sql_to_run
The candidate schema used to carry `rewrite_sql`. The name was load-bearing — it is sent to the model — and it
said "query rewrite only". Result: an index recommendation had no `CREATE INDEX` anywhere in the pipeline, and the
report rendered zero SQL blocks for a plan whose headline finding was missing indexes.
It became `option_type` (index | statistics | rewrite | schema | app | ops) plus `sql_to_run`, described as the exact
T-SQL the user must execute, with the instruction that an index or statistics option with null SQL is a wrong
answer.
Two consequences were deliberate. The report prompt makes a SqlBlock per non-null `sql_to_run` a hard requirement.
And the `verify_semantics` flag in [[Jev scoring]] now keys on `option_type === "rewrite"`, because keying it on
"has SQL" would flag every index option for carrying DDL — the exact noise an earlier ruling removed.
Related: [[Placeholder SQL is dropped]], [[Analyst model]], [[Jev scoring]].
+12
View File
@@ -0,0 +1,12 @@
# Decision: output token ceiling
Once every option had to carry runnable T-SQL, the candidate JSON outgrew the old 2 048-token budget. The user
waited 38 seconds and got "The analyst model returned malformed JSON" — a truncated response has no closing brace.
Both calls default to 4 096 now, and the failure message names the character count and tells the user which extra
parameter raises the ceiling, because `cfg.extra` is spread after `max_tokens`.
The general rule recorded here: when a prompt asks a model for more content, check the budget before blaming the
model's judgement.
Related: [[Analyst model]], [[Placeholder SQL is dropped]].
+14
View File
@@ -0,0 +1,14 @@
# Decision: placeholder SQL is dropped, not rendered
With the new `sql_to_run` requirement, a report on a 100 KB statement came back containing
`SELECT ... FROM ... OPTION (MAXDOP 1);`. A block nobody can paste is worse than no block: it reads as an
instruction and fails on run.
`dropPlaceholderSql` nulls ellipsis SQL after schema validation, so the option simply gets no SqlBlock. The
heuristic is marked as a deliberate shortcut: `...` is not valid T-SQL, so a literal ellipsis inside a string
literal would also be dropped, which costs a block and never invents one.
The model is also told upstream: when `digestForModel` sets `sql_truncated$, [[ShowPlan digest]] has already
warning-ed the analyst out of offering a rewrite for a statement it never received.
Related: [[Option type and sql to run]], [[Output token ceiling]].
+15
View File
@@ -0,0 +1,15 @@
# Plan file encoding
SSMS saves `.sqlplan` as UTF-16LE with a byte-order mark. `File.text()` assumes UTF-8, which turns such a plan into
NUL-filled junk and makes the parser complain about the user's export. `lib/plan-file.ts` decodes by BOM first
(UTF-32LE/BE, UTF-16LE/BE, UTF-8), then falls back to NUL density for BOM-less UTF-16: in ASCII XML roughly every
other byte is `0x00`.
The browser reads the dropped file into bytes and calls `decodePlan`; the server never sniffs encoding. That keeps
[[ShowPlan digest]] independent of how the text arrived.
Four encodings are pinned by tests: UTF-8, UTF-8 with BOM, UTF-16LE with BOM, UTF-16BE with BOM, and both UTF-16
variants with no BOM. A browser test uploads a generated UTF-16LE file and asserts the textarea holds no NUL
characters.
Related: [[ShowPlan digest]], [[Unit suites]], [[Playwright projects]].
+15
View File
@@ -0,0 +1,15 @@
# Playwright projects
`npm run e2e` builds first (`pree2e` hook) and then drives the **production** server, not `next dev`. That choice
caught a real bug: cookie flags and hydration differ between the two, and a dev-only suite never noticed a
`Secure` cookie that Safari refuses on plain http.
Projects: `setup` signs up the QA account and configures both models through the real UI, writing a storage state;
`chromium` runs everything except the session spec; `webkit` runs only the session spec, because it is the engine
that rejects a Secure cookie over `http://localhost`; `mobile` runs only the responsive checks, so a 30-second model
run is never paid for twice.
One worker, 180-second test timeout, and a single run takes about 55 seconds for 23 tests including a real
analyst and real Jev call.
Related: [[WebKit in QA]], [[Unit suites]], [[Analyst model]].
+16
View File
@@ -0,0 +1,16 @@
# Running locally
`npm install && npm run build && npm start` serves the production build on port 3000. `npm run dev` works too, but
cookie flags and hydration differ from production, which is why the end-to-end suite exists.
Two environment values matter: `APP_SECRET` (at least 16 characters, seals stored keys — changing it makes them
unreadable and the app asks for them again) and `DATA_DIR` (where `qai.db` lives, default `./data`).
`.env.example` documents both plus `COOKIE_SECURE`.
Model credentials are not environment configuration. They belong to each user and are pasted into the settings
screen.
One operational trap worth knowing: deleting the data directory while a server is running leaves that server
writing into an unlinked inode. The app keeps answering and the database no longer exists anywhere.
Related: [[Environment variables]], [[Container plan]], [[Gitea remote]].
+16
View File
@@ -0,0 +1,16 @@
# SQLite schema
`lib/db.ts` uses `node:sqlite` — no ORM, no driver, no native build. Four tables:
- `users` — id, unique email, `salt:hash` scrypt password, created_at
- `sessions` — hashed token, user id, expiry
- `settings` — one row per user: analyst base URL, model, sealed key, extra JSON, Jev key, Jev model, email, onboarded
- `analyses` — id, user id, title, raw XML, oul, verdict JSON, status, error, timestamps
`patchAnalysis` only accepts keys from a `PATCHABLE` whitelist, because building `SET` clauses straight from object
keys is how an object key becomes SQL.
The `saveSettings` upsert uses `COALESCE(NULLIF(excluded.analyst_key, ''), settings.analyst_key)` so that a form
submitting a blank secret keeps the stored one. Blank means "unchanged", never "clear".
Related: [[Lazy database open]], [[Analysis row lifecycle]], [[Credential vault]].
+13
View File
@@ -0,0 +1,13 @@
# SSE stream
One POST to `/api/analyze` returns a server-sent event stream. `lib/stream.ts` frames events on the server and
parses them in the browser, and also extracts text deltas from an OpenAI-compatible streaming response.
Four named stages are reported as they happen: digesting, proposing, judging, writing. The console shows them as
`1 of 4 · Reading the plan` style pills. A `done` frame re-enables the Analyse button; an `error` frame names the
stage it died in, so a rejected plan reads "digesting: ..." rather than failing silently.
If the browser disconnects, in-flight model fetches are aborted and the row is stored as `abandoned` or `partial`
instead of piling ghost runs onto the analyst endpoint.
Related: [[Analysis row lifecycle]], [[Console runner]], [[OpenUI report language]].
+12
View File
@@ -0,0 +1,12 @@
# Session cookie rules
The session cookie holds a 32-byte random token; only its hash is stored, and the expiry is enforced in SQL.
`httpOnly` and `sameSite: lax` always.
`Secure` is set from the request, not the build: only when `X-Forwarded-Proto` reports `https`, or when
`COOKIE_SECURE=1` is set for a direct TLS deploy. The previous rule (`secure: NODE_ENV === "production"`) meant a
production build on plain `http://localhost` asked browsers for a Secure cookie. Chromium allows that on localhost;
Safari refuses to store it at all, so every page after signup looked signed-out and the wizard appeared to sign
users out. A Chromium-only suite could never observe it.
Related: [[WebKit in QA]], [[First-run gate]], [[Running locally]].
+17
View File
@@ -0,0 +1,17 @@
# Settings form
components/settings-form.tsx is the credential screen, used twice: as step 2 of the first-run guide and on
/settings. It renders analyst base URL, analyst key, analyst model, extra JSON parameters, Jev key and Jev model,
plus the account email change field.
It renders values from settingsView only, so a stored secret is never echoed back into the DOM - the fields show
bullet-masked values, and a blank submission means "keep what is stored", enforced again in the upsert (see
[[SQLite schema]]).
The form carries a hidden after field. When the submission comes from the wizard, the action returns the user to
/setup?step=3 so the readiness screen and its "Saved." message actually render; without it a configured user was
forwarded straight to the console and Save appeared to do nothing.
Every field has a real label element, asserted by the responsive Playwright checks.
Related: [[First-run gate]], [[Credential vault]], [[Email collision check]].
+19
View File
@@ -0,0 +1,19 @@
# ShowPlan digest
`lib/digest.ts` converts a SQL Server ShowPlan XML document into a bounded JSON summary before any model is
consulted. The point is that the models judge facts, not guess them: [[Plan file encoding]] hands it clean text,
then it walks the parsed document for operators, tables, missing-index groups, waits and warnings.
Each operator keeps `NodeId`, the logical or physical op name, `EstimateIO`, `EstimateCPU`,
`EstimatedTotalSubtreeCost`, estimated rows, and actual rows where the plan carries
`RunTimeCountersPerThread`. Attribute spellings differ across SQL Server builds, so every cost field falls
through the known variants rather than trusting one.
`digestForModel` is the budget gate: 4 000 characters of statement text, 20 tables, 12 operators, 5 missing
index groups, 20 warnings. It also sets `sql_truncated` when the statement was cut, which stops [[Analyst model]]
from offering a rewrite of a query it never received.
Measured on a 2 694 090-byte production plan (UTF-16LE, 1 352 044 characters, 2 729 tags deep): 2 ms to decode,
56 ms to digest, 37 distinct tables, and `StatementOptmEarlyAbortReason = TimeOut` surfaced as the headline finding.
Related: [[Jev scoring]], [[Max nested tags for real plans]], [[Table names live on Object]].
+10
View File
@@ -0,0 +1,10 @@
# Smoke script
`bash scripts/smoke.sh` checks a running instance with curl and nothing else: healthz answers, the login and signup
pages render, a signed-out visit to `/app` redirects, and `/api/analyze` refuses a request without a session.
Five checks, exits non-zero on the first failure.
It works against any `BASE_URL`, and `--docker` is reserved for running it against the container once a Docker host
is available.
Related: [[Jev goal gate]], [[Running locally]], [[Container plan]].
+13
View File
@@ -0,0 +1,13 @@
# Decision: table names come from the Object child
`digestPlan` built its table list by reading `Database`/`Schema`/`Table` attributes off `<IndexScan>` and
`<TableScan>` elements. Real ShowPlan puts those attributes on the `<Object>` child. The production plan with 51
objects therefore digested to **zero tables**, and the analyst model was asked to fix a query whose tables it could
not see.
The fix reaches into the child: the scan's own attributes, else `<Object>`. The third fallback level that looked
plausible in theory turned out to be dead code and was deleted rather than kept as insurance.
Pinned by a test using the real element shape, and visible in the measured difference: 0 tables before, 37 after.
Related: [[Max nested tags for real plans]], [[ShowPlan digest]].
+15
View File
@@ -0,0 +1,15 @@
# Unit suites
`npm test` runs `node --no-warnings --test tests/*.test.ts` — 43 tests, no test framework installed, nothing
external. The analyst and Jev are exercised through injected `fetch`, so no key and no network is needed.
Constraints that shape the code, not the tests: modules loaded by bare `node --test` cannot resolve the `@`
import alias, so test-imported modules import each other by relative `./x.ts` path (which is why
`allowImportingTsExtensions` is on), and `next/headers` plus `next/navigation` are imported lazily inside
`lib/auth.ts`.
The digest suite is the one that grew teeth: it now asserts nesting depth beyond the parser default, table names
taken from `<Object>` children, character-reference decoding, six plan-file encodings, and that a truncated plan's
error names a reason.
Related: [[Playwright projects]], [[Smoke script]], [[Plan file encoding]].
+11
View File
@@ -0,0 +1,11 @@
# WebKit in QA
A `webkit` project exists for one reason: Chromium treats `http://localhost` as a secure context and will store a
`Secure` cookie there, so a chromium-only suite passes while Safari users are bounced to the login page after
signup. That is precisely the bug reported after delivery.
The spec signs up its own account, walks the wizard's Next links, and asserts the final destination is the
onboarding gate at `/setup?step=2` — proof of a live session — because a lost session would land on `/login`.
A second spec reloads a deep page and checks the session survives.
Related: [[Session cookie rules]], [[Playwright projects]], [[First-run gate]].
+15
View File
@@ -0,0 +1,15 @@
# Wiki conventions
kb/AGENTS.md is the schema layer of this wiki: the layout, the category list and the page rules. It exists so a new
page can be added without inventing structure each time.
The rules that matter: one concept per file, the file name is the link target, every page cross-references 3 to 6
others and every link must resolve, measured numbers are stated with the artifact they came from, and a decision
page names the old behaviour, its cost, the replacement, and what it broke.
The wiki is deliberately separate from the repository's own documentation. raw/ holds copies of README, the spec,
the implementation plan, the design contract and the decisions ledger - copied, never edited in place - so the pages
above have citable sources. Keeping the wiki in its own root also keeps its generated graph in kb/.ua/ instead of
overwriting the code graph at the repository root.
Related: [[Gitea remote]], [[Design contract]], [[Running locally]].
+85
View File
@@ -0,0 +1,85 @@
# qAI knowledge base — index
Wiki pages about SQL Server qAI: 34 pages across 7 categories.
Raw sources are in `raw/` (copied, never edited here). Conventions live in `AGENTS.md`. History lives in `log.md`.
## Pipeline
- [[ShowPlan digest]] — lib/digest.ts converts a SQL Server ShowPlan XML document into a bounded JSON summary before any model is
consulted.
- [[Plan file encoding]] — SSMS saves .sqlplan as UTF-16LE with a byte-order mark.
- [[Analyst model]] — lib/analyst.ts talks to any OpenAI-compatible endpoint the user pastes in settings.
- [[Jev scoring]] — Jev is a separate typed judgment model, not a second opinion in prose.
- [[OpenUI report language]] — The report is elements, not markdown.
- [[SSE stream]] — One POST to /api/analyze returns a server-sent event stream.
## Data and storage
- [[SQLite schema]] — lib/db.ts uses node:sqlite — no ORM, no driver, no native build.
- [[Lazy database open]] — Opening SQLite at module import time broke npm run build with "database is locked": Next collects page data in
parallel
- [[Analysis row lifecycle]] — A row is created before the plan is validated, so every attempt is visible in the rail.
## Security and credentials
- [[Credential vault]] — Model keys are pasted by the user into the settings screen.
- [[Session cookie rules]] — The session cookie holds a 32-byte random token;
- [[First-run gate]] — The console requires two things before it renders: an analyst base URL with model and key, and a Jev key.
## Interface
- [[Console runner]] — components/runner.tsx is the whole console: file drop with encoding-aware decode, the paste box, stage pills,
SSE readin
- [[History rail]] — The rail lists the signed-in user's runs newest first, with the settings link pinned at the bottom.
- [[Export and print]] — Two export actions, both client-side, no PDF library: a Copy button that pulls the SQL blocks out of the rendered
report
- [[Design tokens]] — app/tokens.css defines the palette and scale: dark console background, phosphor-green accent reserved for
action and sta
- [[Settings form]] — components/settings-form.tsx is the credential screen, used twice: as step 2 of the first-run guide and on
/settings.
- [[Design contract]] — design-system/sql-server-qai/MASTER.md is the contract the UI is built against: dark workbench palette, type
scale, spac
## Verification
- [[Unit suites]] — npm test runs node --no-warnings --test tests/*.test.ts — 43 tests, no test framework installed, nothing
external.
- [[Playwright projects]] — npm run e2e builds first (pree2e hook) and then drives the **production** server, not next dev.
- [[WebKit in QA]] — A webkit project exists for one reason: Chromium treats http://localhost as a secure context and will store a
Secure coo
- [[Smoke script]] — bash scripts/smoke.sh checks a running instance with curl and nothing else: healthz answers, the login and signup
pages
- [[Jev goal gate]] — scripts/jev-gate.mjs asks Jev whether a document actually builds the stated goal, before code is written from it.
It ask
## Operations
- [[Running locally]] — npm install && npm run build && npm start serves the production build on port 3000.
- [[Container plan]] — Dockerfile is a multi-stage Node 25 build — Node 25 because node:sqlite is what the data layer uses — and
docker-compose
- [[Environment variables]] — Deliberately short.
- [[Gitea remote]] — The repository is pushed to a self-hosted Gitea at http://192.168.88.39:3000/ahmed/sql-server-query-ai.git, branch
main.
- [[Wiki conventions]] — kb/AGENTS.md is the schema layer of this wiki: the layout, the category list and the page rules.
## Decisions
- [[Option type and sql to run]] — The candidate schema used to carry rewrite_sql.
- [[Placeholder SQL is dropped]] — With the new sql_to_run requirement, a report on a 100 KB statement came back containing
SELECT ...
- [[Max nested tags for real plans]] — A 2.7 MB production plan refused to import with "Maximum nested tags exceeded", which read like a corrupt export.
The pl
- [[Table names live on Object]] — digestPlan built its table list by reading Database/Schema/Table attributes off <IndexScan> and
<TableScan> elements.
- [[Output token ceiling]] — Once every option had to carry runnable T-SQL, the candidate JSON outgrew the old 2 048-token budget.
- [[Email collision check]] — Changing your email in settings wrote straight to a UNIQUE column.
## Sources
Immutable inputs, copied verbatim into `raw/`. These are the artifacts every measured number above came from.
- `raw/README.md` — the repository's own front page.
- `raw/2026-09-21-sql-server-qai-spec.md` — approved specification.
- `raw/2026-09-21-sql-server-qai-plan.md` — the 13-task implementation plan the code was built from.
- `raw/design-master.md` — the design contract.
- `raw/sdd-progress.md` — the decisions ledger every page in the Decisions category is drawn from.
+7
View File
@@ -0,0 +1,7 @@
# Operation log
## 2026-09-22 — initial wiki
- Copied 5 repository sources into `raw/` (README, spec, 13-task plan, design master, SDD ledger).
- Wrote 34 pages from the documentation, the in-code comments and the decisions ledger, grouped into 7 categories.
- Every page cross-references 3 to 6 others; measured numbers come from the artifacts named in `raw/`.
- Deliberately kept out: anything from `plan-cxpaket.sqlplan` beyond the aggregate measurements it produced.
File diff suppressed because it is too large Load Diff
+114
View File
@@ -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.
+112
View File
@@ -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.
+233
View File
@@ -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
+84
View File
@@ -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 &#xD;&#xA; (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.
Binary file not shown.