Skip to content

FE-697: Decouple turns from specifications#96

Merged
kostandinang merged 17 commits into
mainfrom
ka/fe-697-decouple-turns-from-specs
May 8, 2026
Merged

FE-697: Decouple turns from specifications#96
kostandinang merged 17 commits into
mainfrom
ka/fe-697-decouple-turns-from-specs

Conversation

@kostandinang
Copy link
Copy Markdown
Contributor

@kostandinang kostandinang commented May 6, 2026

Implements docs/design/MULTI_CHAT.md §3 Phase 1: turns are now anchored to a new chat container instead of hanging directly off specification, and a reconciliation_need queue records directed open issues between knowledge items. No user-visible change in this slice.

Substrate

  • New chat table — one kind='interview' chat per spec, plus future side-chat slots; active_turn_id mirrors the legacy spec head.
  • Nullable turn.chat_id, populated on every new write; legacy turn.specification_id retained during transition.
  • specification.primary_chat_id set transactionally with spec creation; legacy specification.active_turn_id retained.
  • reconciliation_need table with directed source_item_id / target_item_id, narrow kind (supersedes / needs_confirmation) and status (open / resolved), partial unique index gating duplicate open rows per (source, target, kind), cascade FK on knowledge_item, plus caused_by_turn_id and a nullable caused_by_patch_id placeholder for the future patch ledger.

Application changes

  • Spec creation inserts spec + interview chat in one Drizzle transaction (no observable partial state).
  • createTurn populates chat_id from the spec's primary chat and rejects a parent_turn_id whose chat differs (parent-chat consistency invariant).
  • advanceHead updates spec head and chat head in one transaction; throws and rolls back if the chat update affects 0 rows, protecting A79's dual-pointer invariant against partial-failure drift.
  • openReconciliationNeed / resolveReconciliationNeed / listOpenReconciliationNeeds add the lifecycle API. Today's knowledge_item write paths are append-only, so no production fan-out is wired in this slice — that arrives with side-chat V3, the architect loop, or the patch ledger.

Migration ordering

Migrations are numbered 0014_chat_table0017_reconciliation_need (timestamps 1776310000000+) so they slot above FE-656's 0013_annotation regardless of merge order. If FE-656 V1 lands in main first, this branch slots cleanly above. If FE-697 lands first, FE-656 V1 will need to renumber its annotation during rebase.

Canonical reconciliation

memory/SPEC.md A79 and A80 move to validated for the Phase 1 substrate. memory/PLAN.md moves the multi-chat-substrate frontier from Active to Recently Completed; the next live frontier is continuous workspace.

Unblocks

  • Side-chat V3 — cascade reads from reconciliation_need instead of ad-hoc REVISIT state.
  • Side-chat V4 — multiple persistent threads via chat table.
  • Architect / generator loop — first version becomes shippable without the patch ledger.

Verification

  • npm run verify — 0 lint, fmt, typecheck errors; 673 tests; build succeeds.
  • 11 chat-substrate tests covering schema, transactional spec creation, parent-chat consistency, head mirroring, transactional rollback, read-path equivalence.
  • 9 reconciliation-need tests covering schema, lifecycle, partial unique index, resolve-then-reopen, multi-kind-per-pair, cascade.
  • Manual fixture playback per docs/design/MULTI_CHAT.md §8 against a real .brunch/brunch.db (39 specs, 81 turns, 75 knowledge items): all backfilled correctly, dual-pointer equivalence holds for every spec. The playback caught the FE-656 timestamp collision that unit tests would have missed.

Watch

Legacy turn.specification_id and specification.active_turn_id remain alongside the new chat pointers. Cleanup migration is deferred until callers read ownership through chat_id.

Test plan

  • Verify CI passes against this branch
  • Confirm migration order with FE-656's stack — coordinate merge order with FE-672 / FE-673 if needed
  • Optional: re-run .brunch/ fixture playback after rebasing onto a current main

@kostandinang kostandinang self-assigned this May 6, 2026
@lunelson lunelson changed the base branch from ln/fe-692-planning-groundwork to graphite-base/96 May 7, 2026 10:44
kostandinang and others added 14 commits May 7, 2026 12:22
Implements V2 of the side-chat patch list per docs/design/SIDE_CHAT.md
(§5.1–5.2, §6.2, §6.3 soft tier): adds `edit`, `edge`, and `drill-down`
patch kinds to the V1 patch-list seam.

Server (edit-impact.ts, edit-route.ts, db.ts, app.ts):
- classifyEditImpact returns `none | soft | hard` based on downstream
  count and active-review-set membership.
- PATCH /api/specifications/:id/knowledge-items/:itemId applies edits
  for none/soft impact and returns previousContent/previousRationale
  for undo. Hard impact returns `updated: false` and defers to V3.
- POST /api/specifications/:id/knowledge-edges/validate validates a
  proposed edge against the D125 relation policy.
- POST /api/specifications/:id/knowledge-edges creates a valid edge.
- DELETE /api/specifications/:id/knowledge-edges removes an edge for
  the edge applier's undo handler.
- Adds getDownstreamItems, isItemInActiveReviewSet,
  updateKnowledgeItemContent, removeKnowledgeRelationship to db.ts.

Client (edit-api.ts, edit-applier.ts, route.tsx, patch-list-reducer.ts,
patch-list-host.tsx):
- Patch-list reducer extended with EditPatch / EdgePatch /
  DrillDownPatch using a distributive Omit so the discriminated union
  survives StagePatchInput.
- Three applier factories (makeEditApplier, makeEdgeApplier,
  makeDrillDownApplier) follow the makeAnnotateApplier shape; each
  returns a real undo handler:
    • edit's undo re-PATCHes with previousContent/previousRationale.
    • edge's undo DELETEs the created edge.
    • drill-down throws a clear "not yet implemented in V2" error
      rather than logging and faking success — its real D127
      detail-focus implementation lands with V3.
- EditItemResponse is a discriminated union: hard-impact responses
  don't carry previous values; updated responses always do.

Verification: npm run verify (929 tests, 13 new). Manual playback
deferred (not blocking).

Closes the user-facing piece of revisit/edit mode (Requirement 10) for
the soft-impact case. Hard-impact reshape lives with V3 reading from
FE-697's reconciliation_need queue.
Preserve edit rationale semantics, scope edge deletion to the active specification, and fail edit undo when the restore is deferred.

Co-authored-by: Cursor <cursoragent@cursor.com>
Fail edge undo when deletion is rejected and keep knowledge-edge DELETE registration on the shared route helper.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use one Zod schema for validate, create, and delete edge mutations so the route contract cannot drift across identical payloads.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the delete edge response assertion aligned with the exported return type so rejected undo paths retain their server reason.

Co-authored-by: Cursor <cursoragent@cursor.com>
Two cleanups the V2 review pass missed:

- Bind the structured-list-view scrollTo spy to a variable so the
  unbound-method rule no longer fires (latent issue exposed when
  V2's schema cascades through the type-aware lint graph; same fix
  applied to FE-697 already).
- Drop the stale `specificationId={1}` prop from PatchListProvider
  test usages — review commits removed `specificationId` from
  PatchListProviderProps but didn't update the test fixtures.

npm run verify: 0 lint errors (was 22), 935 tests pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Introduces `chat` as a turn container per docs/design/MULTI_CHAT.md §3.
Every specification now has exactly one `kind='interview'` chat; spec
creation inserts both atomically. Legacy `turn.specification_id` and
`specification.active_turn_id` are retained alongside `turn.chat_id`,
`specification.primary_chat_id`, and the mirrored `chat.active_turn_id`,
so existing spec-scoped reads keep working through the transition.

Migrations 0013-0015 add the new tables and columns, backfilling one
interview chat per existing spec and pointing both legacy and chat
pointers at the same head. New writes populate both. createTurn rejects
a parent that lives in a different chat. advanceHead mirrors the head
update to the interview chat.

reconciliation_need queue is the next slice on this branch.

Verification: npm run verify (lint, fmt:check, typecheck, 663 tests, build).
Introduces `reconciliation_need` per docs/design/MULTI_CHAT.md §3.4 §5:
a directed graph signal queue between knowledge_item rows. Each row
records `(source_item_id, target_item_id, kind, status)` with optional
`reason`, `caused_by_turn_id`, and a nullable `caused_by_patch_id`
placeholder for the future patch ledger. A partial unique index gates
duplicate `open` rows for the same (source, target, kind) triple while
still permitting the same triple to reopen after the previous one
resolves. Cascade-on-delete on both knowledge_item FKs keeps the queue
honest if items disappear.

Migration 0016 creates the table and the partial index. db.ts adds
openReconciliationNeed, resolveReconciliationNeed, and
listOpenReconciliationNeeds for the lifecycle API. Today's
knowledge_item write paths are append-only, so no production fan-out
is wired in this slice — that arrives with side-chat V3, the
architect loop, or the patch ledger.

Together with d99fb74 (chat container) this closes the multi-chat
substrate frontier item. SPEC.md A79 and A80 move to validated for
the Phase 1 substrate; I111's protecting tests are now real
(`chat-substrate.test.ts`, `reconciliation-need.test.ts`). PLAN.md
moves the frontier to Recently Completed; the next live frontier is
continuous workspace.

Verification: npm run verify (lint, fmt:check, typecheck, 672 tests, build).
Adds a Watch entry on the Recently Completed line so the manual
outer-loop migration playback against a real `.brunch/` fixture
isn't lost. Unit tests cover the migration logic but don't
exercise backfill on pre-existing data.
Wraps the spec-head and chat-head updates in a single Drizzle
transaction so partial failure can't silently break A79's
dual-pointer invariant. If the chat update affects 0 rows
(chat row missing despite primary_chat_id being set), advanceHead
now throws and the spec-head update rolls back to the previous
head instead of committing in isolation.

Also adds a one-line schema comment near `reconciliation_need_open_unique`
clarifying that omitting specification_id is sound only because
knowledge_item.id is globally unique across specs.

Verification: npm run verify (673 tests). The new rollback test
in chat-substrate.test.ts exercises the missing-chat path with FK
checks toggled to set up the corrupted state.
Manual fixture playback per docs/design/MULTI_CHAT.md §8 against a real
.brunch/brunch.db (39 specs, 81 turns, 75 knowledge items) revealed
that my migrations 0013-0016 collided with FE-656's `0013_annotation`
on timestamp 1776300000000. Drizzle's migrate decides "applied" by
timestamp alone (not hash), so on the user's DB which already had
FE-656's annotation applied, my 0013_chat_table was silently skipped
and 0014_turn_chat_id then failed with "no such table: chat".

Renumber my migrations to 0014-0017 (timestamps 1776310000000+) so
they slot above FE-656's annotation regardless of merge order:

- 0013_chat_table → 0014_chat_table
- 0014_turn_chat_id → 0015_turn_chat_id
- 0015_specification_primary_chat → 0016_specification_primary_chat
- 0016_reconciliation_need → 0017_reconciliation_need

Re-running migrations against the real fixture: 39 interview chats
backfilled (one per spec), 81 turns get chat_id, 39 specs get
primary_chat_id, and spec.active_turn_id == chat.active_turn_id holds
for every spec. PLAN.md's Recently Completed entry now reflects the
playback evidence and the 0014-0017 numbering.

Verified: npm run verify (673 tests) plus the fixture playback above.

When FE-656 V1 lands in main, this branch slots cleanly above
0013_annotation. If FE-697 lands first, FE-656 V1 will need to
renumber its annotation during rebase.
@kostandinang kostandinang force-pushed the ka/fe-697-decouple-turns-from-specs branch from eb13579 to df11855 Compare May 7, 2026 13:16
@kostandinang kostandinang changed the base branch from graphite-base/96 to ka/fe-673-side-chat-v2 May 7, 2026 13:16
Base automatically changed from ka/fe-673-side-chat-v2 to main May 7, 2026 13:38
@kostandinang kostandinang marked this pull request as ready for review May 7, 2026 14:57
@cursor
Copy link
Copy Markdown

cursor Bot commented May 7, 2026

PR Summary

Medium Risk
Medium risk due to new DB tables/columns and migrations that backfill existing data and change write paths to maintain dual pointers (spec vs chat) transactionally. Failures could impact turn creation/head advancement or introduce data drift if invariants are violated.

Overview
Introduces a multi-chat substrate by adding a new chat table (one kind='interview' chat per specification) and wiring turn.chat_id plus specification.primary_chat_id, with migrations that backfill existing specs/turns and mirror the active head into chat.active_turn_id.

Updates server write paths so spec creation inserts the spec + interview chat in one transaction, createTurn always attaches turns to the spec’s interview chat (and rejects cross-chat parents), and advanceHead transactionally updates both the legacy specification.active_turn_id and the chat head (rolling back on partial failure).

Adds a reconciliation_need table (with a partial unique index for open needs) and corresponding DB helpers/tests to open/resolve/list needs, plus a small edit-route tightening: edits are treated as hard if the item or any downstream affected item is in an active review set.

Reviewed by Cursor Bugbot for commit a4a5f4b. Bugbot is set up for automated code reviews on this repo. Configure here.

@augmentcode
Copy link
Copy Markdown

augmentcode Bot commented May 7, 2026

🤖 Augment PR Summary

Summary: Implements Phase 1 of the multi-chat substrate by introducing durable chat containers and a directed reconciliation-need queue, while keeping legacy spec-scoped turn pointers for a safe transition.

Changes:

  • Added chat table and backfilled one kind='interview' chat per specification
  • Added nullable turn.chat_id and specification.primary_chat_id, with backfills
  • Made specification creation transactional (spec + interview chat + primary_chat_id)
  • Updated turn creation to populate chat_id and enforce parent-turn chat consistency
  • Updated head-advance logic to mirror active_turn_id to the interview chat atomically
  • Introduced reconciliation_need table (directed source/target items, kind/status, partial unique index)
  • Plumbed Side-chat V2 patch kinds (edit/edge/drill-down) through patch list host and added edit/edge server routes + client appliers

Technical Notes: Adds edit-impact classification based on downstream edges / active review sets, and expands test coverage for chat substrate, reconciliation needs, and edit/edge lifecycle.

🤖 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. 2 suggestions posted.

Fix All in Augment

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

Comment thread src/server/edit-route.ts Outdated
Comment thread src/server/db.ts
kostandinang and others added 2 commits May 7, 2026 17:27
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@kostandinang kostandinang added this pull request to the merge queue May 8, 2026
Merged via the queue into main with commit 2b1f0bf May 8, 2026
6 checks passed
@kostandinang kostandinang deleted the ka/fe-697-decouple-turns-from-specs branch May 8, 2026 13:26
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.

2 participants