FE-552: Drizzle ORM and core extraction#14
Conversation
FE-552 Drizzle ORM + core extraction
Migrate raw DDL to Drizzle schema ( |
🤖 Augment PR SummarySummary: Migrates server persistence to Drizzle ORM and extracts interview orchestration into a transport-agnostic core layer. Changes:
Technical Notes: Migrations are auto-applied at startup via 🤖 Was this summary useful? React with 👍 or 👎 |
| if (existing) return existing; | ||
| const result = db.prepare('INSERT INTO project (name) VALUES (?)').run(name); | ||
| return db.prepare('SELECT * FROM project WHERE id = ?').get(result.lastInsertRowid) as Project; | ||
| const existing = db.select().from(schema.project).orderBy(schema.project.created_at).limit(1).get(); |
There was a problem hiding this comment.
src/server/db.ts:40 — getOrCreateProject() orders by created_at ascending and then selects orderBy(id).limit(1) after insert, which will return the oldest project (or the wrong row) once multiple projects exist. This can cause subsequent turn writes/head advancement to attach to an unintended project.
Severity: high
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| */ | ||
| export async function* conductTurn( | ||
| db: DB, | ||
| projectId: number, |
There was a problem hiding this comment.
src/server/core.ts:55 — conductTurn() accepts a projectId but currently ignores it and always uses getOrCreateProject(db)/project.id. If callers intend to operate on a specific project, this can create turns and advance HEAD on the wrong project.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| console.log('SSE:', event.type); | ||
| res.write(formatSSE(event)); | ||
| } | ||
| for await (const domainEvent of conductTurn(db, project.id, prompt)) { |
There was a problem hiding this comment.
src/server/app.ts:28 — The SSE handler no longer guards the for await loop with try/finally, so if conductTurn() throws outside its internal SDK try/catch (e.g., DB constraint error during createTurn()), the response may never emit [DONE] or call res.end(). This can leave clients hanging with an open SSE connection.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| return [{ type: 'start', messageId: event.messageId }]; | ||
|
|
||
| case 'thinking': { | ||
| if (currentBlock !== 'thinking') { |
There was a problem hiding this comment.
src/server/sse-adapter.ts:135 — In createDomainAdapter.translate(), if a thinking event arrives after a text block has started, currentBlock switches to 'thinking' without emitting a text-end, leaving an unterminated text block in the UI stream protocol. If upstream event ordering changes, this can produce invalid SSE sequences for clients.
Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drizzle schema (schema.ts) replaces raw DDL as single source of truth for types and migrations. Migration runner auto-applies at startup via migrate(). Interview orchestration extracted from app.ts into core.ts — conductTurn() yields AsyncIterable<DomainEvent> consumed by a thin Express SSE adapter via createDomainAdapter(). 51 tests pass (39 existing + 12 new core tests). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… columns
Replace hand-written Project/Turn/Option interfaces with InferSelectModel
from Drizzle schema. Switch is_resolution, is_recommended, is_selected to
integer({ mode: 'boolean' }) — Drizzle auto-maps to/from boolean, removing
manual 0/1 conversions. Test assertions updated to true/false.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Without this, SQLite silently ignores REFERENCES constraints, allowing orphaned rows if turns are inserted before projects exist. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Was ordering by created_at ASC (returns oldest) and using a separate query after insert. Now uses DESC ordering and .returning() for insert. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously, advanceHead ran unconditionally after catching SDK errors, leaving HEAD pointing at a turn with empty/partial content. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
09cabd2 to
dcf5c8c
Compare

chore: add FE-552 issue number to slice 3c in PLAN.md
Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com
feat: migrate to Drizzle ORM and extract core service layer
Drizzle schema (schema.ts) replaces raw DDL as single source of truth for
types and migrations. Migration runner auto-applies at startup via migrate().
Interview orchestration extracted from app.ts into core.ts — conductTurn()
yields AsyncIterable consumed by a thin Express SSE adapter
via createDomainAdapter(). 51 tests pass (39 existing + 12 new core tests).
Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com
refactor: derive types from Drizzle schema, use boolean mode for flag columns
Replace hand-written Project/Turn/Option interfaces with InferSelectModel
from Drizzle schema. Switch is_resolution, is_recommended, is_selected to
integer({ mode: 'boolean' }) — Drizzle auto-maps to/from boolean, removing
manual 0/1 conversions. Test assertions updated to true/false.
Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com
chore: clarify ln-build review routing, remove stale handoff
Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com
chore: add FE-553 issue number to slice 3d in PLAN.md
Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com