Skip to content

FE-552: Drizzle ORM and core extraction#14

Merged
lunelson merged 8 commits into
mainfrom
ln/fe-552-drizzle-core-extraction
Apr 2, 2026
Merged

FE-552: Drizzle ORM and core extraction#14
lunelson merged 8 commits into
mainfrom
ln/fe-552-drizzle-core-extraction

Conversation

@lunelson
Copy link
Copy Markdown
Contributor

@lunelson lunelson commented Apr 1, 2026

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

@linear
Copy link
Copy Markdown

linear Bot commented Apr 1, 2026

FE-552 Drizzle ORM + core extraction

Migrate raw DDL to Drizzle schema (drizzle/schema.ts) with migration runner that auto-applies at startup. Extract interview orchestration from app.ts into core.tsconductTurn() returns AsyncIterable<DomainEvent>. Express handler becomes a thin adapter translating DomainEvents to SSE.\n\nSPEC refs: D18 (Drizzle), D19 (layered architecture), A18, A19\nPLAN ref: slice 3c\nBREADBOARD ref: docs/design/BREADBOARD.md

@lunelson lunelson changed the title chore: add FE-552 issue number to slice 3c in PLAN.md FE-552: Drizzle ORM and core extraction Apr 1, 2026
Copy link
Copy Markdown
Contributor Author

lunelson commented Apr 1, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@augmentcode
Copy link
Copy Markdown

augmentcode Bot commented Apr 1, 2026

🤖 Augment PR Summary

Summary: Migrates server persistence to Drizzle ORM and extracts interview orchestration into a transport-agnostic core layer.

Changes:

  • Add Drizzle Kit configuration (drizzle.config.ts) plus generated migrations/snapshots under drizzle/
  • Introduce Drizzle schema as the source of truth for tables and types (src/server/schema.ts)
  • Replace raw DDL + manual SQL helpers with Drizzle-backed DB access in src/server/db.ts (including boolean-mapped flag columns)
  • Add src/server/core.ts with conductTurn() that yields AsyncIterable<DomainEvent> and persists turn state
  • Update Express app to be a thin adapter that translates DomainEvents to AI SDK SSE events via createDomainAdapter()
  • Update and expand tests (new core.test.ts; existing tests now close db.$client and assert booleans)

Technical Notes: Migrations are auto-applied at startup via migrate(), and integer({ mode: 'boolean' }) is used to map SQLite integer flags to booleans.

🤖 Was this summary useful? React with 👍 or 👎

Copy link
Copy Markdown

@augmentcode augmentcode Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 4 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/server/db.ts Outdated
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();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/server/db.ts:40getOrCreateProject() 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

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread src/server/core.ts
*/
export async function* conductTurn(
db: DB,
projectId: number,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/server/core.ts:55conductTurn() 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

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread src/server/app.ts
console.log('SSE:', event.type);
res.write(formatSSE(event));
}
for await (const domainEvent of conductTurn(db, project.id, prompt)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread src/server/sse-adapter.ts
return [{ type: 'start', messageId: event.messageId }];

case 'thinking': {
if (currentBlock !== 'thinking') {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Contributor Author

lunelson commented Apr 2, 2026

Merge activity

  • Apr 2, 6:20 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Apr 2, 6:25 AM UTC: Graphite rebased this pull request as part of a merge.
  • Apr 2, 6:26 AM UTC: @lunelson merged this pull request with Graphite.

@lunelson lunelson changed the base branch from ln/fe-544-turn-tree-schema to graphite-base/14 April 2, 2026 06:23
@lunelson lunelson changed the base branch from graphite-base/14 to main April 2, 2026 06:24
lunelson and others added 8 commits April 2, 2026 06:25
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>
@lunelson lunelson force-pushed the ln/fe-552-drizzle-core-extraction branch from 09cabd2 to dcf5c8c Compare April 2, 2026 06:25
@lunelson lunelson merged commit b7cfe7c into main Apr 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant