{"record":{"id":"34443e1f1692e2d0","repo":"affaan-m/ECC","slug":"provenance-must-contain-derived-rules","errorCode":null,"errorMessage":"provenance must contain derived rules","messagePattern":"provenance must contain derived rules","errorType":"validation","errorClass":"ContractError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/contract.py","lineNumber":262,"sourceCode":"                anchor.get(\"evidence_time\"), anchor.get(\"source_duration\"),\n                label=\"anchor evidence time\",\n            )\n            if reference_durations is not None:\n                digest = anchor.get(\"source_ref_sha256\")\n                expected_duration = reference_durations.get(digest) if isinstance(digest, str) else None\n                if expected_duration is None or anchor.get(\"source_duration\") != expected_duration:\n                    raise ContractError(\"anchor evidence source duration is not bound to its receipt reference\")\n    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","sourceCodeStart":244,"sourceCodeEnd":280,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/contract.py#L244-L280","documentation":"validate_provenance enforces that a Tasteforge provenance payload declares at least one derived rule. If the 'rules' key is absent, not a list, or an empty list, this ContractError is raised to guarantee every generated artifact carries non-empty provenance rules.","triggerScenarios":"Calling validate_provenance(payload) or validate_bundle() with payload = {} , payload = {'rules': None}, {'rules': 'x'} (non-list), or {'rules': []} (empty list).","commonSituations":"Hand-authoring a provenance.json by hand and forgetting the rules array; a pipeline stage that failed to derive any rules still emitting a provenance file; serializing an empty rules list after a filter step removed all rules; renaming the 'rules' key in a producer without updating the consumer.","solutions":["Add a non-empty 'rules' list to the provenance payload, each entry with 'rule_id' and non-empty 'evidence'.","Verify the producer that derives rules actually ran and wrote its output (check for upstream failures that yield zero rules).","Confirm the correct provenance file is being loaded (not a stub/placeholder file).","If zero rules is legitimately valid for your use case, bypass/extend the validator rather than passing an empty payload."],"exampleFix":"// before\npayload = {\"rules\": []}\n// after\npayload = {\"rules\": [{\"rule_id\": \"r1\", \"evidence\": [{\"reference_sha256\": \"<64-hex>\", \"times\": [0.0, 1.5], \"source_duration\": 10.0}]}]}","handlingStrategy":"validation","validationCode":"def has_rules(payload):\n    rules = payload.get(\"rules\")\n    return isinstance(rules, list) and len(rules) > 0\nif not has_rules(payload):\n    raise ValueError(\"provenance payload must declare a non-empty 'rules' list before validation\")","typeGuard":"def is_provenance_payload(p: object) -> bool:\n    return isinstance(p, dict) and isinstance(p.get(\"rules\"), list) and len(p[\"rules\"]) > 0","tryCatchPattern":"try:\n    validate_provenance(payload)\nexcept ContractError as e:\n    logger.error(\"provenance rejected: %s\", e)\n    payload = regenerate_provenance()  # re-derive rules from source\n    validate_provenance(payload)","preventionTips":["Always generate provenance via the official producer rather than hand-writing the JSON.","Assert a non-empty rules list in CI before running validate_bundle.","Never strip the 'rules' key in downstream transforms of the payload."],"tags":["provenance","validation","schema"],"backgroundTag":"empty-required-field","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"}