Commit 46dfe69
feat(server): persistent draft controller + ctx.db draft seam [YW-260] (#156)
The DashFrame draft controller — the heart of the assistant drive loop.
CONDUCTS the wystack draft mechanism (`withDraft` write-path +
`compactLog` + `applyCommands`) over DashFrame's durable YW-125 tables;
does NOT reimplement YW-121.
## What it does
- `createDraftController(app, db)`: `openDraft` / `appendToDraft` /
`publishDraft` / `discardDraft` / `getDraftLog`.
- **Persistent**: a draft survives process restart with the draftId as
the durable handle (log materialized into `draft_command_log`).
- **publish = atomic command-log replay** onto canonical via
`applyCommands(commit)` — cold-safe (reads only the durable log, never
the shadow), idempotent retry (drops log before sweep).
- **ctx.db draft seam** (`withDraftSeam` in app.ts): when a `draftId` is
in context, substitutes `ctx.db` with the draft-scoped overlay so
command handlers write into `<table>__draft` UNMODIFIED.
- **Security boundary**: credentials never drafted — closed 6-table
shadow set, no `secret_mappings`/`project_meta` shadow.
## Foundation
Builds on the now-merged wystack draft foundation (`5b02c0c`: withDraft
+ lifecycle + jsonb encode/decode codecs + PK-filtered reads). Verified
end-to-end against the FIXED primitive: jsonb round-trips, PK-pinned
draft reads, cold-publish, discard sweep.
(Supersedes #153, which auto-closed when its bump base branch merged.
Rebased onto main, identical content + the read-decode foundation.)
Tracked internally as YW-260.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- greptile_comment -->
<details open><summary><h3>Greptile Summary</h3></summary>
This PR introduces the persistent draft controller
(`createDraftController`) and the `ctx.db` draft seam (`withDraftSeam` /
`createFallThroughDraftDb`) for DashFrame's agent drive loop, building
on the wystack draft foundation. The seam is dormant in this slice — no
host yet injects a `draftId` into request context — so no existing
behavior changes.
- **`createDraftController`** (new file): implements `openDraft` /
`appendToDraft` / `publishDraft` / `discardDraft` / `getDraftLog` over
real PGlite tables. The durable log (`draft_command_log`) is a
materialized compacted projection (replace-all on each append); publish
reads only the log, never the shadow, making warm and cold publish
identical. The known durability window between the canonical commit and
`deleteLog` is correctly documented and tracked as GitHub issue #157.
- **Draft seam** (`app.ts`): `createFallThroughDraftDb` routes draftable
artifact tables to `<table>__draft` and falls through to canonical for
non-draftable tables (`project_meta`, `secret_mappings`).
`buildDashframeApp` now unconditionally wraps `rawApp` to insert the
seam; the no-draft path is byte-identical to the original `rawApp.call`
decomposition.
- **`drizzle-orm` promotion**: moved from `devDependencies` to
`dependencies`, correct since `getTableName` / `eq` are used in
production runtime code.
</details>
<details><summary><h3>Confidence Score: 5/5</h3></summary>
Safe to merge — the draft seam is dormant (no host injects a draftId),
so canonical read/write paths are unchanged. The new controller adds
durable state on top of existing shadow tables with no schema-breaking
changes.
All load-bearing invariants are correct and thoroughly tested: publish
strips draftId before replay, writeLog is atomic (transactional
delete+insert), the fall-through seam correctly routes non-draftable
tables to canonical, and appendToDraft avoids the raw withDraft handle
that would bypass the seam. The publish crash window is documented and
tracked as GitHub issue #157. The cmdId column addition is nullable and
handled by the existing syncSchema bootstrapper. No ticket IDs in
source, no security regressions.
No files require special attention. The two static constant sets
(DRAFTABLE_TABLE_NAMES in app.ts and DRAFT_SHADOW_TABLES in
draft-controller.ts) must stay in sync when a new artifact table is
added — both are annotated to that effect.
</details>
<details><summary><h3>Important Files Changed</h3></summary>
| Filename | Overview |
|----------|----------|
| apps/server/src/draft-controller.ts | New persistent draft lifecycle
controller: open/append/publish/discard over real PGlite tables.
Correctly routes through fall-through seam, strips draftId before
publish replay, and uses transactional writeLog for atomic log
replacement. Known durability window now tracked as GitHub issue #157. |
| apps/server/src/app.ts | Adds draft seam: withDraftSeam,
createFallThroughDraftDb, and draftIdFromContext. The buildDashframeApp
wrapper now unconditionally wraps rawApp, decomposing rawApp.call into
createTracked + runHandler to allow draft handle injection. No-draft
path is byte-identical. |
| apps/server/src/draft-controller.test.ts | Comprehensive integration
test suite (13 cases) covering isolation, persistence/cold-reload,
atomic publish, discard, no-draft canonical path, log re-seq, jsonb
round-trip, sparse overlay, non-draftable fall-through, appendToDraft
seam routing, command correlation IDs, and mutation snapshot safety. |
| packages/server-core/src/schema.ts | Adds nullable cmd_id text column
to draft_command_log for command correlation ID persistence. As a
nullable, default-less column, it is auto-added to existing tables by
syncSchema's renderAddNullableColumnsIfNotExists — no separate migration
needed. |
| packages/server-core/src/schema.test.ts | Adds assertion for cmd_id
column in the draft_command_log schema contract test. |
| apps/server/package.json | Promotes drizzle-orm from devDependencies
to dependencies — correct, since getTableName and eq are used in
production runtime code. |
</details>
<details><summary><h3>Sequence Diagram</h3></summary>
<a href="#gh-light-mode-only">
```mermaid
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Host
participant DraftController
participant App as WyStackApp (wrapped)
participant SeamDb as FallThroughDraftDb
participant Shadow as table__draft
participant Log as draft_command_log
participant Canonical as canonical tables
Host->>DraftController: openDraft()
DraftController-->>Host: draftId (UUID)
Host->>DraftController: appendToDraft(draftId, batch)
DraftController->>App: createTracked() → baseDb
loop for each cmd in batch
DraftController->>App: runHandler(path, args, baseDb, draftId-context)
App->>SeamDb: withDraftSeam → FallThroughDraftDb
Note over SeamDb: draftable tables → shadow<br/>non-draftable → canonical
SeamDb->>Shadow: write (draftable tables)
SeamDb->>Canonical: read (non-draftable: project_meta)
App-->>DraftController: handler result
end
DraftController->>Log: readLog(draftId) → prior
DraftController->>DraftController: compactLog([...prior, ...ranSnapshots])
DraftController->>Log: writeLog (tx: delete + insert, re-seq 0..n)
DraftController-->>Host: CommandResult[]
Host->>DraftController: publishDraft(draftId)
DraftController->>Log: readLog(draftId) → compacted commands
DraftController->>App: applyCommands(app, log, commit, no-draftId-context)
App->>Canonical: atomic commit of all commands
App-->>DraftController: CommitResult
DraftController->>Log: deleteLog(draftId) [idempotency gate]
DraftController->>Shadow: sweepShadows best-effort all 6 tables
DraftController-->>Host: CommitResult
Note over Host: Host MUST fire onWrite(tablesWritten)
Host->>DraftController: discardDraft(draftId)
DraftController->>Log: deleteLog(draftId)
DraftController->>Shadow: sweepShadows throw on error
```
</a>
<a href="#gh-dark-mode-only">
```mermaid
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Host
participant DraftController
participant App as WyStackApp (wrapped)
participant SeamDb as FallThroughDraftDb
participant Shadow as table__draft
participant Log as draft_command_log
participant Canonical as canonical tables
Host->>DraftController: openDraft()
DraftController-->>Host: draftId (UUID)
Host->>DraftController: appendToDraft(draftId, batch)
DraftController->>App: createTracked() → baseDb
loop for each cmd in batch
DraftController->>App: runHandler(path, args, baseDb, draftId-context)
App->>SeamDb: withDraftSeam → FallThroughDraftDb
Note over SeamDb: draftable tables → shadow<br/>non-draftable → canonical
SeamDb->>Shadow: write (draftable tables)
SeamDb->>Canonical: read (non-draftable: project_meta)
App-->>DraftController: handler result
end
DraftController->>Log: readLog(draftId) → prior
DraftController->>DraftController: compactLog([...prior, ...ranSnapshots])
DraftController->>Log: writeLog (tx: delete + insert, re-seq 0..n)
DraftController-->>Host: CommandResult[]
Host->>DraftController: publishDraft(draftId)
DraftController->>Log: readLog(draftId) → compacted commands
DraftController->>App: applyCommands(app, log, commit, no-draftId-context)
App->>Canonical: atomic commit of all commands
App-->>DraftController: CommitResult
DraftController->>Log: deleteLog(draftId) [idempotency gate]
DraftController->>Shadow: sweepShadows best-effort all 6 tables
DraftController-->>Host: CommitResult
Note over Host: Host MUST fire onWrite(tablesWritten)
Host->>DraftController: discardDraft(draftId)
DraftController->>Log: deleteLog(draftId)
DraftController->>Shadow: sweepShadows throw on error
```
</a>
</details>
<sub>Reviews (5): Last reviewed commit: ["chore(server): drop duplicate
drizzle-or..."](59f11d7)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=39463883)</sub>
<!-- /greptile_comment -->
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent 9fa2358 commit 46dfe69
7 files changed
Lines changed: 1323 additions & 60 deletions
File tree
- apps/server
- src
- packages/server-core/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| 40 | + | |
40 | 41 | | |
41 | 42 | | |
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
45 | 46 | | |
46 | | - | |
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| 41 | + | |
41 | 42 | | |
42 | 43 | | |
| 44 | + | |
43 | 45 | | |
44 | 46 | | |
45 | 47 | | |
46 | 48 | | |
47 | 49 | | |
48 | 50 | | |
| 51 | + | |
| 52 | + | |
49 | 53 | | |
50 | 54 | | |
51 | 55 | | |
| |||
218 | 222 | | |
219 | 223 | | |
220 | 224 | | |
221 | | - | |
222 | | - | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
223 | 268 | | |
224 | | - | |
225 | | - | |
226 | | - | |
227 | | - | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
228 | 356 | | |
229 | 357 | | |
230 | 358 | | |
231 | 359 | | |
232 | 360 | | |
233 | 361 | | |
234 | 362 | | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
235 | 370 | | |
236 | 371 | | |
237 | 372 | | |
| |||
243 | 378 | | |
244 | 379 | | |
245 | 380 | | |
246 | | - | |
247 | | - | |
248 | | - | |
249 | | - | |
250 | | - | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
251 | 386 | | |
252 | | - | |
253 | | - | |
254 | | - | |
255 | | - | |
256 | | - | |
257 | | - | |
258 | | - | |
259 | | - | |
260 | | - | |
261 | | - | |
262 | | - | |
263 | | - | |
264 | | - | |
265 | | - | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
266 | 398 | | |
267 | 399 | | |
268 | 400 | | |
| |||
278 | 410 | | |
279 | 411 | | |
280 | 412 | | |
281 | | - | |
282 | | - | |
283 | | - | |
284 | | - | |
285 | | - | |
286 | | - | |
287 | | - | |
288 | | - | |
289 | | - | |
290 | | - | |
291 | | - | |
292 | | - | |
293 | | - | |
294 | | - | |
295 | | - | |
296 | | - | |
297 | | - | |
298 | | - | |
299 | | - | |
300 | | - | |
301 | | - | |
302 | | - | |
303 | | - | |
304 | | - | |
305 | | - | |
306 | | - | |
307 | | - | |
308 | | - | |
309 | | - | |
310 | | - | |
311 | | - | |
312 | | - | |
313 | | - | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
314 | 452 | | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
315 | 462 | | |
316 | 463 | | |
317 | 464 | | |
| |||
505 | 652 | | |
506 | 653 | | |
507 | 654 | | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
508 | 659 | | |
0 commit comments