affaan-m/ECC · error · ValueError

bundle collections must be lists

Error message

bundle collections must be lists

What it means

After deep-copying the config, build_application_bundle normalizes the collections audio, candidates, inserts, and historical_receipts and requires each to be a JSON list. Non-list values (dict, string, null supplied explicitly) break the deterministic per-item processing loops, so the error is raised before any bundle work begins.

Solutions

  1. Wrap single items in a list: candidates: [item] instead of candidates: item
  2. Convert keyed maps to lists of objects with the key moved into an 'id' field
  3. Replace explicit null with a missing key (setdefault will supply []) or an empty list []
  4. Validate types before calling: all(isinstance(cfg[k], list) for k in ('audio','candidates','inserts','historical_receipts'))

Example fix

// before
"candidates": {"c1": {...}}
// after
"candidates": [{"id": "c1", ...}]
Defensive patterns

Strategy: type-guard

Validate before calling

COLLECTIONS = ("audio", "candidates", "inserts", "historical_receipts")
assert all(isinstance(cfg.get(k, []), list) for k in COLLECTIONS)

Type guard

def collections_are_lists(cfg):
    return all(isinstance(cfg.get(k, []), list) for k in ("audio","candidates","inserts","historical_receipts"))

Try / catch

try:
    bundle = build_application_bundle(cfg, ci)
except ValueError as e:
    if "collections must be lists" in str(e):
        for k in ("audio","candidates","inserts","historical_receipts"):
            if not isinstance(cfg.get(k, []), list):
                cfg[k] = list(cfg[k].values()) if isinstance(cfg[k], dict) else []
        bundle = build_application_bundle(cfg, ci)
    else:
        raise

Prevention

When it happens

Trigger: Passing config with any of cfg['audio'], cfg['candidates'], cfg['inserts'], or cfg['historical_receipts'] set to a non-list (e.g. a dict keyed by id, a single object instead of a one-element list, or null explicitly present since setdefault only fills missing keys).

Common situations: Configs migrated from a schema that used a map/dict for candidates; YAML/JSON hand-edits collapsing a list to a single object; a serialization layer turning empty lists into null.

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/7977d9d648374a76. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-application/scripts/tasteforge/integration.py:358

    if type(local_only) is not bool:
        raise ValueError("local_only must be an exact boolean")
    _object(config, _REQUIRED, _OPTIONAL)
    if len(_canonical(config)) > _MAX_JSON:
        raise ValueError("application config exceeds local size limit")
    if local_only:
        if compiled_input is not None:
            raise ValueError("local-only preservation cannot accept provider input")
    else:
        _object(compiled_input, {"source_video", "compiled_prompt"})
        url = urlsplit(_text(compiled_input["source_video"]))
        if url.scheme != "https" or not url.hostname or url.username or url.password:
            raise ValueError("source reference must be HTTPS without embedded credentials")
        _text(compiled_input["compiled_prompt"])
    cfg = copy.deepcopy(config)
    for key in ("audio", "candidates", "inserts", "historical_receipts"):
        cfg.setdefault(key, [])
        if not isinstance(cfg[key], list):
            raise ValueError("bundle collections must be lists")
    if local_only and (cfg["candidates"] or cfg["inserts"]):
        raise ValueError("local-only preservation cannot contain candidates or inserts")
    tracks = _snapshot(cfg["baseline"])
    _binding(cfg["source"], tracks, cfg["baseline"])
    audio_keys = [_binding(item, tracks, cfg["baseline"], audio=True) for item in cfg["audio"]]
    expected_audio = {(t, i) for t, clips in tracks.items() if t.startswith("audio")
                      for i in range(len(clips))}
    if len(set(audio_keys)) != len(audio_keys) or set(audio_keys) != expected_audio:
        raise ValueError("every original audio clip must be preserved exactly once")
    stack = _preserved_stack(cfg["protected_intervals"], tracks, cfg["baseline"]["timeline_range"])
    input_hash = None if local_only else _digest(compiled_input)
    edit_hash = _digest({key: cfg[key] for key in _REQUIRED})
    if not local_only:
        candidates = _candidates(cfg["candidates"], cfg["source"]["media"]["sha256"],
                                 input_hash, compiled_input["source_video"])
        _inserts(cfg["inserts"], candidates, cfg, input_hash, edit_hash)
    for receipt in cfg["historical_receipts"]:
        _artifact(receipt)

View on GitHub (pinned to 8321021c54)