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

20 KiB
Raw Blame History

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 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/ 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/ 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

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