{"record":{"id":"dc706bfd282b9dfd","repo":"affaan-m/ECC","slug":"receipt-evidence-artifacts-entries-must-be-objects","errorCode":null,"errorMessage":"receipt evidence_artifacts entries must be objects","messagePattern":"receipt evidence_artifacts entries must be objects","errorType":"validation","errorClass":"ContractError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/contract.py","lineNumber":314,"sourceCode":"            raise ContractError(f\"{modality} manifest crosses the dry-run boundary\")\n        for request in payload[\"requests\"]:\n            if (request.get(\"dry_run\") is not True\n                    or request.get(\"submit\") is not False\n                    or type(request.get(\"provider_calls\")) is not int\n                    or request.get(\"provider_calls\") != 0\n                    or request.get(\"provider_execution\") is not False\n                    or request.get(\"provider_call_mode\") != \"disabled\"):\n                raise ContractError(f\"{modality} request crosses the dry-run boundary\")\n\n\ndef validate_artifact_receipt(out_dir: str | Path, receipt: dict[str, Any]) -> None:\n    \"\"\"Verify that the receipt binds every emitted artifact and its provenance.\"\"\"\n    out_dir = Path(out_dir).resolve()\n    entries = receipt.get(\"evidence_artifacts\")\n    if not isinstance(entries, list):\n        raise ContractError(\"receipt evidence_artifacts must be a list\")\n    if not all(isinstance(entry, dict) for entry in entries):\n        raise ContractError(\"receipt evidence_artifacts entries must be objects\")\n    known_sources: set[tuple[str, str]] = set()\n    source_durations: dict[tuple[str, str], float] = {}\n    for key in (\"references\", \"evidence_files\"):\n        sources = receipt.get(key, [])\n        if not isinstance(sources, list):\n            raise ContractError(f\"receipt {key} must be a list\")\n        for source in sources:\n            if not isinstance(source, dict):\n                raise ContractError(f\"receipt {key} contains an invalid source\")\n            source_path = source.get(\"path\")\n            expected_digest = source.get(\"sha256\")\n            if (not isinstance(source_path, str) or not source_path\n                    or not isinstance(expected_digest, str)\n                    or not re.fullmatch(r\"[0-9a-f]{64}\", expected_digest)):\n                raise ContractError(\"receipt has an invalid source identity\")\n            known_sources.add((source_path, expected_digest))\n            if key == \"references\":\n                source_duration = source.get(\"source_duration\")","sourceCodeStart":296,"sourceCodeEnd":332,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/contract.py#L296-L332","documentation":"After confirming evidence_artifacts is a list, validate_artifact_receipt requires every entry to be an object (dict). Any list containing a non-dict element (string, number, None, nested list) raises this ContractError so per-artifact digest and provenance binding can proceed.","triggerScenarios":"receipt = {'evidence_artifacts': ['out.png', 'out.wav']} (strings instead of objects); entries containing None; or a mixed list where one entry is a dict and another is a str, caught by the all(isinstance(entry, dict)) check.","commonSituations":"Serializing just artifact filenames instead of artifact records; a producer appending paths and objects inconsistently; JSON schema changes between receipt versions; manually appending a quick entry to a receipt for debugging and forgetting to promote it to an object.","solutions":["Convert each entry to an object with the required fields (path and digest/provenance per the receipt schema).","Fix the receipt writer to serialize full artifact records, not bare path strings.","Validate receipt entries with a schema/type check before writing the receipt file.","Regenerate the receipt with the current Tasteforge version so all entries conform."],"exampleFix":"// before\n\"evidence_artifacts\": [\"artifact.png\", \"artifact.wav\"]\n// after\n\"evidence_artifacts\": [{\"path\": \"artifact.png\", \"sha256\": \"<64-hex>\"}, {\"path\": \"artifact.wav\", \"sha256\": \"<64-hex>\"}]","handlingStrategy":"type-guard","validationCode":"bad = [e for e in receipt.get(\"evidence_artifacts\", []) if not isinstance(e, dict)]\nif bad:\n    raise TypeError(f\"non-object artifact entries: {bad!r}\")","typeGuard":"def entries_are_objects(receipt: dict) -> bool:\n    entries = receipt.get(\"evidence_artifacts\")\n    return isinstance(entries, list) and all(isinstance(e, dict) for e in entries)","tryCatchPattern":"try:\n    validate_artifact_receipt(out_dir, receipt)\nexcept ContractError as e:\n    if \"entries must be objects\" in str(e):\n        receipt = promote_paths_to_entries(receipt, out_dir)  # wrap bare paths in objects with digests\n        validate_artifact_receipt(out_dir, receipt)\n    else:\n        raise","preventionTips":["Serialize artifact records (path + sha256), never bare filename strings.","Use a single receipt-builder helper so entry shapes stay consistent.","Type-check receipt entries before writing; reject str/None entries at production time."],"tags":["receipt","artifacts","type-error"],"backgroundTag":"type-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"}