Files
sql-server-query-ai/kb/raw/sdd-progress.md
T
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

85 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.