{"record":{"id":"d3ea56c1aeb5933b","repo":"affaan-m/ECC","slug":"receipt-source-sha-256-changed-after-generation-source-path","errorCode":null,"errorMessage":"receipt source SHA-256 changed after generation: {source_path}","messagePattern":"receipt source SHA-256 changed after generation: (.+?)","errorType":"validation","errorClass":"ContractError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/contract.py","lineNumber":364,"sourceCode":"        path = Path(source_path)\n        try:\n            metadata = path.lstat()\n        except FileNotFoundError:\n            if source_policy == \"require_available\":\n                raise ContractError(f\"receipt source is unavailable: {source_path}\") from None\n            continue\n        if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISREG(metadata.st_mode):\n            raise ContractError(f\"receipt source is not a safe regular file: {source_path}\")\n        try:\n            actual_digest = _sha256(path)\n        except FileNotFoundError:\n            if source_policy == \"require_available\":\n                raise ContractError(f\"receipt source is unavailable: {source_path}\") from None\n            continue\n        except OSError:\n            raise ContractError(f\"receipt source cannot be securely read: {source_path}\") from None\n        if actual_digest != expected_digest:\n            raise ContractError(f\"receipt source SHA-256 changed after generation: {source_path}\")\n\n    emitted = {\n        path.relative_to(out_dir).as_posix()\n        for path in out_dir.rglob(\"*\")\n        if path.is_file() and path.name != \"receipt.json\"\n    }\n    bound_paths: list[str] = []\n    for entry in entries:\n        relative = entry.get(\"path\")\n        if not isinstance(relative, str) or not relative:\n            raise ContractError(\"artifact path must be a non-empty relative path\")\n        bound_paths.append(relative)\n    if len(bound_paths) != len(set(bound_paths)):\n        raise ContractError(\"receipt contains duplicate artifact paths\")\n    missing = emitted - set(bound_paths)\n    extra = set(bound_paths) - emitted\n    if missing:\n        raise ContractError(f\"unbound emitted artifact: {sorted(missing)}\")","sourceCodeStart":346,"sourceCodeEnd":382,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/contract.py#L346-L382","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before (stale receipt)\npython contract.py validate ./bundle  # -> receipt source SHA-256 changed after generation: styles.md\n\n# after (regenerate receipt against current sources)\npython contract.py generate ./bundle && python contract.py validate ./bundle","handlingStrategy":"try-catch","validationCode":"import hashlib, json\nreceipt = json.load(open('bundle/receipt.json'))\nfor entry in receipt['artifacts']:\n    for src in entry['provenance']:\n        path = src['path']\n        if not os.path.exists(path):\n            raise SystemExit(f'source missing: {path}')\n        digest = hashlib.sha256(open(path, 'rb').read()).hexdigest()\n        if digest != src['sha256']:\n            raise SystemExit(f'source changed: {path} — regenerate receipt')","typeGuard":"def source_is_stable(src_entry: dict) -> bool:\n    path, expected = src_entry.get('path'), src_entry.get('sha256')\n    if not isinstance(path, str) or not isinstance(expected, str):\n        return False\n    try:\n        actual = hashlib.sha256(open(path, 'rb').read()).hexdigest()\n    except OSError:\n        return False\n    return actual == expected","tryCatchPattern":"from contract import ContractError\ntry:\n    validate_artifact_receipt(out_dir, entries)\nexcept ContractError as e:\n    if 'changed after generation' in str(e):\n        regenerate_bundle(out_dir)  # sources drifted; rebuild\n    else:\n        raise","preventionTips":["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."],"tags":["integrity","sha256","provenance","validation"],"backgroundTag":"checksum-mismatch","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}