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
- Regenerate the bundle and its receipt with tasteforge so the recorded source digests match the current files.
- Restore the source file to the exact content hash recorded in the receipt (e.g. via git checkout / git restore).
- 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
- Never edit sources after generating a bundle; regenerate immediately after any change.
- Generate and validate the receipt in the same CI step on the same checkout.
- Avoid running formatters/autofixers over provenance source directories post-generation.
- Commit bundles and sources together so digests stay in sync.
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
- artifact cites an unknown provenance source
- Invalid supplied provenance declaration
- rule lacks immutable reference evidence
- a generated candidate cannot claim original-source identity
- anchor evidence source duration is not bound to its receipt…
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)