{"record":{"id":"ff6002e7f61d428c","repo":"affaan-m/ECC","slug":"rule-rule-get-rule-id-lacks-immutable-reference-evidence","errorCode":null,"errorMessage":"rule {rule.get('rule_id')} lacks immutable reference evidence","messagePattern":"rule (.+?) lacks immutable reference evidence","errorType":"validation","errorClass":"ContractError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/contract.py","lineNumber":270,"sourceCode":"    for event in events:\n        placement = event.get(\"placement\")\n        if not isinstance(placement, dict) or not {\"safe_area\", \"max_coverage\", \"occlusion_policy\"}.issubset(placement):\n            raise ContractError(f\"effect {event.get('effect')} lacks placement constraints\")\n\n\ndef validate_provenance(payload: dict[str, Any]) -> None:\n    \"\"\"Require every declared rule to cite immutable, timestamped evidence.\"\"\"\n    rules = payload.get(\"rules\")\n    if not isinstance(rules, list) or not rules:\n        raise ContractError(\"provenance must contain derived rules\")\n    for rule in rules:\n        evidence = rule.get(\"evidence\")\n        if not isinstance(evidence, list) or not evidence:\n            raise ContractError(f\"rule {rule.get('rule_id')} lacks reference evidence\")\n        for item in evidence:\n            digest = item.get(\"reference_sha256\")\n            if not isinstance(digest, str) or len(digest) != 64:\n                raise ContractError(f\"rule {rule.get('rule_id')} lacks immutable reference evidence\")\n            times = item.get(\"times\")\n            if not isinstance(times, list) or not times:\n                raise ContractError(f\"rule {rule.get('rule_id')} lacks time evidence\")\n            source_duration = item.get(\"source_duration\")\n            for time in times:\n                _validate_media_time(\n                    time, source_duration, label=f\"rule {rule.get('rule_id')} time evidence\"\n                )\n\n\ndef validate_manifests(manifests_dir: str | Path) -> None:\n    \"\"\"Require image, video, and 3D-asset dry-run request manifests.\"\"\"\n    manifests_dir = Path(manifests_dir)\n    found = {path.stem for path in manifests_dir.glob(\"*.json\")} if manifests_dir.is_dir() else set()\n    missing = _REQUIRED_MODALITIES - found\n    if missing:\n        raise ContractError(f\"missing modality manifests: {sorted(missing)}\")\n    for modality in _REQUIRED_MODALITIES:","sourceCodeStart":252,"sourceCodeEnd":288,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/contract.py#L252-L288","documentation":"Every evidence item must cite an immutable source via a 'reference_sha256' string of exactly 64 hex characters (a SHA-256 digest). Missing, non-string, or wrong-length digests raise this ContractError to ensure rules bind to verifiable content.","triggerScenarios":"An evidence item with no 'reference_sha256' key, digest = 123 (int), digest = 'abc123' (not 64 chars), or digest = 63/65-character string passed to validate_provenance.","commonSituations":"Computing the digest with a truncated/hex-vs-base64 mismatch; storing the hash as bytes instead of hex string; forgetting to compute the hash at all for a new evidence source; using an md5 (32-char) digest instead of SHA-256.","solutions":["Set 'reference_sha256' to the 64-character lowercase hex SHA-256 of the referenced file, e.g. hashlib.sha256(data).hexdigest().","Verify digest length with len(digest) == 64 before adding the evidence item.","Regenerate the digest after any change to the referenced source file; stale digests mean re-hashing the current bytes.","Ensure the digest is stored as a str, not bytes or int (decode bytes with .hex() or .decode())."],"exampleFix":"// before\nitem = {\"reference_sha256\": hashlib.md5(data).hexdigest()}  # 32 chars\n// after\nitem = {\"reference_sha256\": hashlib.sha256(data).hexdigest()}  # 64 chars","handlingStrategy":"validation","validationCode":"import hashlib, re\nSHA256_RE = re.compile(r\"^[0-9a-f]{64}$\")\ndef digest_ok(item):\n    d = item.get(\"reference_sha256\")\n    return isinstance(d, str) and SHA256_RE.match(d) is not None\nbad = [i for i in evidence_items if not digest_ok(i)]  # fix before validating","typeGuard":"def has_valid_sha256(item: dict) -> bool:\n    d = item.get(\"reference_sha256\")\n    return isinstance(d, str) and len(d) == 64 and all(c in \"0123456789abcdef\" for c in d)","tryCatchPattern":"try:\n    validate_provenance(payload)\nexcept ContractError as e:\n    if \"immutable reference evidence\" in str(e):\n        rehash_all_evidence(payload)  # recompute sha256 hex digests\n        validate_provenance(payload)\n    else:\n        raise","preventionTips":["Always compute digests with hashlib.sha256(data).hexdigest() — never md5 or base64.","Decode bytes digests with .hex() so the stored value is a str.","Re-hash evidence sources after any modification; keep hashing in one shared utility."],"tags":["provenance","sha256","checksum","validation"],"backgroundTag":"invalid-argument-format","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"}