Hmbown/CodeWhale · error · PersistenceBacklogError

receipt schema_version changed

Error message

receipt schema_version changed

What it means

Raised in validate_receipt (scripts/check-persistence-backlog-budget.py:149-150) when receipt["schema_version"] != 2 (SCHEMA_VERSION). The version gate ensures the receipt layout matches what this checker knows how to interpret before any field is trusted.

Source

Thrown at scripts/check-persistence-backlog-budget.py:150

        "sample_count": 1,
    }


def validate_receipt(
    receipt: dict[str, Any],
    *,
    expected_source: dict[str, Any] | None = None,
    require_clean_source: bool = False,
) -> None:
    missing = [field for field in REQUIRED_RECEIPT_FIELDS if field not in receipt]
    if missing:
        raise PersistenceBacklogError(
            "receipt missing required field(s): " + ", ".join(missing)
        )
    if receipt["document_kind"] != RECEIPT_KIND:
        raise PersistenceBacklogError(f"receipt document_kind must be {RECEIPT_KIND}")
    if receipt["schema_version"] != SCHEMA_VERSION:
        raise PersistenceBacklogError("receipt schema_version changed")
    for field, expected in FIXTURE.items():
        validate_frozen_field(field, receipt[field], expected)
    if not isinstance(receipt["source_sha"], str) or not SOURCE_SHA_PATTERN.fullmatch(
        receipt["source_sha"]
    ):
        raise PersistenceBacklogError("receipt source_sha must be an exact lowercase Git SHA")
    if type(receipt["source_dirty"]) is not bool:
        raise PersistenceBacklogError("receipt source_dirty must be boolean")
    for field, prefix in (("rustc_version", "rustc "), ("cargo_version", "cargo ")):
        if not isinstance(receipt[field], str) or not receipt[field].startswith(prefix):
            raise PersistenceBacklogError(f"receipt {field} must be a version string")
    validate_frozen_field("build_profile", receipt["build_profile"], "test")
    validate_frozen_field("sample_count", receipt["sample_count"], 1)
    if expected_source is not None:
        for field in (
            "source_sha",
            "source_dirty",
            "rustc_version",

View on GitHub (pinned to 8880682c63)

Solutions

  1. Regenerate the receipt (and baseline receipt) with the current measure script after any schema bump.
  2. When bumping SCHEMA_VERSION, update checker, Rust emitter, budget, and both receipts in one atomic commit.
  3. Discard receipts whose schema_version predates the current gate.

Example fix

// receipt (before)
"schema_version": 1
// receipt (after)
"schema_version": 2
Defensive patterns

Strategy: type-guard

Validate before calling

if receipt.get("schema_version") != 2:
    sys.exit("receipt schema is not v2; regenerate with the current measure script")

Type guard

def is_current_schema(doc: dict) -> bool:
    return doc.get("schema_version") == 2

Try / catch

try:
    validate_receipt(receipt)
except PersistenceBacklogError as e:
    if "schema_version changed" in str(e):
        raise RuntimeError("schema migration incomplete: regenerate receipt and baseline") from e
    raise

Prevention

When it happens

Trigger: Checking a v1 receipt with a v2 checker after bumping SCHEMA_VERSION; regenerating the budget but reusing an old receipt; a draft schema bump where only one side was updated.

Common situations: Schema migrations where receipts, baseline, budget, and checker must move in lockstep; long-lived receipts checked out alongside updated tooling.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/71f2cbcf96580d7b. Report an issue: GitHub.