affaan-m/ECC · error · ContractError

genre signature axes must be lists

Error message

genre {spec.get('number')} signature axes must be lists

What it means

Once the signature dict is present and complete, every axis value must be a list. validate_genre_specs raises this ContractError when any signature[axis] is a string, scalar, or other non-list type. Axes are treated as collections of descriptors (including the avoid axis), so single values must still be wrapped in a list.

Solutions

  1. Wrap each axis value in a list, e.g. palette: ["warm"] instead of palette: "warm".
  2. Check the signature-producing code for assignments that unwrap lists (e.g. axis = descriptors[0]).
  3. Normalize at load time: for each axis, value if isinstance(value, list) else [value] — but prefer fixing the source.
  4. Run a quick pre-check: all(isinstance(v, list) for v in signature.values()).

Example fix

// before
spec["signature"]["palette"] = "warm"
// after
spec["signature"]["palette"] = ["warm"]
Defensive patterns

Strategy: type-guard

Validate before calling

def axes_are_lists(spec, axes):
    sig = spec.get("signature") or {}
    return all(isinstance(sig.get(a), list) for a in axes)

Type guard

def is_axis_descriptor_list(value) -> bool:
    return isinstance(value, list) and all(isinstance(v, str) for v in value)

Try / catch

try:
    validate_genre_specs(specs)
except ContractError as e:
    if "must be lists" in str(e):
        sig = spec["signature"]
        for a, v in sig.items():
            if not isinstance(v, list):
                sig[a] = [v]
    raise

Prevention

When it happens

Trigger: Calling validate_genre_specs / validate_bundle with signature = {"palette": "warm", ...} (string instead of list) or any axis set to a number/dict/None.

Common situations: Hand-authored YAML where the axis was written as a bare string without [] brackets; a script that assigns the first descriptor directly instead of the full list; JSON round-trips that are fine but original authoring that skipped list syntax.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


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

Appendix: source

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

                        raise ContractError(f"genre {spec.get('number')} has invalid time evidence")
                    for time in group.get("times", []):
                        _validate_media_time(
                            time, group.get("source_duration"),
                            label=f"genre {spec.get('number')} time evidence",
                        )
            temporal = measured.get("temporal", {})
            if isinstance(temporal, dict):
                for group in temporal.get("scene_change_evidence", []):
                    for time in group.get("times", []):
                        _validate_media_time(
                            time, group.get("source_duration"),
                            label=f"genre {spec.get('number')} scene evidence",
                        )
        signature = spec.get("signature")
        if not isinstance(signature, dict) or not _SIGNATURE_AXES.issubset(signature):
            raise ContractError(f"genre {spec.get('number')} has an incomplete signature")
        if not all(isinstance(signature[axis], list) for axis in _SIGNATURE_AXES):
            raise ContractError(f"genre {spec.get('number')} signature axes must be lists")
        if not all(signature[axis] for axis in _SIGNATURE_AXES):
            raise ContractError(f"genre {spec.get('number')} has an empty signature axis, including avoid")


def validate_effect_recipe(
    recipe: dict[str, Any], *, reference_durations: dict[str, float] | None = None
) -> None:
    """Require a seeded aperiodic schedule and anchors on subject-aware effects."""
    if (recipe.get("dry_run") is not True
            or type(recipe.get("provider_calls")) is not int
            or recipe.get("provider_calls") != 0
            or recipe.get("provider_execution") is not False):
        raise ContractError("effect recipe crosses the dry-run provider boundary")
    if not isinstance(recipe.get("seed"), int) or isinstance(recipe.get("seed"), bool):
        raise ContractError("effect recipe must have an integer seed")
    if recipe.get("rng_algorithm") != "python.random.Random/v1":
        raise ContractError("effect recipe must declare its seeded RNG algorithm")
    events = recipe.get("events")

View on GitHub (pinned to 8321021c54)