{"record":{"id":"7977d9d648374a76","repo":"affaan-m/ECC","slug":"bundle-collections-must-be-lists","errorCode":null,"errorMessage":"bundle collections must be lists","messagePattern":"bundle collections must be lists","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/integration.py","lineNumber":358,"sourceCode":"    if type(local_only) is not bool:\n        raise ValueError(\"local_only must be an exact boolean\")\n    _object(config, _REQUIRED, _OPTIONAL)\n    if len(_canonical(config)) > _MAX_JSON:\n        raise ValueError(\"application config exceeds local size limit\")\n    if local_only:\n        if compiled_input is not None:\n            raise ValueError(\"local-only preservation cannot accept provider input\")\n    else:\n        _object(compiled_input, {\"source_video\", \"compiled_prompt\"})\n        url = urlsplit(_text(compiled_input[\"source_video\"]))\n        if url.scheme != \"https\" or not url.hostname or url.username or url.password:\n            raise ValueError(\"source reference must be HTTPS without embedded credentials\")\n        _text(compiled_input[\"compiled_prompt\"])\n    cfg = copy.deepcopy(config)\n    for key in (\"audio\", \"candidates\", \"inserts\", \"historical_receipts\"):\n        cfg.setdefault(key, [])\n        if not isinstance(cfg[key], list):\n            raise ValueError(\"bundle collections must be lists\")\n    if local_only and (cfg[\"candidates\"] or cfg[\"inserts\"]):\n        raise ValueError(\"local-only preservation cannot contain candidates or inserts\")\n    tracks = _snapshot(cfg[\"baseline\"])\n    _binding(cfg[\"source\"], tracks, cfg[\"baseline\"])\n    audio_keys = [_binding(item, tracks, cfg[\"baseline\"], audio=True) for item in cfg[\"audio\"]]\n    expected_audio = {(t, i) for t, clips in tracks.items() if t.startswith(\"audio\")\n                      for i in range(len(clips))}\n    if len(set(audio_keys)) != len(audio_keys) or set(audio_keys) != expected_audio:\n        raise ValueError(\"every original audio clip must be preserved exactly once\")\n    stack = _preserved_stack(cfg[\"protected_intervals\"], tracks, cfg[\"baseline\"][\"timeline_range\"])\n    input_hash = None if local_only else _digest(compiled_input)\n    edit_hash = _digest({key: cfg[key] for key in _REQUIRED})\n    if not local_only:\n        candidates = _candidates(cfg[\"candidates\"], cfg[\"source\"][\"media\"][\"sha256\"],\n                                 input_hash, compiled_input[\"source_video\"])\n        _inserts(cfg[\"inserts\"], candidates, cfg, input_hash, edit_hash)\n    for receipt in cfg[\"historical_receipts\"]:\n        _artifact(receipt)","sourceCodeStart":340,"sourceCodeEnd":376,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/integration.py#L340-L376","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Wrap single items in a list: candidates: [item] instead of candidates: item","Convert keyed maps to lists of objects with the key moved into an 'id' field","Replace explicit null with a missing key (setdefault will supply []) or an empty list []","Validate types before calling: all(isinstance(cfg[k], list) for k in ('audio','candidates','inserts','historical_receipts'))"],"exampleFix":"// before\n\"candidates\": {\"c1\": {...}}\n// after\n\"candidates\": [{\"id\": \"c1\", ...}]","handlingStrategy":"type-guard","validationCode":"COLLECTIONS = (\"audio\", \"candidates\", \"inserts\", \"historical_receipts\")\nassert all(isinstance(cfg.get(k, []), list) for k in COLLECTIONS)","typeGuard":"def collections_are_lists(cfg):\n    return all(isinstance(cfg.get(k, []), list) for k in (\"audio\",\"candidates\",\"inserts\",\"historical_receipts\"))","tryCatchPattern":"try:\n    bundle = build_application_bundle(cfg, ci)\nexcept ValueError as e:\n    if \"collections must be lists\" in str(e):\n        for k in (\"audio\",\"candidates\",\"inserts\",\"historical_receipts\"):\n            if not isinstance(cfg.get(k, []), list):\n                cfg[k] = list(cfg[k].values()) if isinstance(cfg[k], dict) else []\n        bundle = build_application_bundle(cfg, ci)\n    else:\n        raise","preventionTips":["Serialize collections as JSON arrays, never maps","Coerce null collections to [] in your config loader","Schema-validate configs (e.g. with a JSON Schema) before bundling","Wrap single items into one-element lists at authoring time"],"tags":["validation","type-check","config"],"backgroundTag":"type-mismatch","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"}