{"record":{"id":"8dac734b1990322b","repo":"affaan-m/ECC","slug":"receipt-key-must-be-a-list","errorCode":null,"errorMessage":"receipt {key} must be a list","messagePattern":"receipt (.+?) must be a list","errorType":"validation","errorClass":"ContractError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/contract.py","lineNumber":320,"sourceCode":"                    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\")\n                if not _is_finite_real(source_duration):\n                    raise ContractError(\"receipt reference has an invalid finite source duration\")\n                source_duration = cast(float, source_duration)\n                if float(source_duration) <= 0:\n                    raise ContractError(\"receipt reference has an invalid finite source duration\")\n                source_durations[(source_path, expected_digest)] = float(source_duration)","sourceCodeStart":302,"sourceCodeEnd":338,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/contract.py#L302-L338","documentation":"tasteforge's receipt validator requires each provenance section of a receipt (\"references\" and \"evidence_files\") to be a JSON list of source descriptors. When a receipt declares one of these keys with a non-list value (e.g. a string, object, or null that is explicitly present), ContractError('receipt {key} must be a list') is raised. This protects bundle validation from malformed receipts that would otherwise be iterated unsafely.","triggerScenarios":"Calling validate_artifact_receipt / validate_bundle with a receipt dict where receipt[\"references\"] or receipt[\"evidence_files\"] is present but not a list — e.g. a dict keyed by filename, a single object instead of a one-element list, a comma-separated string, or null.","commonSituations":"Hand-written or hand-edited receipt JSON; a receipt generator upgraded from an older single-source format (one object) to the current list format; scripts building receipts programmatically that append objects to a dict instead of a list; YAML/JSON round-trips that converted a list to a mapping.","solutions":["Convert the receipt's \"references\" and \"evidence_files\" values to JSON arrays, each element being an object with \"path\" and \"sha256\" fields","Regenerate the receipt with the tasteforge tooling instead of editing it by hand","If a single source was recorded, wrap it in a list: [{\"path\": ..., \"sha256\": ...}]","Validate the receipt JSON against the expected shape before passing it to validate_bundle"],"exampleFix":"// before\n\"references\": {\"path\": \"src/data.csv\", \"sha256\": \"abc...\"}\n// after\n\"references\": [{\"path\": \"src/data.csv\", \"sha256\": \"abc...\"}]","handlingStrategy":"validation","validationCode":"def receipt_list_ok(receipt, key):\n    v = receipt.get(key, [])\n    return isinstance(v, list)\n# call before validate:\n# assert receipt_list_ok(receipt, 'references') and receipt_list_ok(receipt, 'evidence_files')","typeGuard":"def is_source_list(v) -> bool:\n    return isinstance(v, list) and all(isinstance(s, dict) for s in v)","tryCatchPattern":"try:\n    validate_artifact_receipt(receipt, out_dir)\nexcept ContractError as e:\n    if 'must be a list' in str(e):\n        key = str(e).split()[1]\n        receipt[key] = [receipt[key]] if isinstance(receipt[key], dict) else []\n    raise","preventionTips":["Always emit references/evidence_files as JSON arrays, even for a single source","Validate receipt JSON with a schema (jsonschema) before validation","Never hand-edit receipts; regenerate with tasteforge","Round-trip receipts through json.loads to catch type drift"],"tags":["validation","schema","receipt"],"backgroundTag":"schema-validation-failed","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"}