Skip to content

feat(migrate): versioned schema migrations for the on-disk KB #200

Description

@plind-junior

Labels: enhancement

What you're trying to do

The .vouch/ layout is the durable contract — yaml claims, markdown pages with frontmatter, json sessions, jsonl audit log. As soon as the pydantic models in src/vouch/models.py evolve (new field, renamed field, narrowed enum, dropped key), every KB created against an older model version becomes a load-time error. There is no version stamp on disk and no upgrade story; the only way back today is hand-editing yaml files or restoring from a backup. ROADMAP 0.2's multi-dim scopes (#2) is the first change that will actually require this, and shipping #2 without it would silently break every KB in the wild.

The other half of the gap is reversibility. Without a structured plan/apply/rollback flow, even a careful operator who does upgrade has no audit trail of which fields changed when, no way to dry-run, and no way to undo a botched migration short of git revert against the entire .vouch/ tree. Add a versioned migration runner whose plans are themselves first-class artifacts (yaml manifests in the repo + a Proposal on apply), so the upgrade goes through the same review gate as every other mutation and lands in the audit log alongside it.

Suggested shape

vouch migrate status                    # show current KB schema version + target + drift
vouch migrate plan                      # dry-run: list every file that would change + why
vouch migrate plan --to 0.3.0           # plan against a specific target version
vouch migrate apply                     # apply pending migrations, audit-logged, atomic
vouch migrate apply --yes               # skip the interactive confirm (CI)
vouch migrate rollback                  # reverse the most recent applied migration
vouch migrate verify                    # parse-load every artifact under the current version
  • New migrations/ directory at the repo root carrying yaml manifests: 0001-add-scope-spec.yaml, 0002-rename-claim-confidence.yaml, etc. Each manifest declares from_version, to_version, an ordered list of transforms (rename / default / drop / split / merge / custom-script), and a reverse block that runs rollback. The vouch repo treats this directory the same way adapters/ is treated for feat(cli): vouch install-mcp <host> — one-command adapter writer (8 hosts) #180 — additive, contributor-friendly, single-file PRs.
  • New on-disk file .vouch/schema_version (single-line semver) is the load-time check; absent file means "treat as 0.1.0" so existing KBs keep working until first migrate.
  • New module src/vouch/migrations/ holds the runner (runner.py), the manifest parser (manifest.py), atomic file-rewrite helpers (rewriter.py), and the rollback journal (journal.py). All file mutations go through the existing KBStore atomic-write path; no parallel I/O layer.
  • Apply is one transaction per artifact kind: every claim's *.yaml rewritten in a temp dir, fsynced, then atomically renamed; rollback reads .vouch/migrations/rollback-<id>.jsonl and reverses each rewrite. The audit log carries one kb.migrate.apply event per applied manifest with the file count and the rollback-journal id.
  • vouch migrate plan outputs a unified-diff-style preview per file kind plus a counts summary; --json emits a stable machine shape for CI gating.
  • The migration runner refuses to apply if the live KB has pending proposals — a clean precondition that keeps an in-flight reviewer queue from being silently rewritten under them.
  • vouch init writes the current schema_version on bootstrap; the bundle exporter (chore(deps-dev): update mypy requirement from >=1.10 to >=2.1.0 #6 / existing) embeds the version so imports can refuse mismatched bundles with a clear error pointing at vouch migrate plan --from <bundle-version>.

Acceptance

  • A KB initialised against vouch 0.1.0 loads cleanly under 0.3.0 after vouch migrate apply and every artifact round-trips through model_validate without warning.
  • vouch migrate plan against a 10k-artifact KB produces a deterministic diff in under 5 seconds; running it twice in a row produces byte-identical output.
  • vouch migrate apply followed by vouch migrate rollback returns the KB to a byte-equivalent state (modulo the audit-log entries that record the round trip); CI asserts equivalence on the fixture KB.
  • Interrupting vouch migrate apply mid-run (SIGKILL between transforms) leaves the KB at the prior version on next load — atomic rename + rollback journal prove it.
  • vouch migrate apply against a KB with pending proposals exits non-zero with an actionable message (run vouch list-pending and resolve before migrating).
  • A test that adds a synthetic 0099-test-rename.yaml manifest in tests/fixtures/migrations/, applies it to a generated KB, and asserts both forward and rollback hits the right files.

Out of scope

  • Cross-major version jumps in a single step (only consecutive manifests apply; a 0.1 → 0.5 KB walks through 0.2, 0.3, 0.4 manifests in order).
  • Custom-script transforms beyond what's expressible in the yaml manifest verbs in v1; a custom: path/to/migration.py escape hatch lands once the verb taxonomy stabilises.
  • Migrating the state.db cache (FTS5 + embeddings) — that's already disposable and rebuilt by vouch index; the migration runner just deletes it and lets the next CLI invocation rebuild.
  • Forward-porting bundles from a future version downward (only same-or-older bundles can be migrated forward to current).
  • Concurrent multi-process migrate runs — single-writer assumption, same as vouch serve.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions