affaan-m/ECC · error · ContractError

at least one genre spec is required

Error message

at least one genre spec is required

What it means

validate_genre_specs requires a non-empty list of genre spec dictionaries; each spec defines a numbered style target. An empty (or missing) list cannot satisfy the 'at least one distinct genre' contract, so validation fails early with this error.

Solutions

  1. Provide at least one complete genre spec with number, style_fingerprint, and references
  2. Fix the config file so the genre list is populated
  3. If genres come from CLI flags/args, pass the required genre arguments explicitly
  4. Check that the spec-loading code isn't filtering out all entries before validation

Example fix

# before
specs = []
validate_genre_specs(specs)
# after
specs = [{"number": 1, "style_fingerprint": "noir-neon", "dry_run": True, "avoid": ["generic stock look"]}]
validate_genre_specs(specs)
Defensive patterns

Strategy: validation

Validate before calling

specs = config.get("genres", [])
if not specs:
    raise SystemExit('config must define at least one genre spec')

Type guard

def has_genre_specs(v) -> bool:
    return isinstance(v, list) and len(v) > 0 and all(isinstance(s, dict) for s in v)

Try / catch

try:
    validate_bundle(root)
except ContractError as e:
    if 'at least one genre spec' in str(e):
        print('Populate the genre list in your config before validating')
    else:
        raise

Prevention

When it happens

Trigger: Calling validate_bundle/validate_genre_specs with specs=[] or None, or a config file whose genre list was never populated.

Common situations: Template config with an empty genres key, YAML/JSON list accidentally emptied during editing, a script path where specs are read from a file that exists but has no entries, or the CLI flag for genres omitted so an empty default is used.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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

Appendix: source

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

    if not stat.S_ISDIR(metadata.st_mode):
        raise ContractError("output bundle root must be a directory")
    pending = [root]
    while pending:
        directory = pending.pop()
        with os.scandir(directory) as entries:
            for entry in entries:
                if entry.is_symlink():
                    raise ContractError(f"output bundle contains a symlink: {entry.path}")
                if entry.is_dir(follow_symlinks=False):
                    pending.append(Path(entry.path))
                elif not entry.is_file(follow_symlinks=False):
                    raise ContractError(f"output bundle contains a special file: {entry.path}")


def validate_genre_specs(specs: list[dict[str, Any]]) -> None:
    """Require complete, semantically distinct numbered genre specs."""
    if not specs:
        raise ContractError("at least one genre spec is required")
    numbers = [spec.get("number") for spec in specs]
    if len(numbers) != len(set(numbers)):
        raise ContractError("genre numbers must be distinct")

    fingerprints = [spec.get("style_fingerprint") for spec in specs]
    signatures = [_semantic_signature(spec) for spec in specs]
    if len(fingerprints) != len(set(fingerprints)) or len(signatures) != len(set(signatures)):
        raise ContractError("genre references collapsed into a generic style; distinct specs required")

    for spec in specs:
        if spec.get("dry_run") is not True:
            raise ContractError(f"genre {spec.get('number')} crosses the dry-run boundary")
        measured = spec.get("measured_features")
        if measured is not None:
            if not isinstance(measured, dict):
                raise ContractError(f"genre {spec.get('number')} has invalid measured evidence")
            _validate_numeric_evidence(measured, label=f"genre {spec.get('number')} evidence")
            total_duration = measured.get("total_duration")

View on GitHub (pinned to 8321021c54)