{"record":{"id":"68a876864545b273","repo":"affaan-m/ECC","slug":"at-least-one-placement-is-required","errorCode":null,"errorMessage":"at least one placement is required","messagePattern":"at least one placement is required","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/resolve.py","lineNumber":143,"sourceCode":"        info = metadata[key]\n        if _fps(info.get(\"fps\")) != rate:\n            raise ValueError(f\"asset fps differs from timeline: {asset}\")\n        if _integer(info.get(\"frames\"), \"asset frames\", 1) < frames:\n            raise ValueError(f\"asset contains too few frames: {asset}\")\n        if alpha and info.get(\"has_alpha\") is not True:\n            raise ValueError(f\"asset requires verified alpha: {asset}\")\n        checked.append(\n            {\n                \"id\": identifier,\n                \"asset\": key,\n                \"record_frame\": start,\n                \"frames\": frames,\n                \"opacity\": opacity,\n                \"composite\": composite,\n            }\n        )\n    if not checked:\n        raise ValueError(\"at least one placement is required\")\n    ends, allocated = [], {}\n    for event in sorted(checked, key=lambda entry: entry[\"record_frame\"]):\n        index = next(\n            (i for i, end in enumerate(ends) if end <= event[\"record_frame\"]), len(ends)\n        )\n        if index == len(ends):\n            ends.append(0)\n        ends[index] = event[\"record_frame\"] + event[\"frames\"]\n        allocated[event[\"id\"]] = {**event, \"track\": base_track_count + index + 1}\n    return [allocated[event[\"id\"]] for event in checked]\n\n\ndef _path(item):\n    media = item.GetMediaPoolItem()\n    raw = media.GetClipProperty(\"File Path\") if media else None\n    return str(Path(raw).expanduser().resolve()) if raw else None\n\n","sourceCodeStart":125,"sourceCodeEnd":161,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/resolve.py#L125-L161","documentation":"allocate_placements refuses to produce a plan with zero placements. After validating each entry, if none survived (or the input list was empty), it throws because downstream allocation, track assignment, and readback all assume at least one event.","triggerScenarios":"Calling allocate_placements/apply_placements/plan with an empty placements list, or with a list where all entries were filtered out before this check.","commonSituations":"Programmatically built placement lists that ended up empty due to an upstream filter; config file with a placeholder empty array; a caller passing None coerced to [].","solutions":["Pass at least one placement dict with asset, start, end","Guard the call site: skip apply entirely when the computed list is empty","Fix the upstream filtering logic that removed all placements","Validate placements input before calling"],"exampleFix":"// before\napply_placements(timeline, pool, placements=filtered)  # filtered == []\n// after\nif not filtered:\n    return  # nothing to apply\napply_placements(timeline, pool, placements=filtered)","handlingStrategy":"validation","validationCode":"if not placements:\n    raise ValueError(\"no placements to apply; check upstream filtering\")","typeGuard":"def is_nonempty_list(v) -> bool:\n    return isinstance(v, list) and len(v) > 0","tryCatchPattern":null,"preventionTips":["Skip apply_placements entirely when the computed list is empty","Log placement-list size at build time","Add an assertion after any filtering step that builds placements","Fail fast in config loading if the placements array is empty"],"tags":["validation","empty-input"],"backgroundTag":"empty-required-field","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"}