{"record":{"id":"9828890313320d2d","repo":"affaan-m/ECC","slug":"native-fps-must-use-a-bounded-decimal-or-rational-label","errorCode":null,"errorMessage":"native fps must use a bounded decimal or rational label","messagePattern":"native fps must use a bounded decimal or rational label","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/integration.py","lineNumber":205,"sourceCode":"def _snapshot(baseline: dict) -> dict:\n    _object(baseline, {\"project_file\", \"snapshot_file\", \"project_name\", \"timeline_name\",\n                       \"fps\", \"timeline_range\"})\n    _rate(baseline[\"fps\"])\n    bounds = _range(baseline[\"timeline_range\"])\n    _artifact(baseline[\"project_file\"])\n    snapshot = _artifact(baseline[\"snapshot_file\"], parse_json=True)\n    if not isinstance(snapshot, dict):\n        raise ValueError(\"native snapshot must be an object\")\n    settings = snapshot.get(\"settings\")\n    native_fps = settings.get(\"timelineFrameRate\") if isinstance(settings, dict) else None\n    # Resolve's conventional decimal NTSC labels represent these exact rates.\n    ntsc = {\"23.976\": \"24000/1001\", \"29.97\": \"30000/1001\", \"59.94\": \"60000/1001\"}\n    try:\n        if type(native_fps) not in (str, int, float):\n            raise ValueError(\"native fps missing\")\n        label = str(native_fps)\n        if not re.fullmatch(r\"[0-9]{1,9}(?:\\.[0-9]{1,12}|/[1-9][0-9]{0,8})?\", label):\n            raise ValueError(\"native fps must use a bounded decimal or rational label\")\n        native_rate = Fraction(ntsc.get(label, label))\n        if native_rate != Fraction(*_rate(baseline[\"fps\"])):\n            raise ValueError(\"native fps differs\")\n    except (ValueError, ZeroDivisionError) as exc:\n        raise ValueError(\"native snapshot fps is missing, invalid or contradictory\") from exc\n    for cfg, key in [(\"project_name\", \"project\"), (\"timeline_name\", \"timeline\")]:\n        if _text(baseline[cfg]) != snapshot.get(key):\n            raise ValueError(\"native snapshot identity mismatch\")\n    tracks = snapshot.get(\"timeline_readback\")\n    if not isinstance(tracks, dict) or not tracks:\n        raise ValueError(\"native clip snapshot required\")\n    for track, clips in tracks.items():\n        if not re.fullmatch(r\"(?:video|audio)[1-9][0-9]*\", track) or not isinstance(clips, list):\n            raise ValueError(\"invalid native snapshot track\")\n        for clip in clips:\n            _check_clip(clip, bounds)\n    return tracks\n","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/integration.py#L187-L223","documentation":"The native snapshot's timelineFrameRate, converted to a string, must match a bounded decimal or rational label: up to 9 digits, optionally followed by 1-12 decimal places or /N rational form. Anything else (scientific notation, negative values, extra text, units) is rejected with this error.","triggerScenarios":"timelineFrameRate stringifies to something like \"2.4e+01\", \"24 fps\", \"-24\", \"24.0000000000001\" (>12 decimals), \"24000/10010\" (denominator with leading zero pattern), or a float str() artifact like \"23.975999999999999\".","commonSituations":"A pipeline storing fps as a float loses the clean label and str() produces long repr; another tool writes scientific notation; someone edits the snapshot and adds units or whitespace; int-typed fps exported as \"24.0 fps\".","solutions":["Set timelineFrameRate to a plain label: \"23.976\", \"29.97\", \"59.94\", \"24\", or the rational form \"24000/1001\".","If the value is a float, round/serialize it deliberately (e.g. format to a fixed decimal string) instead of relying on str(float).","Strip units, whitespace and signs from the fps string in the exporter.","Prefer string or exact rational export from the snapshot producer to avoid float repr artifacts."],"exampleFix":"// before\n\"timelineFrameRate\": 2.3976000000000001e+01\n\n// after\n\"timelineFrameRate\": \"23.976\"","handlingStrategy":"validation","validationCode":"import re\nFPS_RE = re.compile(r\"[0-9]{1,9}(?:\\.[0-9]{1,12}|/[1-9][0-9]{0,8})?\")\ndef check_fps_label(value):\n    label = str(value)\n    if not FPS_RE.fullmatch(label):\n        raise ValueError(f\"fps label {label!r} is not a bounded decimal or rational\")\n    return label","typeGuard":"def is_bounded_fps_label(value) -> bool:\n    return isinstance(value, (str, int, float)) and bool(re.fullmatch(r\"[0-9]{1,9}(?:\\.[0-9]{1,12}|/[1-9][0-9]{0,8})?\", str(value)))","tryCatchPattern":"try:\n    bundle = build_application_bundle(config, None, local_only=True)\nexcept ValueError as exc:\n    if \"bounded decimal or rational label\" in str(exc):\n        print(\"normalize fps to e.g. '23.976' or '24000/1001'\")\n    raise","preventionTips":["Serialize fps as strings with fixed formats, never raw floats.","Round floats to a bounded decimal before export.","Strip units, signs and whitespace from fps strings.","Use rational form for NTSC rates when possible."],"tags":["validation","format","fps"],"backgroundTag":"invalid-argument-format","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"}