Skip to content

feat(migration): versioned schema_version tracking and atomic vouch m…#163

Open
greatjourney589 wants to merge 4 commits into
vouchdev:testfrom
greatjourney589:feat/vouch-migrate-atomic-schema-versioning
Open

feat(migration): versioned schema_version tracking and atomic vouch m…#163
greatjourney589 wants to merge 4 commits into
vouchdev:testfrom
greatjourney589:feat/vouch-migrate-atomic-schema-versioning

Conversation

@greatjourney589

@greatjourney589 greatjourney589 commented Jun 4, 2026

Copy link
Copy Markdown

What changed

Closes #162

Adds versioned on-disk format tracking and an atomic vouch migrate command. A new VOUCH_SCHEMA_VERSION constant in models.py is the single source of truth for the schema the installed vouch expects. KBStore.init() writes schema_version into config.yaml; KBStore.__init__() reads it back and raises SchemaMismatchError when there is a mismatch instead of letting Pydantic throw a cryptic ValidationError at read time. A new src/vouch/migration.py module provides a Migration registry, a Transform protocol, and migrate_kb() — which writes all migrated artifacts to a sibling temp directory, validates every file under the target Pydantic models, then atomically renames the directory into place (or rolls back completely on any failure). bundle.py stamps schema_version into every exported manifest and rejects bundles built by a newer vouch. health.doctor() surfaces a critical finding on mismatch.

Why

Without a migration path, any breaking change to a Pydantic model (a new non-defaulted field, a renamed field) silently corrupts every existing KB at read time. There is no vouch migrate, no version header in any artifact, and no recovery short of manually patching every YAML file. This is the last hard blocker before 1.0 can freeze the on-disk format. Closes the migration story listed under 0.1 in ROADMAP.md.

What might break

  • KBStore.__init__() now raises SchemaMismatchError when config.yaml contains a schema_version that does not match the installed vouch. Any code that was silently succeeding despite a schema mismatch will now get a loud error. The fix is vouch migrate.
  • KBs with no schema_version in config.yaml (all existing KBs before this PR) are treated as "0.1" and pass the guard without requiring migration — no action needed for current users.
  • bundle.import_check now rejects bundles whose schema_version is newer than the installed vouch. Bundles with no schema_version field (pre-this-PR) are accepted unchanged.
  • health.status() gains a schema_version key — additive, no existing callers break.

VEP

proposals/VEP-0004-migration.md (draft, included in this PR per the surface-change requirement).

Tests

  • make check passes locally (lint + mypy + pytest)
  • New / changed behaviour has a test
    • tests/test_migration.py — round-trip with transforms, dry-run, rollback on validation failure, no-op, transform factories, missing-KB error
    • tests/test_storage.pySchemaMismatchError fires on version mismatch, missing schema_version is allowed, init writes the field
    • tests/test_bundle.py — export stamps schema_version, future-version bundle is rejected, pre-VEP bundle is accepted
  • CHANGELOG.md updated under ## [Unreleased]

Summary by CodeRabbit

Release Notes

  • New Features

    • Added knowledge base schema versioning with atomic migration support
    • New vouch migrate command to upgrade KB format between versions, with --backup and --dry-run options
    • Bundle schema validation during import to prevent incompatible versions
    • Schema version reporting in health status checks
  • Documentation

    • Introduced VEP-0004 design proposal for versioned on-disk format migration with atomic rollback

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b6bab023-1b66-4972-953b-9f4ac3eb7ee3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements VEP-0004: schema versioning and on-disk migration for vouch KBs. It introduces a VOUCH_SCHEMA_VERSION constant, storage-layer validation that detects schema mismatches on KB initialization, a migration engine supporting chained transforms and atomic rollback, bundle-level version checks, a vouch migrate CLI command, and health reporting/validation, with comprehensive test coverage across all surfaces.

Changes

Versioned KB migration with atomic rollback

Layer / File(s) Summary
VEP-0004 design specification
proposals/VEP-0004-migration.md
Specifies schema versioning rules, vouch migrate CLI interface and behavior, migration registry and chained-transform algorithm with atomic swap guarantees, bundle and health integration, compatibility rules (defaulting missing versions to 0.1), and error semantics.
Schema version constant
src/vouch/models.py
Introduces VOUCH_SCHEMA_VERSION = "0.1" as a centralized module constant.
Storage-layer schema validation
src/vouch/storage.py, tests/test_storage.py
Adds SchemaMismatchError exception, extends KBStore.__init__ to validate stored schema against installed version (treating missing versions as legacy-compatible), includes schema version in starter config, and tests schema persistence and mismatch detection.
Migration engine
src/vouch/migration.py
Implements Transform and Migration abstractions, a migrate_kb() orchestration function that chains version steps, applies transforms to YAML and Markdown frontmatter, validates migrated artifacts via Pydantic, performs atomic directory swaps with rollback on failure, supports dry-run mode, and provides rename_field() and add_default() helper factories for defining migration steps.
Migration test suite
tests/test_migration.py
Comprehensive coverage: _chain path resolution, no-op behavior, transform factory behavior, end-to-end round-trip with schema bump, dry-run reporting, rollback on validation failure, and missing KB directory error case.
Bundle schema support
src/vouch/bundle.py, tests/test_bundle.py
Updates bundle export to include schema_version in manifest, adds import-time validation to reject bundles with newer schema versions, maintains backward compatibility for pre-schema bundles, and tests all three cases.
CLI error handling and migrate command
src/vouch/cli.py
Adds SchemaMismatchError handling in _cli_errors() and _load_store() (exit code 3 with migration hint), implements vouch migrate command with --dry-run, --backup (timestamped copy), and version overrides, reads stored schema version from config (defaulting to "0.1"), reports changed files, and prints completion status when not in dry-run.
Health reporting
src/vouch/health.py
status() now reports KB's persisted schema_version in machine-readable dict; doctor() compares stored version against installed version and emits schema_version_mismatch error finding with migration guidance.
Server integration
src/vouch/server.py, src/vouch/jsonl_server.py
Both server surfaces extend their _store() helpers to handle SchemaMismatchError, converting it to RuntimeError for consistent error propagation.

Sequence Diagram

sequenceDiagram
  participant KB as KB Directory<br/>(.vouch/)
  participant migrate_kb
  participant migrate_fn as migrate_kb()
  participant temp as Temp<br/>Directory
  participant validate as _validate_<br/>migrated
  participant swap as _atomic_<br/>swap
  
  KB->>migrate_kb: discover root, read config
  migrate_kb->>migrate_kb: compute migration steps
  migrate_kb->>temp: copy KB & apply transforms
  temp->>validate: load YAML & Markdown
  alt validation succeeds
    validate-->>migrate_kb: artifacts valid
    migrate_kb->>swap: temp dir ready
    swap->>KB: atomic rename (temp → .vouch)
    swap-->>migrate_kb: success
  else validation fails
    validate-->>migrate_kb: errors collected
    migrate_kb->>temp: clean up temp dir
    migrate_kb->>KB: original untouched, schema_version unchanged
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • vouchdev/vouch#108: Adds vouch migrate CLI and migration engine framework in parallel code areas (src/vouch/cli.py, migration modules).
  • vouchdev/vouch#34: Both modify src/vouch/bundle.py's import_check validation gates; main PR adds schema version compatibility check, while that PR validates bundle members against Pydantic models.

Suggested reviewers

  • plind-junior

Poem

🐰 Versioned paths and atomically swapped,
No halfway migrations—all-or-nothing, locked.
Schema steps chain, transforms flow and align,
While temp directories keep the rollback design.
From zero-point-one, vouch grows versioned and wise! 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title references schema versioning and atomic migration, which are core to the PR, but is truncated and incomplete. Provide the full title or clarify the complete intent; the truncation ('vouch m…') obscures the specific feature or command being added.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (3)
src/vouch/migration.py (1)

153-168: 💤 Low value

.vouch-pre-migrate is not cleaned up after successful swap.

After a successful atomic swap, the .vouch-pre-migrate directory (containing the pre-migration KB) persists indefinitely. While this could serve as a manual recovery option, it:

  1. Wastes disk space (potentially duplicating the entire KB)
  2. Could confuse users who discover the directory

Consider cleaning it up after successful swap, or documenting this behavior as an intentional rollback mechanism.

♻️ Option A: Clean up after successful swap
 def _atomic_swap(kb_dir: Path, tmp_dir: Path) -> None:
     """Replace kb_dir with tmp_dir atomically.

     Renames kb_dir → kb_dir/../.vouch-pre-migrate, then tmp_dir → kb_dir.
     On POSIX this pair of renames is as atomic as the filesystem allows.
     """
     pre = kb_dir.parent / ".vouch-pre-migrate"
     if pre.exists():
         shutil.rmtree(pre)
     kb_dir.rename(pre)
     try:
         tmp_dir.rename(kb_dir)
+        shutil.rmtree(pre)
     except Exception:
         # Best-effort rollback of the first rename.
         pre.rename(kb_dir)
         raise
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/migration.py` around lines 153 - 168, The function _atomic_swap
currently leaves the pre-migration folder (pre = kb_dir.parent /
".vouch-pre-migrate") behind after a successful swap; modify _atomic_swap so
that after tmp_dir.rename(kb_dir) completes successfully it removes the pre
directory (e.g., call shutil.rmtree(pre) or shutil.rmtree(pre,
ignore_errors=True)); keep the existing rollback behavior in the except branch
(pre.rename(kb_dir) then raise) and ensure the cleanup runs only on success (not
on exception) to avoid losing the rollback snapshot.
src/vouch/server.py (1)

54-55: 💤 Low value

Consider including migration hint in the error message.

The SchemaMismatchError class docstring mentions "Run vouch migrate", but str(e) only returns the first line of the exception message. Unlike KBNotFoundError handling (lines 50-53) which appends a helpful hint, this handler doesn't provide actionable guidance.

However, reviewing the upstream contract in storage.py:68-72, the exception message itself should include the migration instruction, so this is likely fine. If the base message doesn't include the hint, consider adding it explicitly like the KBNotFoundError case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/server.py` around lines 54 - 55, The SchemaMismatchError except
block currently raises RuntimeError(str(e)) without the migration hint; update
the handler in server.py (the except SchemaMismatchError as e block) to append
the migration instruction (e.g. "Run `vouch migrate`") to the error
message—mirroring the KBNotFoundError handling—while preserving exception
chaining (raise RuntimeError(f"{str(e)} — Run `vouch migrate`") from e) so
callers get both the original message and the actionable migration hint.
src/vouch/cli.py (1)

734-739: 💤 Low value

Backup is created even during dry-run.

When --backup and --dry-run are both specified, the backup is still created (line 734-739 executes before the dry-run check at line 741). This may be surprising since dry-run typically implies no disk writes. Consider either:

  1. Documenting this behavior in the help text
  2. Skipping backup creation during dry-run
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/cli.py` around lines 734 - 739, The backup block currently runs
unconditionally when backup is True (uses ts, backup_dir, shutil.copytree,
click.echo), causing writes even for dry-run; change the logic so backup is only
created when not dry_run (e.g., guard the backup code with "if backup and not
dry_run:" or move the backup block to run after the dry-run check), or
alternatively update the CLI help text for the backup option to state that
backups occur during dry-run; reference the variables/operations kb_dir, root,
shutil.copytree, backup, and dry_run when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@proposals/VEP-0004-migration.md`:
- Around line 147-148: Update the VEP wording to match the implementation:
change the statement about health.doctor() emitting a `critical`-severity
finding for schema version mismatches to say it emits an `error`-severity
finding, referencing the `schema_version_mismatch` finding produced by
health.doctor() (compare to VOUCH_SCHEMA_VERSION) so the VEP and the
implementation use the same severity vocabulary.
- Around line 85-87: The fenced code block containing the CLI usage line `vouch
migrate [--dry-run] [--backup] [--from VERSION] [--to VERSION]` lacks a language
tag and triggers markdownlint MD040; update the fence to include an appropriate
language identifier such as "bash" or "sh" (e.g., replace the opening ``` with
```bash) so the block is correctly annotated for CLI/shell content.
- Around line 129-133: The atomic swap steps are inconsistent because
`.vouch/.migrate-tmp/` is moved when `.vouch/` is renamed; update the spec so
the temp output lives outside `.vouch/` (e.g., `.vouch-migrate-tmp/` or
`.migrate-tmp/` as a sibling) and then perform the swap as: write all migrated
artifacts to the sibling temp dir, validate via the target Pydantic models,
rename `.vouch/` → `.vouch-pre-migrate/`, then rename the sibling temp dir →
`.vouch/`; alternatively keep the current temp name but reverse/adjust the
rename ordering to avoid moving the temp directory.

In `@src/vouch/bundle.py`:
- Around line 261-269: The comparison of bundle_schema to VOUCH_SCHEMA_VERSION
in import_check is unsafe because bundle_schema is untrusted and may be
non-string or a dotted version string; change import_check to normalize and
validate the incoming schema: coerce bundle_schema to str only if not None,
parse both bundle_schema and VOUCH_SCHEMA_VERSION with a robust version parser
(e.g., packaging.version.parse) and compare Version objects, catching any
exceptions (TypeError, ValueError) from parsing; on parse/compare failure return
an ImportCheckResult with a clear error message instead of raising, referencing
the bundle_schema variable, the import_check function, the VOUCH_SCHEMA_VERSION
constant, and the ImportCheckResult constructor so callers (vouch import-check /
kb_import_check / _h_import_check) receive a safe result.

In `@src/vouch/cli.py`:
- Around line 716-718: The call to discover_root(Path(path)) can raise
KBNotFoundError and will produce an unhelpful traceback; wrap the call in a
try/except that catches KBNotFoundError and emits the same friendly hint/exit
behavior used by _load_store() (or simply call and use _load_store() instead of
discover_root here), then proceed to compute kb_dir and cfg_path only after
recovering the root; reference the discover_root function and KBNotFoundError in
the handler so the user sees a clear message and the process exits cleanly.

In `@src/vouch/migration.py`:
- Around line 129-140: The validate loop uses p.parent.name in (s for s in
["sources"]) which mismatches the skip logic in _copy_tree_for_migration that
checks the loop variable sub == "sources", causing nested files like
sources/abc/some.yaml to be validated incorrectly; change the skip condition in
the validator loop to the same approach as the copier (check sub == "sources")
and simplify the expression (remove the generator expression), i.e., skip
validation when sub == "sources" or equivalent, keeping the rest of the flow
(use VALIDATORS.get(sub) and the existing try/except) unchanged.

In `@tests/test_bundle.py`:
- Around line 357-383: The test should assert the explicit acceptance path
instead of only checking for absence of the substring "newer": replace the final
assertion with a direct acceptance check (e.g., assert result.ok) and optionally
assert no issues (e.g., assert not result.issues) so the test validates
bundle.import_check returns an accepted result for the pre-schema manifest;
update the assertion in test_import_check_accepts_bundle_without_schema_version
to check result.ok (and/or empty result.issues) against the manifest built for
the test.

In `@tests/test_migration.py`:
- Around line 225-232: The test asserts that the temp directory ".migrate-tmp"
does not exist, but the migration code creates/cleans ".vouch-migrate-tmp" (see
src.vouch.migration usage); update the assertion in the test (around the
on_disk/tmp-dir checks referencing kb_dir) to assert that (kb_dir /
".vouch-migrate-tmp").exists() is False (or use .exists() and negate) so the
test actually verifies that the real migration temp directory used by the
migration logic was removed. Also keep the surrounding checks (on_disk content
and config schema_version) unchanged.

---

Nitpick comments:
In `@src/vouch/cli.py`:
- Around line 734-739: The backup block currently runs unconditionally when
backup is True (uses ts, backup_dir, shutil.copytree, click.echo), causing
writes even for dry-run; change the logic so backup is only created when not
dry_run (e.g., guard the backup code with "if backup and not dry_run:" or move
the backup block to run after the dry-run check), or alternatively update the
CLI help text for the backup option to state that backups occur during dry-run;
reference the variables/operations kb_dir, root, shutil.copytree, backup, and
dry_run when making the change.

In `@src/vouch/migration.py`:
- Around line 153-168: The function _atomic_swap currently leaves the
pre-migration folder (pre = kb_dir.parent / ".vouch-pre-migrate") behind after a
successful swap; modify _atomic_swap so that after tmp_dir.rename(kb_dir)
completes successfully it removes the pre directory (e.g., call
shutil.rmtree(pre) or shutil.rmtree(pre, ignore_errors=True)); keep the existing
rollback behavior in the except branch (pre.rename(kb_dir) then raise) and
ensure the cleanup runs only on success (not on exception) to avoid losing the
rollback snapshot.

In `@src/vouch/server.py`:
- Around line 54-55: The SchemaMismatchError except block currently raises
RuntimeError(str(e)) without the migration hint; update the handler in server.py
(the except SchemaMismatchError as e block) to append the migration instruction
(e.g. "Run `vouch migrate`") to the error message—mirroring the KBNotFoundError
handling—while preserving exception chaining (raise RuntimeError(f"{str(e)} —
Run `vouch migrate`") from e) so callers get both the original message and the
actionable migration hint.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d8321cc3-5583-4714-9a47-22178dc48202

📥 Commits

Reviewing files that changed from the base of the PR and between 3beb821 and bd66e82.

📒 Files selected for processing (12)
  • proposals/VEP-0004-migration.md
  • src/vouch/bundle.py
  • src/vouch/cli.py
  • src/vouch/health.py
  • src/vouch/jsonl_server.py
  • src/vouch/migration.py
  • src/vouch/models.py
  • src/vouch/server.py
  • src/vouch/storage.py
  • tests/test_bundle.py
  • tests/test_migration.py
  • tests/test_storage.py

Comment thread proposals/VEP-0004-migration.md Outdated
Comment thread proposals/VEP-0004-migration.md Outdated
Comment thread proposals/VEP-0004-migration.md Outdated
Comment thread src/vouch/bundle.py Outdated
Comment thread src/vouch/cli.py Outdated
Comment thread src/vouch/migration.py
Comment thread tests/test_bundle.py Outdated
Comment on lines +357 to +383
def test_import_check_accepts_bundle_without_schema_version(store: KBStore, tmp_path: Path) -> None:
"""Pre-VEP-0004 bundles (no schema_version field) must still import."""
import hashlib as _hl
bundle_path = tmp_path / "old.tar.gz"
payload = b"id: c1\n"
manifest = {
"spec": bundle.SPEC_VERSION,
"bundle_id": "abc",
"files": [
{"path": "claims/c1.yaml", "size": len(payload),
"sha256": _hl.sha256(payload).hexdigest()}
],
"counts": {},
"safety": {},
}
with tarfile.open(bundle_path, "w:gz") as tar:
mf_bytes = json.dumps(manifest).encode()
mf_info = tarfile.TarInfo(bundle.MANIFEST_NAME)
mf_info.size = len(mf_bytes)
tar.addfile(mf_info, io.BytesIO(mf_bytes))
info = tarfile.TarInfo("claims/c1.yaml")
info.size = len(payload)
tar.addfile(info, io.BytesIO(payload))

result = bundle.import_check(store.kb_dir, bundle_path)
# No schema version rejection issue
assert not any("newer" in i for i in result.issues)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Strengthen the pre-schema compatibility test assertion.

This test currently passes as long as no issue contains "newer", even if missing schema_version is effectively rejected for another reason. Please assert the acceptance path directly (e.g., result.ok) using a structurally valid manifest payload.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_bundle.py` around lines 357 - 383, The test should assert the
explicit acceptance path instead of only checking for absence of the substring
"newer": replace the final assertion with a direct acceptance check (e.g.,
assert result.ok) and optionally assert no issues (e.g., assert not
result.issues) so the test validates bundle.import_check returns an accepted
result for the pre-schema manifest; update the assertion in
test_import_check_accepts_bundle_without_schema_version to check result.ok
(and/or empty result.issues) against the manifest built for the test.

Comment thread tests/test_migration.py
@greatjourney589

Copy link
Copy Markdown
Author

Hi, @plind-junior
Could you please review this PR?

@plind-junior

Copy link
Copy Markdown
Collaborator

The atomic-swap pattern for migrate_kb is exactly right — writing everything to .vouch-migrate-tmp/, validating under the target Pydantic models, then doing the rename means a bad transform leaves the live KB completely untouched. That's the hard guarantee this codebase needed before the format could be called stable.

One thing to double-check in _copy_tree_for_migration: the change-detection comparison on line 686 is new_text != _yaml_dump(_yaml_load(original_text) or {}) — it's comparing the transformed output against a re-serialised version of the original, not the raw original text. That means a file whose YAML round-trips with different key ordering or whitespace (e.g., a multi-line string or a mapping that yaml.dump reorders) will show up as "changed" even when the transform was a no-op. Could you use the pre-transform raw dict for that baseline instead — something like new_text != _yaml_dump(raw_before_transforms) — so the changed list only flags files the transforms actually touched?

Non-blocking: MIGRATIONS is currently empty (the 0.1 baseline entry is just a comment explaining the intent). That's fine for now, but _chain("0.1", "99.0") raises a ValueError with a message that lists Available steps: [], which could confuse someone who hits it on a real upgrade. A tiny "no migration path exists between these versions; check that your vouch installation is up to date" would be friendlier.

Small process note: the PR body says Closes #162, but #162 is the review-gate policy engine feature. This PR implements the migration story described in #107 — worth fixing the reference so the issue closes correctly on merge. Also, CHANGELOG.md has a checkbox in the PR template that's unchecked — happy to let it slide if you'll add the entry before merge.

Thanks for taking this on — vouch migrate is the last piece the format-stability story needed. 🙏

@plind-junior

Copy link
Copy Markdown
Collaborator

PR didn't target test branch will be auto-closed by an agent

@plind-junior plind-junior changed the base branch from main to test June 9, 2026 06:51
@plind-junior

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3b9dc42843

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/vouch/health.py


def status(store: KBStore) -> dict:
def status(store: KBStore) -> dict[str, Any]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Badge Remove the duplicate status signature

Adding this second def status immediately after the existing one leaves the first function definition without an indented body, so src/vouch/health.py raises an IndentationError on import. Because cli.py, the JSONL server, and the MCP server all import health, this prevents the main entry points from starting at all.

Useful? React with 👍 / 👎.

Comment thread src/vouch/migration.py
Comment on lines +172 to +176
def _copy_tree_for_migration(kb_dir: Path, tmp_dir: Path, steps: list[Migration]) -> list[str]:
"""Copy and transform every migratable file from kb_dir into tmp_dir."""
changed: list[str] = []

for sub in MIGRATABLE_YAML_SUBDIRS:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve non-migrated KB files during the swap

When a real migration has at least one step, this copy routine seeds the replacement directory only from the whitelisted artifact subdirectories plus pages/config, and _atomic_swap then replaces the entire live .vouch/ directory and deletes the original. That drops existing files outside this whitelist such as .gitignore, audit.log.jsonl, state.db, and pending proposed/ files; a user running vouch migrate with pending proposals or custom ignores loses data. Seed the temp directory from the full KB tree before transforming artifacts, or explicitly carry over the non-migrated files.

Useful? React with 👍 / 👎.

Comment thread src/vouch/cli.py
Comment on lines +1133 to +1134
def migrate(dry_run: bool, backup: bool, from_version: str | None,
to_version: str | None, path: str) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the existing migrate command registered

This adds a second migrate callback decorated with @cli.command() using the same command name as the existing migrate command earlier in this file. Click stores commands by name, so the later registration replaces the command that supported --check, --to-version, --json, and the legacy KB-format migration path; for example, existing vouch migrate --check users now hit an unknown option instead of the dry-run check. Merge the new schema-version behavior into the existing command or register it under a distinct name.

Useful? React with 👍 / 👎.

Comment thread src/vouch/migration.py
Comment on lines +267 to +270
_audit.log_event(
kb_dir, event="migration.rollback", actor="vouch-migrate",
reversible=False, dry_run=dry_run,
data={"from_version": from_v, "to_version": to_v, "errors": errors},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid writing audit entries during dry-run failures

If validation fails while the user is running vouch migrate --dry-run, this branch appends a migration.rollback event to the live KB before returning the error. That violates the dry-run contract described by migrate_kb() and the CLI help: a user checking an invalid migration gets their audit.log.jsonl mutated even though no migration was requested to run. Skip audit writes when dry_run is true, or record this only after an actual write attempt.

Useful? React with 👍 / 👎.

Comment thread src/vouch/health.py
))
else:
stored = _stored_schema_version(store)
if stored is not None and stored != VOUCH_SCHEMA_VERSION:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Normalize schema_version before reporting mismatch

When config.yaml contains an unquoted schema_version: 0.1, YAML loads it as a float; KBStore.assert_schema_ok() accepts that by comparing str(stored) to VOUCH_SCHEMA_VERSION, but doctor compares the raw value here and emits a schema_version_mismatch error for a KB the storage guard considers current. Cast stored to str before this comparison to keep doctor consistent with the loader.

Useful? React with 👍 / 👎.

Comment thread src/vouch/cli.py
Comment on lines +97 to +100
except SchemaMismatchError as e:
click.echo(f"error: {e}", err=True)
click.echo("hint: run `vouch migrate` to upgrade the on-disk layout.", err=True)
sys.exit(3)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Let health commands inspect schema mismatches

With this early exit in _load_store, vouch doctor --json and vouch status --json on a mismatched KB terminate before the health code can emit the new schema_version_mismatch finding or any machine-readable payload. The only command that should bypass normal store loading is migrate; health commands need a way to construct a store without enforcing the schema guard so users and automation can diagnose the mismatch.

Useful? React with 👍 / 👎.

Comment thread src/vouch/migration.py
Comment on lines +191 to +195
raw = _yaml_load(p.read_text()) or {}
baseline_text = _yaml_dump(raw)
transformed = _apply_transforms(dict(raw), sub, steps)
new_text = _yaml_dump(transformed)
dst.write_text(new_text)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve bytes for artifacts not changed by transforms

This writes _yaml_dump(raw) for every YAML artifact in every migration step, even when none of the transforms changed that artifact. In migrations where a step only bumps the schema or touches another subdirectory, a real run silently rewrites unrelated committed YAML files, dropping comments/formatting and creating noisy diffs that dry-run does not report because changed compares against the dumped baseline. Copy the original bytes unless the transformed data actually differs from the loaded input.

Useful? React with 👍 / 👎.

Comment thread src/vouch/migration.py
Comment on lines +159 to +161
pre = kb_dir.parent / ".vouch-pre-migrate"
if pre.exists():
shutil.rmtree(pre)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refuse to clobber an existing pre-migrate directory

Before every migration this deletes any existing sibling named .vouch-pre-migrate. If a previous failed run left the rollback directory behind, or a user created that path as a manual backup, invoking vouch migrate removes it before the new migration has succeeded. Use a unique temporary name or fail when the path already exists rather than recursively deleting possible recovery data.

Useful? React with 👍 / 👎.

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.

feat: review-gate policy engine — conditional auto-approve, block, and escalation rules [VEP]

2 participants