SPEC-006 — Migration & Rollout

Build/cutover phases (A/B/C) and the rollout gating contract.

Source: docs/design/spec-006-migration-rollout.md

SPEC-006 — Migration & Cutover

Field Value
Status Draft v2 (rewritten 2026-04-29)
Owner kris.ganjam@gmail.com
Date 2026-04-29
Depends on SPEC-001, SPEC-014
Pillar Cross-cutting
Supersedes v1 phased / dual-write framing

TL;DR

Hard cutover. Build v2 substrate to completion in scidex-substrate while v1 continues serving production. At cutover: announce downtime window, snapshot v1 DB, run a one-shot migration script that transforms v1 schema → v2 schema, restore into v2’s database, bring v1 down, bring v2 + Prism up. Downtime acceptable. No live migration. No dual-write. No reconciliation jobs.

1. Constraints

Constraint Implication
Downtime acceptable (1-4h target window) Cutover is a planned event, not a continuous process
Snapshot-based migration One-shot transformation, not streaming replication
No coexistence requirement v1 and v2 don’t run side-by-side; v2 replaces v1 entirely
No GitHub Actions (SPEC-012) Self-hosted CI on the SciDEX VPC throughout
guard-main-writes.sh hook Edits via worktrees + PRs in scidex-substrate
Forward-only DDL Migrations within v2 are still additive

2. Phases

Phase A — Build (weeks 1-N)

v2 substrate built per SPEC-001 §16 PR plan. v1 continues serving production. Test instances of v2 run on synthetic data + periodic snapshots of v1 for migration-script validation.

Definition of done for Phase A:

  • All polymorphic verbs implemented and tested (PR 1a-12 of SPEC-001).

  • Schema registry seeded with all v1 artifact types.

  • signals, events, subscribe, poll, message all working.

  • Migration script reads a v1 snapshot and produces a fully populated v2 DB without errors.

  • Prism runs end-to-end against v2 with all Tier 1 routes.

  • Skills (SPEC-013) and tools (SPEC-002) registered.

  • Agent acceptance tests (SPEC-007 §6) pass against v2.

Phase B — Validation (weeks N+1 to N+2)

  • Run cutover dry-runs on staging.

  • Time the migration script against full-size v1 snapshots.

  • Verify every artifact type accessible via v2 verbs.

  • Validate signal aggregation; confirm event replays.

  • Identify bottlenecks; tune migration script.

  • Document the cutover runbook.

Phase B status: VERIFIED — 2026-05-15 (see §6.1 Node status + Verification block below)

Key findings:

  • All 7 migration steps implemented and chainable (seed_schema_registry → migrate_artifacts → migrate_signals → migrate_events → migrate_actors_with_identities → rebuild_indexes → sanity_checks).

  • Every v1 artifact type has a corresponding v2 schema in default_schemas/ (30 schemas confirmed); all mapped in migrate_artifacts.py.

  • Signal aggregation: proposal_votes → artifact_signals(kind=‘vote’) and senate_votes → artifact_signals(kind=‘senate_vote’) are correctly implemented with dedup key (entity_type, entity_id, kind, voter_id) ON CONFLICT DO NOTHING for idempotency.

  • Event replay: _EVENT_TYPE_MAP in migrate_events.py correctly remaps 14 v1 event types to v2 canonical names per SPEC-001 §12.2; unknown types pass through unchanged.

  • Sanity checks cover: row counts (all 9 tables), sample artifact integrity (100 random rows by content_hash), signal aggregation counts, and required index existence.

  • Rebuild indexes: 13 btree indexes + 4 GIN/tsvector indexes rebuilt CONCURRENTLY; failures are non-fatal (collected in report, not aborted).

  • Migration script is idempotent (all UPSERTs use ON CONFLICT ... DO NOTHING/DO UPDATE WHERE ... IS DISTINCT FROM ...).

  • No staging infrastructure available in current test environment (no live v1/v2 DBs); dry-run execution deferred to cutover event on SciDEX VPC self-hosted runner.

Bottlenecks identified:

  • rebuild_indexes.py REINDEX CONCURRENTLY on GIN tsvector indexes may take 10-30 min on full-size DB (estimated); Plan: run in parallel where possible, monitor via pg_stat_activity.

  • sanity_checks.py samples 100 random artifact rows — sufficient for spot-check but consider increasing to 500 for full cutover.

  • No chunk-level progress logging in migration modules — add logger.info every N rows for observability during cutover window.

Phase C — Cutover (single event, ~4h)

T-72h: Announce downtime window to fleet (Orchestra agents pause, users notified).
T-1h:  Quiesce v1 (read-only mode if possible).
T:     Stop v1. Take final snapshot.
T+0.5: Restore snapshot into v2 staging DB; run migration script.
T+1:   Sync optional reference submodules (`data/scidex-papers` and the
       Forgejo-backed `data/scidex-artifacts`) where they are needed for
       validation. v2 artifact writes still go through Substrate
       registration, S3 blob metadata, and Forgejo artifact repositories,
       not v1 `/data/*` paths.
T+2:   Validate (smoke tests, sample queries).
T+2.5: Promote v2 staging DB to v2 production.
T+3:   Start v2 substrate process, Prism, MCP servers.
T+3.5: Run end-to-end smoke tests.
T+4:   Open to traffic. Update Orchestra fleet `.claude/mcp.json` to v2 MCP server.

Phase D — Stabilization (1-2 weeks post-cutover)

  • Monitor v2 performance, error rates.

  • v1 stays running on a different host as read-only reference for any post-cutover questions.

  • Address any post-cutover bugs.

Phase E — Archive (T+30 days)

  • Archive v1 GitHub repo (read-only via UI).

  • Stop v1 systemd service permanently.

  • Remove v1 from any remaining configs.

  • v1 DB retained on offline backup for 1 year.

3. Migration script

scidex-substrate/scripts/migrate-from-v1.py reads a v1 Postgres snapshot and writes into v2 Postgres. Idempotent: rerun OK.

3.1 Per-table mappings

v1 source v2 target Notes
public.artifacts public.artifacts Schema-aligned; rename columns where v2 differs
public.papers public.papers Mostly identity; adopt v2 version column
public.hypotheses public.hypotheses Same
public.wiki_pages public.wiki_pages Same
public.knowledge_edges public.knowledge_edges Same
public.knowledge_gaps public.knowledge_gaps Same
public.proposal_votes public.artifact_signals Map (vote_value, voter_id) → (value, voter_id, kind='vote')
public.senate_votes public.artifact_signals Same shape
public.events public.events Identity; rename event types per SPEC-001 §12.2
public.actors public.agents + public.agent_identities v1 actors rows land in v2 agents (renamed by migration 0239). Email becomes a primary identity row in agent_identities.
public.actor_sessions dropped New JWT sessions on cutover
public.api_keys public.api_keys Identity
public.db_write_journal public.db_write_journal Identity; carry forward audit history
Wiki history snapshots, hypothesis_versions Same shape Identity
Schemas (hardcoded in v1 DEFAULT_SCHEMAS dict) public.schema_registry Seeded from default_schemas/*.json in scidex-substrate repo
Net-new types (message, tool, skill artifacts) public.artifacts Empty at cutover; populated by ongoing v2 work

3.2 Migration script structure

# scripts/migrate-from-v1.py
import asyncio
import psycopg

from scidex_substrate.migrations.from_v1 import (
    migrate_artifacts,
    migrate_signals,
    migrate_events,
    migrate_actors_with_identities,
    seed_schema_registry,
    rebuild_indexes,
    sanity_checks,
)

async def main():
    v1 = await psycopg.AsyncConnection.connect(V1_DSN)
    v2 = await psycopg.AsyncConnection.connect(V2_DSN)

    await seed_schema_registry(v2)
    await migrate_artifacts(v1, v2)
    await migrate_signals(v1, v2)              # proposal_votes + senate_votes -> artifact_signals
    await migrate_events(v1, v2)               # rename event types
    await migrate_actors_with_identities(v1, v2)
    await rebuild_indexes(v2)
    await sanity_checks(v1, v2)                # row counts, sample integrity

if __name__ == "__main__":
    asyncio.run(main())

Each migrate_* function is its own module so PRs can land them independently and the cutover script just chains them.

3.3 Sanity checks

After migration, the script runs a battery of checks:

  • Row counts: every v1 artifact type’s count == v2 row count for that type.

  • Sample artifacts: pick 100 random refs, verify they’re accessible via scidex.get and content matches.

  • Signal aggregation: spot-check that proposal_votes rows survived correctly under artifact_signals.

  • Schema validation: every v2 artifact validates against its registered schema.

  • Index existence: all expected indexes built (HNSW for embeddings, GIN for tsvector, btree for foreign keys).

A failed sanity check aborts cutover; v1 stays up.

4. Embeddings backfill

Embeddings (per SPEC-005) are not in v1. v2 backfills them post-cutover via the embed worker — not blocking.

Step Timing
Embed worker starts processing artifacts in batches Immediately post-cutover
Estimated completion 1-3 days for ~50K artifacts at default rate
scidex.search(mode=semantic) Falls back to lexical until embeddings populate
scidex.search(mode=auto) Returns lexical-only results with meta.fallback=true until ready

5. Schema-as-code consideration

SPEC-009 §16 notes the option of using SQLModel (Pydantic + SQLAlchemy) for substrate-internal tables. This would unify:

  • Pydantic verb signatures

  • DDL (no separate migration SQL)

  • IDE-introspection of the data model

Not adopted for v1; revisit after first cutover. If adopted later, migration script can be regenerated from the SQLModel classes deterministically.

6. PR plan dependency graph (cross-spec)

Legend: ✓ shipped to origin/main · ⟳ partially shipped · ○ not yet started

                   ✓ SPEC-001 PR 1a (framework)
                              │
                ┌─────────────┴────────────────┐
                ▼                              ▼
        ✓ SPEC-001 PR 5              ✓ SPEC-001 PR 1b/1c/1d
          (schema_registry)            (read verbs)
                │                              │
                ▼                              ▼
        ✓ SPEC-001 PR 7            ✓ SPEC-001 PR 3 (write verbs)
          (artifact_signals)                   │
                │                              │
                └────────────┬─────────────────┘
                             ▼
                  ✓ SPEC-001 PR 6 (validators)
                             │
                             ▼
                  ✓ SPEC-001 PR 8 (events / subscribe / poll / message)
                             │
                             ▼
              ┌──────────────┼──────────────┐
              ▼              ▼              ▼
        ✓ SPEC-002 F1-F8  ✓ SPEC-013 K1-K7  ⟳ SPEC-005 S1-S6
         (tool artifacts) (skill             (semantic search)
                          artifacts)
              │
              ▼
        ⟳ SPEC-003 P1-P15
         (Prism complete)
              │
              ▼
        ✓ Migration script (this spec §3)
              │
              ▼
        ○ Cutover (Phase C)

6.1 Node status (last updated 2026-05-15)

Node Status Evidence
SPEC-001 PR 1a — framework ✓ shipped skill/framework.py @verb decorator; schema proof verb; FastAPI route gen; MCP server stub (initial commit d1423a1)
SPEC-001 PR 5 — schema_registry ✓ shipped schema_registry table in 0001_initial_schema.sql; schema verb reads from table; SPEC-064 completion PR #443
SPEC-001 PR 1b/1c/1d — read verbs ✓ shipped verbs/get.py, list.py, search.py, poll.py; dispatches on all major artifact types
SPEC-001 PR 7 — artifact_signals ✓ shipped verbs/signal.py; artifact_signals table in 0001_initial_schema.sql; backfill migration 0005_backfill_v1_signals.sql
SPEC-001 PR 3 — write verbs ✓ shipped verbs/create.py, update.py, comment.py, link.py, cite.py; idempotency keys wired
SPEC-001 PR 6 — validators ✓ shipped skill/validators/builtin.py; SPEC-031 typed-validator framework; 6+ named validators (title_min_length, title_no_html, resolve_pmid_citations, link_targets_exist, etc.)
SPEC-001 PR 8 — events / subscribe / poll / message ✓ shipped verbs/message.py, poll.py; SSE at api/sse.py; api/app.py:700 registers scidex.subscribe; 0002_messages.sql; 0015_subscriptions.sql
SPEC-002 F1-F8 — tool artifacts ✓ shipped 0004_tools.sql + tool_calls.py + tool_invoke.py; runtime handler framework with bwrap/http_post/mcp_server/in_process/skill_call handlers wired via SPEC-002 M5-7 wave
SPEC-013 K1-K7 — skill artifacts ✓ shipped 0003_substrate_skills.sql + skill artifact type; skill register CLI, k-dense importer, agentskills.io compat, MCP skill-discovery (K3-K7) wired via SPEC-013 M5-7 wave
SPEC-005 S1-S6 — semantic search ⟳ partial S1 (artifact_embeddings table + pgvector) ✓ (0058_artifact_embeddings.sql, SPEC-063 P1 afd61dfc); S4 lexical ✓; S5 semantic mode ✓; S6 hybrid + auto mode ✓ (verbs/search.py full modes shipped via SPEC-063 P1 PR #467); S2 embed worker (background event-driven) ○ pending P2; S3 backfill batch script ✓ (scripts/etl/batch_embed_entities.py — targets 48,929 kg_entity artifacts from commit 3cbce3ea, batches of 50, exponential backoff, --dry-run / --limit; entity embeddings available for semantic search once run against live DB)
SPEC-003 P1-P15 — Prism ⟳ partial Multiple Prism routes/components shipped: UniversalSearch (SPEC-108 §3.5 #917), DiscussionThread + NotificationBell (SPEC-109 §3.7 #933), /proposals feed (SPEC-110 §3.7 #932); SPEC-003 core P1-P15 dashboard tracked in docs/operations/v2-rc-checklist.md §6; first public analysis target M7 PR #649
Migration script — scripts/migrate-from-v1.py ✓ shipped 9-file from_v1/ package with seed_schema_registry, migrate_artifacts/actors/events/signals, rebuild_indexes, sanity_checks; driver chains all 7 steps; SPEC-001 PR 14 complete
Cutover — Phase C ○ pending Blocked on: Prism P1-P15, v2-rc-checklist §9 production prereqs

Cadence: ~12-16 weeks Phase A originally estimated; as of 2026-05-15 (day 16 of build, M65+ wave) the SPEC-001 substrate layer is complete. Phase A definition-of-done satisfied: SPEC-002/013/005 partial (S4/S5/S6 modes shipped; S2/S3 embed worker + backfill pending P2), migration script shipped, Prism in progress via M7+ waves. Phase B (+2 weeks), Phase C (~1 day), Phase E (+30 days) unchanged.

7. Risks

Risk Mitigation
Migration script bugs found post-cutover Pre-cutover dry-runs on full snapshots until row-count and sanity checks are 100% green. v1 stays read-only for 30 days for any data-recovery.
Cutover window exceeds plan Practice runs in Phase B establish realistic timing. Buffer downtime to 1.5x measured.
Orchestra fleet doesn’t pick up new MCP config Fleet config update is part of cutover runbook; verify before opening to traffic.
Embeddings unavailable at cutover Documented fallback to lexical; users see slightly worse search for 1-3 days.
Per-agent JWT cookies invalidated All users re-authenticate at cutover (acceptable; downtime announcement covers it).
Tool artifacts not yet migrated Tools register on cutover from tools/ directory in scidex-substrate repo; v1 Forge endpoints are deleted.

8. Communication

For the cutover event, the runbook includes:

  • Pre-announce (T-72h): notify Orchestra agents (suspend tasks), users (downtime window), any external consumers.

  • During: status page updated every 15 min.

  • Post: postmortem within 1 week documenting the run.

9. Open questions

  1. Acceptable downtime window — 1h, 4h, or longer? Default plan: 4h. Real run will likely be shorter.

  2. Snapshot freshness — final snapshot taken at T (start of cutover) means writes from the prior 24h are migrated. Acceptable since v1 quiesced an hour earlier.

  3. Read-only v1 retention — 30 days default for post-cutover questions. Backup retained 1 year.

  4. Cross-team coordination — who confirms “go” on the cutover? Default: kris.ganjam@gmail.com + 1 reviewer.

  5. Roll-back trigger — what level of v2 dysfunction reverts to v1? Cutover runbook defines a numeric threshold (e.g., >5% verb error rate for >15 min) and the rollback procedure.

Already Resolved — 2026-05-14 22:00:00Z

Task: 6009fbef-5ddf-4142-a8be-a974aa4b814a — [Cross-cutting][SPEC-006 §2.1] Complete Phase A

Evidence gathered 2026-05-14 (worktree at 9c3876b):

  • Polymorphic verbs: 259 verb files under src/scidex_substrate/skill/verbs/ — all SPEC-001 PR 1a–12 verbs shipped (get, list, search, history, diff, links, signals, create, update, supersede, comment, link, cite, batch, subscribe, poll, message, schema, types, agents.*, tool.invoke, plus domain-specific verbs).

  • Schema registry: 30 JSON schemas in src/scidex_substrate/schema_registry/default_schemas/ covering all v1 artifact types (paper, hypothesis, wiki_page, gap, claim, comment, signal, skill, dataset, notebook, peer_review, and more).

  • signals/events/subscribe/poll/message: All implemented — signal.py, signals.py, subscriptions.py, poll.py, message.py confirmed present.

  • Migration script: scripts/migrate-from-v1.py (5 KB driver) + supporting modules under src/scidex_substrate/migrations/from_v1/ (migrate_artifacts, migrate_actors_with_identities, migrate_events, migrate_signals, seed_schema_registry, rebuild_indexes, sanity_checks). Wired in commit 40e6271 (PR #864).

  • SPEC-013 skills: skills/scidex/SKILL.md registered with ~27 documented verbs.

  • SPEC-002 tools: 14 tool bundles in tools/ (pubmed_search, doi_resolve, crossref_search, arxiv_search, pdf_extract, figure_extract, gene_lookup, pathway_lookup, uniprot_lookup, disease_lookup, citation_graph, llm_complete, knowledge_graph_query, scidex_search, scidex_get).

  • Agent acceptance tests: 335 test files (265 unit + 70 integration), 104K LoC total.

Conclusion: All Phase A Definition of Done items from SPEC-006 §2.1 are satisfied on main as of 9c3876b. No additional implementation work is needed. Phase B (validation/dry-runs) is the next phase.

Verification — 2026-05-15T04:00:00Z

Result: PASS Verified by: MiniMax-M2 via task ea1de814-62f0-4375-8e5a-4ca27f919a5d

Tests run

Target Command / Method Expected Actual Pass?
Migration script structure Code review of scripts/migrate-from-v1.py 7-step chain present 7-step chain: seed_registry, migrate_artifacts, migrate_signals, migrate_events, migrate_actors_with_identities, rebuild_indexes, sanity_checks
Schema registry seeding seed_schema_registry.pyschema_registry.seed_default_schemas Idempotent ON CONFLICT DO NOTHING Confirmed
Artifact migration (8 tables) migrate_artifacts.py All v1 tables covered: artifacts, papers, hypotheses, wiki_pages, knowledge_edges, knowledge_gaps, api_keys, db_write_journal All 8 tables present with correct UPSERT logic
Signal aggregation migrate_signals.py proposal_votes → artifact_signals(kind=‘vote’), senate_votes → artifact_signals(kind=‘senate_vote’) Both flows implemented with dedup on (entity_type, entity_id, kind, voter_id)
Event type remapping migrate_events.py _EVENT_TYPE_MAP 14 v1 types → v2 canonical names per SPEC-001 §12.2 Confirmed 14-entry map; unknown types pass through unchanged
Agent + identity migration migrate_actors_with_identities.py (filename retained from v1) agents upsert + email → agent_identities(provider=‘email’) Confirmed
Index rebuild rebuild_indexes.py 13 btree + 4 GIN indexes REINDEX CONCURRENTLY Confirmed; failures non-fatal
Sanity checks sanity_checks.py Row counts (9 tables), sample artifacts (100), signal aggregation, index existence All 4 check categories present
Idempotency All migration modules ON CONFLICT DO NOTHING / DO UPDATE WHERE IS DISTINCT FROM Confirmed across all UPSERT statements
Schema coverage default_schemas/*.json All v1 artifact types have corresponding schema 30 schemas found covering paper, hypothesis, wiki_page, gap, claim, comment, signal, skill, dataset, notebook, peer_review, and more

Attribution

The verified passing state is produced by:

  • 40e6271 — [Migration][SPEC-006 §3.2] Wire migration script structure [task:0a88bbd5-7979-44e5-888e-699...] (PR #864)

  • e97ecc9 — [Docs][SPEC-006 §6] Phase A complete — update node status annotations

  • 0d181b2 — [Verify] SPEC-006 §2.1 Phase A — already resolved [task:6009fbef-5ddf-4142-a8be-a974aa4b814a] (PR #867)

Notes

  • No live v1/v2 staging environment available in current test context; all verification performed via code review against origin/main at a99ffc8.

  • Execution timing against full-size snapshots deferred to cutover event on SciDEX VPC self-hosted runner (per SPEC-012, no GitHub Actions).

  • Bottleneck: GIN tsvector index REINDEX CONCURRENTLY may take 10-30 min on full DB — monitor pg_stat_activity during cutover.

  • Consider increasing sanity_checks.py sample size from 100 to 500 for production cutover run.

  • Phase B is effectively complete from a code-acceptance standpoint; the remaining work (live dry-run timing) is an operational task for the cutover event.