{"record":{"id":"f43c41ec6474e162","repo":"affaan-m/ECC","slug":"clip-i-has-no-path","errorCode":null,"errorMessage":"clip {i} has no 'path'","messagePattern":"clip (.+?) has no 'path'","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/taste/timeline.py","lineNumber":218,"sourceCode":"\n# ---------------------------------------------------------------------------\n# clip normalisation\n# ---------------------------------------------------------------------------\n\n\ndef _normalise(clips: Iterable[dict], fps: float | Fraction) -> list[dict]:\n    \"\"\"Validate clips and pre-compute integer frame counts and offsets.\n\n    Returns dicts with ``path``, ``name``, ``frames`` (int, >= 1) and\n    ``offset_frames`` (int). Working in frames from here down is what makes the\n    sequence duration exactly the sum of the clip durations.\n    \"\"\"\n    out: list[dict] = []\n    offset = 0\n    for i, c in enumerate(clips):\n        path = str(c.get(\"path\") or \"\")\n        if not path:\n            raise ValueError(f\"clip {i} has no 'path'\")\n        dur = float(c.get(\"duration\") or 0.0)\n        if dur <= 0:\n            raise ValueError(f\"clip {i} ({path}) has non-positive duration {dur!r}\")\n        frames = max(1, seconds_to_frames(dur, fps))  # never emit a zero-length event\n        name = str(c.get(\"name\") or Path(path).stem)\n        out.append(\n            {\n                \"path\": path,\n                \"name\": name,\n                \"frames\": frames,\n                \"offset_frames\": offset,\n                \"seconds\": dur,\n            }\n        )\n        offset += frames\n    if not out:\n        raise ValueError(\"no clips to write - a timeline needs at least one event\")\n    return out","sourceCodeStart":200,"sourceCodeEnd":236,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/taste/timeline.py#L200-L236","documentation":"Structural guard in _normalise: a clip in the input list lacks the required 'path' key, so the timeline builder cannot locate the media file. The 0-based clip index at fault is included in the message.","triggerScenarios":"Calling build_fcpxml or build_edl with a clips list where an entry is missing the 'path' key, has path=None, or path='' (empty string coerces to falsy).","commonSituations":"Building clips programmatically and forgetting the key; JSON clip manifests with null paths for placeholder clips; a key-name mismatch such as 'file' or 'src' instead of 'path'.","solutions":["Add the 'path' key to each clip dict with a non-empty string.","Rename your key (e.g. 'src' -> 'path') or map it before calling: `{'path': c['src'], 'duration': c['dur']}`.","Validate the clips list before building: check every clip has a truthy 'path'.","Fix upstream data extraction that emitted null paths."],"exampleFix":"// before\nclips = [{\"name\": \"intro\", \"duration\": 3.0}]\n// after\nclips = [{\"name\": \"intro\", \"path\": \"media/intro.mp4\", \"duration\": 3.0}]","handlingStrategy":"validation","validationCode":"def validate_clips_have_paths(clips):\n    for i, c in enumerate(clips):\n        if not str(c.get('path') or '').strip():\n            raise ValueError(f\"clip {i} has no 'path'\")\n    return True","typeGuard":"def is_valid_clip(c) -> bool:\n    return isinstance(c, dict) and bool(str(c.get('path') or '').strip())","tryCatchPattern":"try:\n    xml = build_fcpxml(clips, fps=30)\nexcept ValueError as e:\n    if \"has no 'path'\" in str(e):\n        logger.error('invalid clip manifest: %s', e)\n        raise","preventionTips":["Standardize clip dicts with a dataclass/TypedDict requiring 'path'","Validate clip manifests (JSON schema) before building timelines","Rename alternate keys ('src', 'file') to 'path' at ingestion","Reject null paths at the data-collection step, not the build step"],"tags":["validation","timeline","schema"],"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"}