{"record":{"id":"328f308cf1f1a4ea","repo":"affaan-m/ECC","slug":"artifact-changed-or-binding-invalid-actual-path","errorCode":null,"errorMessage":"Artifact changed or binding invalid: {actual[\"path\"]}","messagePattern":"Artifact changed or binding invalid: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/assets.py","lineNumber":132,"sourceCode":"    provider = _text(source.get('provider'), 'provider')\n    identifiers = {key: _text(source[key], key) for key in ('request_id', 'workflow_id')\n                   if key in source}\n    if not identifiers:\n        raise ValueError('Provider provenance requires request_id or workflow_id')\n    if verify:\n        evidence = _verify_binding(source.get('evidence'), base)\n    else:\n        evidence = _fingerprint(_path(source.get('evidence_path'), base))\n    return dict(provider=provider, **identifiers, evidence=evidence,\n                verification='supplied_local_evidence_only')\n\n\ndef _verify_binding(value: Any, base: Path, modality: str | None = None) -> dict[str, Any]:\n    if not isinstance(value, dict):\n        raise ValueError('Missing artifact binding')\n    actual = _fingerprint(_path(value.get('path'), base), modality)\n    if type(value.get('bytes')) is not int or any(value.get(k) != v for k, v in actual.items()):\n        raise ValueError(f'Artifact changed or binding invalid: {actual[\"path\"]}')\n    return actual\n\n\n_TASTE_FIELDS = ('genre_number', 'genre_slug', 'style_fingerprint', 'reference_sha256')\n\n\ndef _bundle(value: Any, base: Path, verify: bool) -> tuple[dict[str, Any], dict[tuple[str, str], Any]]:\n    from .contract import validate_bundle\n\n    if verify:\n        binding = _verify_binding(value, base)\n        root = Path(binding['path']).parent\n    else:\n        root = _path(value, base)\n        binding = _fingerprint(root / 'receipt.json')\n    validate_bundle(root)\n    requests = {}\n    for modality in sorted(MODALITIES):","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/assets.py#L114-L150","documentation":"After fingerprinting the artifact at the binding's path, the library compares the actual fingerprint (bytes and hashes) against the recorded values. Any mismatch — or a non-int 'bytes' field — throws this error, meaning the artifact was modified, replaced, or the binding was recorded incorrectly.","triggerScenarios":"Editing or re-saving the artifact after the binding was recorded; recording bytes as a string instead of int; copying the manifest between machines where the artifact differs; partial/corrupted file transfer.","commonSituations":"Post-processing (crop/convert) an external result after recording provenance; git line-ending or encoding normalization altering files; regenerated artifacts overwriting the original without updating the manifest.","solutions":["Restore the original artifact file that matches the recorded binding (or re-record the binding against the current file with the fingerprint helper)","Ensure the 'bytes' field in the binding is an integer, not a string","Re-run the provider request to regenerate a pristine external result and rebind","Check for tools (editors, formatters, sync clients) that silently modify files and exclude artifacts from them"],"exampleFix":"// before\n\"evidence\": {\"path\": \"a1.png\", \"bytes\": \"1024\"}\n// after\n\"evidence\": {\"path\": \"a1.png\", \"bytes\": 1024, \"sha256\": \"<actual hash>\"}","handlingStrategy":"try-catch","validationCode":"import hashlib, os\nfor a in assets:\n    ev = ((a.get(\"provider_provenance\") or {}).get(\"evidence\") or {})\n    p = ev.get(\"path\")\n    if p and os.path.exists(p):\n        actual = hashlib.sha256(open(p, \"rb\").read()).hexdigest()\n        if actual != ev.get(\"sha256\"):\n            raise ValueError(f\"artifact drifted before ingestion: {p}\")","typeGuard":null,"tryCatchPattern":"try:\n    ingest_assets(assets)\nexcept ValueError as e:\n    if str(e).startswith(\"Artifact changed or binding invalid\"):\n        restore_original_artifact(path_from_message(e)); retry()\n    else:\n        raise","preventionTips":["Never edit artifacts after recording their binding — regenerate and rebind instead","Store bytes as an int in bindings","Exclude artifact directories from formatters, sync tools, and git autocrlf","Record bindings immediately after the provider returns, before any post-processing"],"tags":["integrity","fingerprint","tamper-check"],"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"}