Data Model: Legacy→Journal Capture Cutover
Behavioral / state model for the capture surface (revised post-squad). No new persistent schema; this documents entities, the layout state machine, and the invariants the implementation must preserve. Cutover reuses migrate_journal / project_store_migration — this model describes the states around those engines, not a new copy engine.
Entities
LayoutState (per repository-root checkout)
signal. Its presence does NOT mean "migrated" — a greenfield root gets it written on first read.
(LEGACY | CUTOVER_PENDING | PROJECT_ONLY), migration_id, updated_at.
+ marker, so the "never migrated" signal self-destructs on first emit.
greenfield root is published PROJECT_ONLY and never persists LEGACY.
.initializedmarker (marker_path): durable "this root has been initialized".layout-generation.jsonrecord (record_path):generation,mode- Current bug:
_read_lockedon a fresh root writes_initial_state()=LEGACY - Target: the no-legacy-data decision runs before that LEGACY write, so a
LegacyQueue (source, preserved)
migrate_journal.discover_source_dbs.
Consequence: honest sync now convergence (#3278) is deferred — copy-without-delete cannot converge the legacy-row boundary (documented at migrate_journal.py:40-52).
- Store: legacy
queue.db+ scopedqueue-<digest>.db, discovered via - Role: migration input only; rows never deleted (C-002, #2750 out of scope).
EventJournal / ProjectSyncStore (authoritative destination)
ProjectLayoutRequiredError) — live writes require PROJECT_ONLY.
(event_id, source_digest) (IC-00).
- Store: per-project event journal selected by ProjectSyncStore (#3293 — kept).
- Guard:
_require_project_destination(journal.py:117-119, - Dedup:
appendidempotence keys onevent_id; cutover dedup keys on
Credentials → auth signal (not a path)
gate. It must NOT drive scope_db_path(...) at preflight.py/target_authority.py (C-003/FR-009 revert-risk).
read_queue_scope_from_credentialsyields a boolean "authenticated?" for the setup-plan
Layout State Machine
marker/record absent
│
▼ detect legacy data (real reader: discover_source_dbs + row counts)
┌──────────────┐ no legacy data ┌──────────────┐
│ (unresolved) │ ────────────────────▶ │ PROJECT_ONLY │ live writes land (terminal)
└──────────────┘ (decided BEFORE └──────────────┘
│ any LEGACY persist) ▲
│ legacy data present │ migrate_journal / project_store_migration
▼ │ (stable migration_id, dedup, attribution)
┌──────────────┐ begin_cutover ┌──────────────────┐ publish_project_only
│ LEGACY │ ───────────────▶ │ CUTOVER_PENDING │ ─────────────────────▶ PROJECT_ONLY
│(migration │ │ live writes: │
│ input) │ │ block-and-retry, │ crash here ⇒ re-enter same
└──────────────┘ │ then loud (never │ migration_id, complete/retry
▲ │ silent LEGACY) │ (never brick)
│ SPEC_KITTY_NO_AUTO_CUTOVER└──────────────────┘
│ ⇒ stay LEGACY, surface loud actionable refusal (manual migrate)
Invariants (must always hold)
events (FR-001/NFR-001).
authoritative store exactly once — 0 loss, 0 duplication, incl. ownerless rows (FR-003/FR-005/NFR-002). Guaranteed by reusing the engines' copy-verify-under-lock.
migration_id and converges with no extra/divergent writes; a crash never bricks the root (NFR-005).
the current engine only unlinks temp scratch files.)
silently route to LEGACY-and-swallow — it blocks-and-retries, then surfaces loudly. (Replaces the deferred honest-sync-now invariant.)
(#3293 kept — C-003/FR-009); restoring credential parsing does not change which physical store a live write lands in.
- INV-1 (no silent success): no capture path returns success while journaling zero
- INV-2 (conservation): after cutover, every pre-existing legacy event appears in the
- INV-3 (idempotence / crash-safety): an interrupted cutover re-enters the same
- INV-4 (source preservation): legacy rows are read, never deleted (C-002). (Verified:
- INV-5 (no mid-cutover drop): a live write arriving during
CUTOVER_PENDINGmust not - INV-6 (selection preserved): ProjectSyncStore-owned queue selection is unchanged
Cutover Transitions (auto, reusing canonical engines)
1. On first live write, resolve layout (before any LEGACY persist). 2. PROJECT_ONLY → write (done). 3. Unresolved + no legacy data (real reader) → publish PROJECT_ONLY → write. 4. Legacy data present + escape hatch unset → begin_cutover (deterministic root-derived migration_id) → copy via migrate_journal/project_store_migration (dedup (event_id, source_digest), attribute project_uuid to ownerless rows, quarantine divergent payloads; source retained) → publish_project_only → write. Live writes during CUTOVER_PENDING: block-and-retry, else loud. Re-entrant on crash. 5. Legacy data present + SPEC_KITTY_NO_AUTO_CUTOVER set → do not mutate; surface a loud, actionable refusal pointing at sync project-store-migrate (never silent).