{"record":{"id":"943aa0aeffd9a533","repo":"affaan-m/ECC","slug":"modality-manifest-crosses-the-dry-run-boundary","errorCode":null,"errorMessage":"{modality} manifest crosses the dry-run boundary","messagePattern":"(.+?) manifest crosses the dry-run boundary","errorType":"validation","errorClass":"ContractError","httpStatus":null,"severity":"critical","filePath":"skills/taste-application/scripts/tasteforge/contract.py","lineNumber":296,"sourceCode":"                )\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:\n        payload = json.loads((manifests_dir / f\"{modality}.json\").read_text(encoding=\"utf-8\"))\n        if payload.get(\"modality\") != modality or not payload.get(\"requests\"):\n            raise ContractError(f\"invalid or empty {modality} manifest\")\n        if (payload.get(\"dry_run\") is not True or payload.get(\"submit\") is not False\n                or type(payload.get(\"provider_calls\")) is not int\n                or payload.get(\"provider_calls\") != 0\n                or payload.get(\"provider_execution\") is not False):\n            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\")","sourceCodeStart":278,"sourceCodeEnd":314,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/contract.py#L278-L314","documentation":"Manifests must stay strictly inside the dry-run boundary: dry_run=true, submit=false, provider_calls exactly int 0, and provider_execution=false at the manifest level. Any violation raises this ContractError to prevent accidental paid provider execution.","triggerScenarios":"A manifest with dry_run=false, submit=true, provider_calls=3, provider_calls='0' (string instead of int), provider_calls=true (bool — type() check rejects bools), or provider_execution=true in validate_manifests.","commonSituations":"Flipping dry_run to false to 'just test real generation' before review; leaving submit=true from a previous real run; YAML/JSON round-trip converting 0 into a string or bool; a generator defaulting provider_execution to true; copying a live-run config into the manifest.","solutions":["Set dry_run=true, submit=false, provider_execution=false, and provider_calls=0 (integer) on the manifest.","Change provider_calls to an int, not a string or bool — the check uses type(...) is not int so bools and strings are rejected.","Review why the manifest crossed the boundary: if real provider execution is intended, it does not belong in a dry-run bundle and must go through a separate approval path.","Regenerate the manifest from the dry-run template to restore safe defaults."],"exampleFix":"// before\n{\"modality\": \"image\", \"dry_run\": false, \"submit\": true, \"provider_calls\": 2, \"provider_execution\": true, \"requests\": [...]}\n// after\n{\"modality\": \"image\", \"dry_run\": true, \"submit\": false, \"provider_calls\": 0, \"provider_execution\": false, \"requests\": [...]}","handlingStrategy":"validation","validationCode":"def within_dry_run_boundary(payload):\n    return (payload.get(\"dry_run\") is True\n            and payload.get(\"submit\") is False\n            and type(payload.get(\"provider_calls\")) is int\n            and payload.get(\"provider_calls\") == 0\n            and payload.get(\"provider_execution\") is False)","typeGuard":"def is_dry_run_manifest(payload: dict) -> bool:\n    return (isinstance(payload.get(\"provider_calls\"), int)\n            and not isinstance(payload.get(\"provider_calls\"), bool)\n            and payload.get(\"dry_run\") is True\n            and payload.get(\"submit\") is False\n            and payload.get(\"provider_calls\") == 0\n            and payload.get(\"provider_execution\") is False)","tryCatchPattern":"try:\n    validate_manifests(manifests_dir)\nexcept ContractError as e:\n    if \"dry-run boundary\" in str(e):\n        logger.critical(\"manifest requests real provider execution — refusing to validate; review before any live run\")\n        raise SystemExit(1)  # do not auto-fix safety boundary violations\n    raise","preventionTips":["Never flip dry_run/submit flags to test real generation — use a separate reviewed live-run pipeline.","Keep provider flags controlled by config with safe defaults, not hand-edited JSON.","Watch for serialization turning int 0 into '0' or bool — re-check types after round-trips.","Add a CI gate that fails any manifest with submit=true or provider_calls != 0."],"tags":["dry-run","safety","provider","cost"],"backgroundTag":"conflicting-config-options","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"}