affaan-m/ECC · error · ContractError

missing provenance

Error message

missing provenance

What it means

validate_bundle checks that every tasteforge output bundle contains a provenance.json file documenting how the bundle was produced. If the file is absent from the bundle's out_dir, the contract layer raises this ContractError. Provenance is a mandatory bundle artifact, so the validator fails fast rather than accepting an incomplete bundle.

Solutions

  1. Re-run the full tasteforge pipeline so the bundle is regenerated with provenance.json present
  2. Check the bundle path passed to validate_bundle — it must be the bundle root, not a subdirectory
  3. If copying/archiving bundles, copy all top-level JSON files including provenance.json
  4. If writing a custom generator, emit provenance.json into out_dir before validation

Example fix

# before
validate_bundle(out_dir / "genres")
# after
validate_bundle(out_dir)  # bundle root, contains provenance.json
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path
def ensure_bundle_complete(out_dir: Path) -> None:
    if not (out_dir / "provenance.json").is_file():
        raise FileNotFoundError(f"{out_dir} has no provenance.json; regenerate the bundle")

Type guard

def has_provenance(out_dir) -> bool:
    return (out_dir / "provenance.json").is_file()

Try / catch

from tasteforge.contract import ContractError
try:
    validate_bundle(out_dir)
except ContractError as e:
    if "missing provenance" in str(e):
        regenerate_bundle(out_dir)  # re-run pipeline to restore artifacts

Prevention

When it happens

Trigger: Calling validate_bundle (directly or via _bundle) on an output directory that lacks out_dir/provenance.json — e.g. a partially written run, a manually pruned directory, or a generator that skipped provenance emission.

Common situations: Running an older/partial pipeline that predates provenance emission, deleting provenance.json while copying bundles, or pointing validate_bundle at the wrong directory (e.g. the genres/ subdir instead of the bundle root).

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/47240e81e556b0c6. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-application/scripts/tasteforge/contract.py:455

    actual_digest = hashlib.sha256(
        json.dumps(digest_payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
    ).hexdigest()
    if claimed_digest != actual_digest:
        raise ContractError("receipt SHA-256 does not match its canonical content")


def validate_bundle(out_dir: str | Path) -> None:
    """Validate required multimodal files and cross-artifact invariants."""
    out_dir = Path(out_dir)
    _validate_output_tree(out_dir)
    specs = [json.loads(path.read_text(encoding="utf-8"))
             for path in sorted((out_dir / "genres").glob("*.json"))]
    validate_genre_specs(specs)

    validate_manifests(out_dir / "manifests")
    provenance_path = out_dir / "provenance.json"
    if not provenance_path.is_file():
        raise ContractError("missing provenance")
    validate_provenance(json.loads(provenance_path.read_text(encoding="utf-8")))

    receipt_path = out_dir / "receipt.json"
    if not receipt_path.is_file():
        raise ContractError("missing receipt")
    receipt = json.loads(receipt_path.read_text(encoding="utf-8"))
    if (receipt.get("dry_run") is not True
            or receipt.get("provider_execution") is not False
            or type(receipt.get("provider_calls")) is not int
            or receipt.get("provider_calls") != 0):
        raise ContractError("receipt crosses the dry-run boundary")
    validate_artifact_receipt(out_dir, receipt)

    reference_durations: dict[str, float] = {}
    for reference in receipt.get("references", []):
        digest = reference.get("sha256")
        duration = reference.get("source_duration")
        if not isinstance(digest, str) or not _is_finite_real(duration):

View on GitHub (pinned to 8321021c54)