affaan-m/ECC · error · ContractError

receipt source SHA-256 changed after generation

Error message

receipt source SHA-256 changed after generation: {source_path}

What it means

validate_artifact_receipt re-verifies the SHA-256 of each provenance source file recorded in receipt.json against the digest captured at generation time. This ContractError is raised when a source file's current digest no longer matches the digest bound into the receipt, meaning the input changed (or was tampered with) after the bundle was generated. The library fails closed: a receipt whose provenance is stale cannot be trusted to describe the bundle.

Solutions

  1. Regenerate the bundle and its receipt with tasteforge so the recorded source digests match the current files.
  2. Restore the source file to the exact content hash recorded in the receipt (e.g. via git checkout / git restore).
  3. Identify which source changed by recomputing SHA-256 of each provenance source and diffing against receipt entries, then decide whether to regenerate or revert.

Example fix

# before (stale receipt)
python contract.py validate ./bundle  # -> receipt source SHA-256 changed after generation: styles.md

# after (regenerate receipt against current sources)
python contract.py generate ./bundle && python contract.py validate ./bundle
Defensive patterns

Strategy: try-catch

Validate before calling

import hashlib, json
receipt = json.load(open('bundle/receipt.json'))
for entry in receipt['artifacts']:
    for src in entry['provenance']:
        path = src['path']
        if not os.path.exists(path):
            raise SystemExit(f'source missing: {path}')
        digest = hashlib.sha256(open(path, 'rb').read()).hexdigest()
        if digest != src['sha256']:
            raise SystemExit(f'source changed: {path} — regenerate receipt')

Type guard

def source_is_stable(src_entry: dict) -> bool:
    path, expected = src_entry.get('path'), src_entry.get('sha256')
    if not isinstance(path, str) or not isinstance(expected, str):
        return False
    try:
        actual = hashlib.sha256(open(path, 'rb').read()).hexdigest()
    except OSError:
        return False
    return actual == expected

Try / catch

from contract import ContractError
try:
    validate_artifact_receipt(out_dir, entries)
except ContractError as e:
    if 'changed after generation' in str(e):
        regenerate_bundle(out_dir)  # sources drifted; rebuild
    else:
        raise

Prevention

When it happens

Trigger: Calling validate_artifact_receipt() (directly or via validate_bundle()) when a file listed in the receipt's provenance array was edited, regenerated, reformatted (e.g. line-ending or permission changes flowing through the digest path), or replaced between receipt generation and validation.

Common situations: A developer edits a source asset to fix a bug, then re-runs the validator on the old bundle without regenerating the receipt; a git checkout/branch switch swaps the source file to a different version; an external tool (formatter, linter autofix) rewrites the file in place; the bundle was built from an older commit of the sources.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/d3ea56c1aeb5933b. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-application/scripts/tasteforge/contract.py:364

        path = Path(source_path)
        try:
            metadata = path.lstat()
        except FileNotFoundError:
            if source_policy == "require_available":
                raise ContractError(f"receipt source is unavailable: {source_path}") from None
            continue
        if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISREG(metadata.st_mode):
            raise ContractError(f"receipt source is not a safe regular file: {source_path}")
        try:
            actual_digest = _sha256(path)
        except FileNotFoundError:
            if source_policy == "require_available":
                raise ContractError(f"receipt source is unavailable: {source_path}") from None
            continue
        except OSError:
            raise ContractError(f"receipt source cannot be securely read: {source_path}") from None
        if actual_digest != expected_digest:
            raise ContractError(f"receipt source SHA-256 changed after generation: {source_path}")

    emitted = {
        path.relative_to(out_dir).as_posix()
        for path in out_dir.rglob("*")
        if path.is_file() and path.name != "receipt.json"
    }
    bound_paths: list[str] = []
    for entry in entries:
        relative = entry.get("path")
        if not isinstance(relative, str) or not relative:
            raise ContractError("artifact path must be a non-empty relative path")
        bound_paths.append(relative)
    if len(bound_paths) != len(set(bound_paths)):
        raise ContractError("receipt contains duplicate artifact paths")
    missing = emitted - set(bound_paths)
    extra = set(bound_paths) - emitted
    if missing:
        raise ContractError(f"unbound emitted artifact: {sorted(missing)}")

View on GitHub (pinned to 8321021c54)