Research: Legacy→Journal Capture Cutover

Phase 0 consolidation. Source: pre-mission research squad (gitignored work/3425-legacy-layout-research/01-root-cause-and-fix-options.md, 02-related-p0-bugs-same-surface.md, 03-open-pr-conflict-scan.md) plus live dogfood evidence (mission create emitted the #3425 warning five times).

Decision 1 — #3425 is two distinct bugs; the reproductions are mis-attributed

separate defects with separate fixes (IC-01 and IC-02).

Test A fails at default_queue_db_path() (sync/queue.py:195), which raises unconditionally (not layout-gated — the test docstring is wrong) and asserts a retired no-arg OfflineBodyUploadQueue().db_path API. Tests B & C fail because read_queue_scope_from_credentials (sync/queue.py:175) was rewritten JSON-only and no longer derives a scope from the TOML credentials the tests write, so the FR-011 auth gate (mission_setup_plan.py:1043-1075, runs before the boundary preflight) refuses an authenticated host with exit 2.

credentials-parser regression**, NOT CI-env artifacts (PR #3467's body and the first triage mislabeled them). This satisfies the charter Pre-existing Failure Reporting rule.

fail deterministically on a coherent host; (b) revert #3293 — rejected (C-003).

  • Decision: Treat the credential/queue regression and the layout silent-capture as
  • Rationale: The three red-first tests never reach the layout silent-capture path.
  • Correction recorded: the two "SAAS auth refusal" failures are a **real #3293
  • Alternatives considered: (a) treat all three as CI-env noise — rejected, they

Decision 2 — Fresh roots resolve to project_only; legacy-with-data auto-cutover

.layout-generation.json and no legacy data; a root with real legacy data is auto-migrated (lazy, on first live write).

is correct only for a root that actually holds legacy data awaiting migration; a greenfield root has none and is stuck LEGACY forever because leaving LEGACY is manual-only (sync project-store-migratebegin_cutoverpublish_project_only).

reads the legacy store and never deletes it — the source is preserved as migration input, making the mutation recoverable and low-risk (no heavyweight backup needed).

— rejected by user (they chose auto-cutover); (b) eager cutover at startup — rejected as more invasive than lazy-on-first-write with equal safety.

  • Decision: _initial_state()/resolution returns project_only for a root with no
  • Rationale: The LayoutMode.LEGACY default (sync/layout_generation.py:129-136)
  • Non-destructive: because retiring queue.db (#2750) is out of scope, cutover
  • Alternatives considered: (a) keep manual, just make failure loud (detect-and-refuse)

Decision 3 — Fold #3391: make the emitter capture failure observable

is made to succeed (Decision 2), and the emitter swallow site (sync/emitter.py:2114, _capture_to_journal) is fixed to surface a genuinely- unrecoverable failure loud-but-non-fatal — closing #3391.

"no silent-success capture" guarantee has a real gap for unrecoverable cases without the loud backstop. Depending on someone else's unstarted issue is fragile; folding it in makes the mission self-contained. Coordinate with the assignee (MOES-Media) so the fix lands once. Constraint: must stay non-fatal — a capture failure must not crash the host command; and must not warning-storm (IC-02 fixes the common case, so the loud path is exceptional).

issue, fragile); (b) leave the emitter swallow untouched and rely only on writes succeeding — rejected (leaves the unrecoverable-case silent-success gap in FR-001).

  • Decision (updated per user): FOLD #3391 into this mission. The common-case write
  • Rationale: #3391 is an assigned issue with no PR/active work, and FR-001's
  • Alternatives considered: (a) depend on #3391 landing first — rejected (unstarted

Decision 4 — Keep ProjectSyncStore-owned queue selection; rewrite Test A

that contract (drop the retired no-arg queue API) while still pinning the no-silent- capture behavior.

out of scope and would regress the per-project consent work.

  • Decision: Preserve #3293's ProjectSyncStore selection; rewrite Test A to assert
  • Rationale: #3293 was a deliberate recent landing closing #3262; reverting it is

Decision 5 — Fold the same-root cluster (minus #3278)

duplicate legacy↔journal events — IC-03). #3278 (honest sync now) is DEFERRED.

path) is out of scope; #2688 auto-migration direction is partially realized by Decision 2. Nearest architectural parent: #2144 (event durability, stijn-dejongh).

  • Decision: Fold #3476 (backfill cutover silent no-op — IC-04) and #2846 (divergent
  • Rationale: all share the legacy↔journal split root surface. #2750 (retire legacy

Decision 7 (post-squad, user) — Drop #3278 to resolve the FR-006 ↔ #2750 contradiction

contradiction — migrate_journal.py:40-52 records that import-alone never converges the legacy-row boundary (sync now refuses forever) because the convergence/cleanup step was retired (OfflineQueue has no source-path constructor). Honest sync-now therefore requires DELETING migrated legacy rows, which reopens #2750 (kept out of scope). Rather than reopen #2750 or invent new "migrated-retained" state semantics, the user chose the smallest coherent scope: keep #2750 out, defer #3278. #2846 dedup still applies (the copy must not duplicate); only sync-now reporting is deferred.

  • Decision: Defer honest sync now convergence (#3278, was FR-006) to a separate mission.
  • Rationale: the post-plan architecture lens surfaced a documented, code-level

Decision 8 (post-squad) — Reuse the canonical migration engines

project_store_migration (dedup by (event_id, source_digest), project_uuid attribution for ownerless rows, divergent-payload quarantine, documented idempotence) — never a bespoke drain/copy.

copy semantics the original plan proposed to rebuild, and their reuse is required for INV-2 conservation and ownerless-row handling (arch BLOCKER-1, MAJOR-6). Deterministic root-derived migration_id + auto source discovery (discover_source_dbs) replace the operator-supplied --migration-id/--source the engine otherwise needs.

  • Decision: Auto-cutover calls migrate_journal.migrate_queues_to_journal /
  • Rationale: charter "use canonical sources"; the engines already implement exactly the

Decision 9 (post-squad) — Close the CUTOVER_PENDING drop window; crash-safe recovery

wait; if cutover cannot complete they route to the loud emitter surface (never silent). A crash between begin and publish leaves a re-enterable CUTOVER_PENDING/FAILED state that the next run completes/retries under a documented rule — never bricking the root.

guards raise → swallowed → the exact P0, made permanent (arch BLOCKER-2, risk BLOCKER-1).

  • Decision: While mode == CUTOVER_PENDING, live writes block-and-retry within a bounded
  • Rationale: _destination returns LEGACY for CUTOVER_PENDING (268-272) → both write

Decision 10 (post-squad) — Emitter observability mechanism; detection order; auth-signal seam

_emit_for_project_context ~2334-2336 — the latter is the live-reproduced one); deliver boundary observability via a process-level captured-failure flag the command epilogue inspects, not by raising (_emit never-raises across ~30 callers).

persists LEGACY; use the real reader (discover_source_dbs), not the stub detect_legacy_rows_for_scope; data-model corrected to the .initialized marker.

the gate; it must NOT re-introduce credential→scope_db_path derivation at preflight.py/target_authority.py (the C-003/FR-009 revert).

machine-global ~/.spec-kitty (this dev box is itself a legacy root).

  • Emitter (MAJOR-3): fix BOTH swallow sites (_capture_to_journal ~2114 and
  • Detection (MAJOR-5): decide project_only for no-legacy-data roots BEFORE _read_locked
  • Auth-signal (MAJOR-4): IC-01 restores parsing to yield a boolean "authenticated?" for
  • Test isolation (MINOR-8): cutover/layout tests use isolated temp roots; never the live

Decision 6 — Conflict posture

(LOW risk); no open PR addresses #3425. Watch draft #3300 (per-project sync consent) — same family, currently no src/ edits. All target files were last modified by merged #3293 (cd3d6a91d2); nothing has re-touched them since.

  • Decision: Proceed on fix/legacy-journal-capture-cutover; rebase before landing.
  • Rationale (squad finding 03): 0 of 25 open PRs touch the six target files

Adversarial Evidence

No security-impacting dependency decision (no dependency changes). A pre-tasks adversarial-squad pass is not required by the supply-chain contract here; a squad may still be run at the post-tasks anti-laziness point-cut per campsite discipline. No contested finding outstanding.