# Recoil Atlas — RENDER OVERLAY: the render/flow-graph SSOT for the engine Atlas.
#
# TWO GRAPHS, ONE SEAM (de-fork, commit 417281a7) — NOT a single structural graph:
#   * topology (recoil/architecture/topology/generated/topology.full.json) is the
#     DECISION/CAPABILITY SSOT: capabilities, routes, divergences, flags, loops —
#     drift-checked against the live code by build_topology.py --check.
#   * THIS FILE is the RENDER/FLOW SSOT: it OWNS the flow structure topology does
#     NOT model — node_type, edges (from/to adjacency), per-node gate text, per-node
#     inputs[] fan-in, status (live|substrate_only|deprecated), phase_group swimlane,
#     layout, media slots, hover copy. Topology has no node-level flow edges, so it
#     cannot and does not validate this file's nodes/edges/gates.
#   * tools/build_atlas_graph.py JOINS the two and validates ONLY the seam
#     (provenance resolves; substrate/human meta consistent; topology loop count
#     carried), then emits generated/atlas.render.json — the one artifact the
#     viewer renders. The viewer never reads this file directly, and never the
#     retired recoil/docs/pipeline_graph.yaml.
#
# Drift tie: tools/test_render_overlay_ssot.py validates this file against the
# modality registry and its own invariants (anti-rot). Node content below is the
# curated 4-agent audit projection — preserve verbatim on edit; do not hand-restructure.
#
# legend:
#   node_type: input | generation | gate | human_review | decision | store
#   status:    live | substrate_only | deprecated
#   phase_group order: narrative -> prepro -> production -> review_export -> infra

meta:
  node_count: 54
  edge_count: 82
  phase_group_counts:
    narrative: 15
    prepro: 9
    production: 8
    review_export: 2
    infra: 20
  substrate_only_nodes:
    - modality_eval
    - eval_panel
    - strategy_score_bridge
  human_review_nodes:
    - develop
    - wardrobe_intent_gate
    - previz_review
    - board_decision
    - dailies_review
    - final_review_export

nodes:
  # ===================== phase_group: narrative =====================
  - id: develop
    label: "Develop (34-point checklist)"
    phase_group: narrative
    node_type: human_review
    tool: "skill /develop (recoil/.claude/skills/develop/SKILL.md:1)"
    inputs:
      - { artifact: "concept / logline", from: external }
      - { artifact: "STATUS.md + existing dev docs", from: external }
    produces:
      - "thematic_spine.md"
      - "characters.md"
      - "relationship_map.md"
      - "structure_outline.md"
      - "plant_payoff_plan.md"
      - "STATUS.md (34 items)"
    gate: null
    loops:
      - { kind: director_note_route, to: develop, note: "interactive, section-by-section; --audit reports gaps without advancing (develop/SKILL.md:78-87)" }
    status: live
    evidence: "recoil/.claude/skills/develop/SKILL.md:36-141"
    uncertainties: "agent prompt /agents/develop_agent.md not read; checklist not verified line-by-line."

  - id: validate
    label: "Validate (pre-promotion gate)"
    phase_group: narrative
    node_type: gate
    tool: "skill /validate (recoil/.claude/skills/validate/SKILL.md:1)"
    inputs:
      - { artifact: "5 dev docs + STATUS.md", from: develop }
    produces:
      - "validation report (READY / READY WITH WARNINGS / NOT READY)"
    gate:
      validator: "skill /validate (recoil/.claude/skills/validate/SKILL.md:37-95)"
      pass_condition: "Tier-1 HARD GATES all pass (34/34, 5 dev docs, Law-2 beats Ep15/30/45/60, Primary Ache+2 threats, >=8 relationship markers, >=3 plant/payoff, >=1 Object/Phrase/Image thread, >=1 echo, protagonist Lie/Truth+3 need layers, antagonist counter-thesis, theme question) AND Tier-2 avg >= 3.5; Tier-3 warnings block only --strict"
    loops:
      - { kind: director_note_route, to: develop, note: "NOT READY -> back to /develop (validate/SKILL.md:88-102)" }
    status: live
    evidence: "recoil/.claude/skills/validate/SKILL.md:37-102"
    uncertainties: "scoring computation declarative in skill, not verified in code."

  - id: promote
    label: "Promote (create scripting folder + episode_arc)"
    phase_group: narrative
    node_type: generation
    tool: "skill /promote (recoil/.claude/skills/promote/SKILL.md:1)"
    inputs:
      - { artifact: "5 dev docs", from: develop }
      - { artifact: "validation PASS", from: validate }
    produces:
      - "bible/series_bible.md"
      - "bible/characters.md"
      - "bible/episode_arc.md (60 eps)"
      - "state/current_state.json"
      - "ORCHESTRATION.md"
    gate:
      validator: "promote prerequisites (recoil/.claude/skills/promote/SKILL.md:73-85)"
      pass_condition: "34/34 STATUS, 5 dev docs, validate_behavioral_dna.py pass, validate_episode_arc.py pass, /validate pass. Blocked if project exists unless --force"
    loops: []
    status: live
    evidence: "recoil/.claude/skills/promote/SKILL.md:36-118"
    uncertainties: "validate_behavioral_dna.py / validate_episode_arc.py bodies not read; 60-vs-61 ep count (see global note)."

  - id: pre_treatment_gate
    label: "Pre-treatment hard gate"
    phase_group: narrative
    node_type: gate
    tool: "validate_pre_treatment.py (recoil/.claude/hooks/validate_pre_treatment.py:1)"
    inputs:
      - { artifact: "bible/* + state/current_state.json", from: promote }
    produces:
      - "exit 0 proceed / 1 missing prereq / 2 path error"
    gate:
      validator: "validate_pre_treatment.py (recoil/.claude/hooks/validate_pre_treatment.py:35-60)"
      pass_condition: "episode_arc.md has eps 1..60, characters.md passes behavioral DNA, series_bible.md non-empty (exit 0)"
    loops:
      - { kind: director_note_route, to: promote, note: "fail -> run /promote (validate_pre_treatment.py:39-44)" }
    status: live
    evidence: "recoil/.claude/hooks/validate_pre_treatment.py:9-60; treatment/SKILL.md:16-19,100"
    uncertainties: "behavioral-DNA subprocess + exit aggregation not read line-by-line."

  - id: treatment
    label: "Treatment (episode_arc -> treatment.md MASTER)"
    phase_group: narrative
    node_type: generation
    tool: "skill /treatment (recoil/.claude/skills/treatment/SKILL.md:1)"
    inputs:
      - { artifact: "bible/episode_arc.md", from: promote }
      - { artifact: "bible/characters.md", from: promote }
      - { artifact: "bible/series_bible.md", from: promote }
      - { artifact: "pre-treatment gate PASS", from: pre_treatment_gate }
    produces:
      - "treatment.md (MASTER: prose/ep, THE MOMENT, cliffhanger image, THREAD INDEX)"
    gate:
      validator: "validate_treatment.py (recoil/tools/validate_treatment.py)"
      pass_condition: "passes validate_treatment.py; hook ratio 70-85% SILENT, cliffhanger 70-85% MID-ACTION (CONSTANTS.md:70-71)"
    loops:
      - { kind: director_note_route, to: treatment, note: "--close-read/--plus/--flag-weak iterate (treatment/SKILL.md:42-49)" }
    status: live
    evidence: "recoil/.claude/skills/treatment/SKILL.md:12-103; validator at recoil/tools/validate_treatment.py"
    uncertainties: "validate_treatment.py body not read; numeric conditions inferred."

  - id: generate_script
    label: "Generate Script (per-batch episode generation)"
    phase_group: narrative
    node_type: generation
    tool: "skills /generate-script + /generate-script-orchestrated"
    inputs:
      - { artifact: "treatment.md section (prose, THE MOMENT, cliffhanger)", from: treatment }
      - { artifact: "bible/characters.md (voice/stage)", from: promote }
      - { artifact: "format_v12 + CONSTANTS (constraints)", from: external }
      - { artifact: "last 2 episodes (continuity)", from: episode_metrics_gate }
      - { artifact: "current_state.json (next_batch)", from: promote }
    produces:
      - "scripting/episodes/ep_XXX.md (Kill Box; 450-500 words; <=40% dialogue; <=8 exchanges)"
      - "state/batch_N_summary.json (orchestrated)"
    gate: null
    loops:
      - { kind: reroll, to: generate_script, note: "is_valid:false -> --prompt fix, max 3 (generate-script/SKILL.md:131-187)" }
      - { kind: director_note_route, to: generate_script, note: "cinematization + subtext (~70%) passes after word-count validates" }
    status: live
    evidence: "generate-script/SKILL.md:91-231; generate-script-orchestrated/SKILL.md:151-271; recoil/CLAUDE.md:49,77-89"
    uncertainties: "batch=5; 60-vs-61 / 12-vs-13 batch count: skills hardcode 12/60, CONSTANTS says 13/61."

  - id: episode_metrics_gate
    label: "Episode Metrics (per-episode validator)"
    phase_group: narrative
    node_type: gate
    tool: "episode_metrics.py (recoil/tools/episode_metrics.py:617)"
    inputs:
      - { artifact: "ep_XXX.md (just written)", from: generate_script }
    produces:
      - "JSON metrics + is_valid; exit 0/1/2; --prompt fix instructions"
    gate:
      validator: "episode_metrics.py is_valid (recoil/tools/episode_metrics.py:119-123,356-399)"
      pass_condition: "word_count 450-500 AND exchanges <=8 AND dialogue <=40% AND kill_box_status==OK (5 sections present, ordered HOOK->SETUP->ESCALATION->TURN->CLIFFHANGER, non-empty)"
    loops:
      - { kind: reroll, to: generate_script, note: "is_valid:false -> --prompt fix loop max 3 (recoil/CLAUDE.md:77-89)" }
    status: live
    evidence: "recoil/tools/episode_metrics.py:99-123,644-677; CONSTANTS.md:13-16,28; recoil/CLAUDE.md:77-89"
    uncertainties: "pattern distribution + action-block length REPORTED but NOT in is_valid (warnings only, 513-527)."

  - id: batch_checkpoint_gate
    label: "Batch Checkpoint Gate (unlock next batch)"
    phase_group: narrative
    node_type: gate
    tool: "save_checkpoint.py (recoil/.claude/hooks/save_checkpoint.py:333)"
    inputs:
      - { artifact: "5 episodes of batch N", from: generate_script }
      - { artifact: "per-episode metrics PASS", from: episode_metrics_gate }
    produces:
      - "state/checkpoints/batch_NN_checkpoint.json + next_batch unlocked"
    gate:
      validator: "save_checkpoint.py -> validate_batch.py + quality_gate.py (HARD); dramatic_qc_gate.py + baseline_comparison.py (SOFT); transition_gate.py @batch12 (HARD)"
      pass_condition: "HARD: validate_batch.py exit0 (word/dialogue/exchanges/pattern) AND quality_gate.py exit0 (variety/beats/continuity); @batch12 ALSO transition_gate.py != hard-fail (59 transitions). SOFT warn-only: dramatic_qc_gate.py, baseline_comparison.py @batches 3/6/9/12"
    loops:
      - { kind: reroll, to: generate_script, note: "fail -> checkpoint NOT saved, next batch BLOCKED, regen (save_checkpoint.py:368-383)" }
      - { kind: director_note_route, to: generate_script, note: "soft QC/voice flags recommend /rewrite, don't block" }
    status: live
    evidence: "recoil/.claude/hooks/save_checkpoint.py:1-40,62-117,333-529; CONSTANTS.md:24-34"
    uncertainties: "gate bodies (validate_batch/quality_gate/transition_gate/dramatic_qc) not opened."

  - id: finish
    label: "Finish (post-gen pipeline orchestrator)"
    phase_group: narrative
    node_type: decision
    tool: "skill /finish (recoil/.claude/skills/finish/SKILL.md:1)"
    inputs:
      - { artifact: "60 episodes ep_*.md", from: generate_script }
      - { artifact: "treatment.md, characters.md, GEMINI_API_KEY", from: external }
    produces:
      - "orchestrated doctor->annotations->revise->validate->verify->compile"
    gate:
      validator: "Step 1 Preflight (finish/SKILL.md:44-62)"
      pass_condition: "episodes + treatment.md + characters.md exist, GEMINI_API_KEY set"
    loops: []
    status: live
    evidence: "recoil/.claude/skills/finish/SKILL.md:40-180,228-267"
    uncertainties: "--from/--skip-doctor change which sub-steps run."

  - id: finish_script_doctor
    label: "Script Doctor (Gemini multi-pass diagnostic)"
    phase_group: narrative
    node_type: generation
    tool: "script_doctor.py --full; skill /script-doctor"
    inputs:
      - { artifact: "full corpus + characters.md + treatment.md", from: generate_script }
    produces:
      - "_pipeline/state/script_doctor_brief.json (structural T/I, close-read C, series F; P1/P2/P3)"
    gate: null
    loops: []
    status: live
    evidence: "finish/SKILL.md:64-82; script-doctor/SKILL.md:66-156"
    uncertainties: "diagnoses only; does not modify episodes."

  - id: finish_extract_annotations
    label: "Extract Annotations"
    phase_group: narrative
    node_type: generation
    tool: "script_doctor.py --to-annotations (finish/SKILL.md:84-92)"
    inputs:
      - { artifact: "script_doctor_brief.json", from: finish_script_doctor }
    produces:
      - "_pipeline/state/script_doctor_annotations.json (REWRITE/DELETE/FLAG)"
    gate: null
    loops: []
    status: live
    evidence: "finish/SKILL.md:84-92; script-doctor/SKILL.md:189-197"
    uncertainties: ""

  - id: finish_apply_revisions
    label: "Apply Revisions"
    phase_group: narrative
    node_type: generation
    tool: "apply_annotations.py (finish/SKILL.md:94-108)"
    inputs:
      - { artifact: "script_doctor_annotations.json", from: finish_extract_annotations }
      - { artifact: "ep_XXX.md", from: generate_script }
    produces:
      - "modified ep_XXX.md (REWRITE+DELETE auto; FLAG logged for manual)"
    gate: null
    loops:
      - { kind: director_note_route, to: finish_apply_revisions, note: "FLAG NOT auto-applied -> /rewrite or --deep-fix (finish/SKILL.md:96-108)" }
    status: live
    evidence: "finish/SKILL.md:94-108,258-262"
    uncertainties: "/revise is standalone batch-apply for same annotations."

  - id: finish_revalidate
    label: "Re-validate Modified Episodes"
    phase_group: narrative
    node_type: gate
    tool: "episode_metrics.py --json (finish/SKILL.md:110-128)"
    inputs:
      - { artifact: "episodes modified by revisions", from: finish_apply_revisions }
    produces:
      - "per-episode is_valid; word-count auto-adjust"
    gate:
      validator: "episode_metrics.py (recoil/tools/episode_metrics.py:119-123)"
      pass_condition: "is_valid:true per modified ep; OVER->trim, UNDER->add detail, max 3 attempts then manual"
    loops:
      - { kind: reroll, to: finish_revalidate, note: "out of range -> 3 attempts (finish/SKILL.md:118-128)" }
    status: live
    evidence: "finish/SKILL.md:110-128"
    uncertainties: "reuses episode_metrics gate; only modified eps."

  - id: finish_verify
    label: "Verify (post-revision, informational)"
    phase_group: narrative
    node_type: gate
    tool: "script_doctor.py --verify (finish/SKILL.md:130-138)"
    inputs:
      - { artifact: "revised + re-validated episodes", from: finish_revalidate }
    produces:
      - "verification result + remaining issues count"
    gate:
      validator: "script_doctor.py --verify (finish/SKILL.md:130-138)"
      pass_condition: "INFORMATIONAL ONLY — does NOT block compile (finish/SKILL.md:137)"
    loops:
      - { kind: director_note_route, to: finish_apply_revisions, note: "issues remain -> re-extract -> /revise -> verify (script-doctor/SKILL.md:202-207)" }
    status: live
    evidence: "finish/SKILL.md:130-138; script-doctor/SKILL.md:202-207"
    uncertainties: ""

  - id: finish_compile
    label: "Compile (episodes -> Fountain)"
    phase_group: narrative
    node_type: generation
    tool: "compile_episodes.py; skill /compile"
    inputs:
      - { artifact: "final ep_XXX.md corpus", from: finish_verify }
    produces:
      - "[TITLE]_COMPLETE.fountain"
      - "[TITLE]_WITH_METADATA.fountain"
    gate: null
    loops: []
    status: live
    evidence: "finish/SKILL.md:140-154; compile/SKILL.md:60-95"
    uncertainties: "narrative->visual handoff boundary (Script Lock)."

  # ===================== phase_group: prepro =====================
  - id: script_lock
    label: "Script Lock"
    phase_group: prepro
    node_type: store
    tool: "Narrative /finish -> /compile (recoil/CLAUDE.md); locked script consumed by visual pipeline"
    inputs:
      - { artifact: "validated episode scripts (episode_metrics passed)", from: finish_compile }
    produces: ["scripting/episodes/ep_NNN.md (locked screenplay SSOT)"]
    gate:
      validator: "tools/episode_metrics.py (recoil/CLAUDE.md EPISODE WRITING RULE)"
      pass_condition: "is_valid:true per episode (V12) before promotion to visual"
    loops:
      - { kind: director_note_route, to: "scripting/episodes/ep_NNN.md", note: "chains A/B/C causality/set-geography/continuity patch locked script (PLAYBOOK:13-64); note_id-gated (REC-176)" }
    status: live
    evidence: "recoil/CLAUDE.md PIPELINE; pipeline/CLAUDE.md:90; DIRECTOR_NOTES_PLAYBOOK.md:13"
    uncertainties: "no single script_lock function; lock is conventional (validated+compiled), consumed by ingest_pipeline._load_episode_script()."

  - id: camera_test
    label: "Camera Test Pass (Stage 0)"
    phase_group: prepro
    node_type: generation
    tool: "/camera-test + ingest_pipeline.run_camera_test (ingest_pipeline.py:633,657); CAMERA_TEST_MODEL"
    inputs:
      - { artifact: "locked episode script text (_load_episode_script)", from: script_lock }
    produces: ["_pipeline/state/visual/camera_tested/ep_NNN.json (shot-boundary blocks)", "derivation manifest stamp (script_sha source)"]
    gate:
      validator: "validate_camera_test_budget + Pydantic CameraTestedEpisode (ingest_pipeline.py:718,728)"
      pass_condition: "Pydantic-valid AND min<=total_shots<=max (default 28-41); MAX_RETRIES=2 then save-with-warning or review_queue"
    loops:
      - { kind: reroll, to: camera_test, note: "retry w/ error-feedback up to 2 (ingest_pipeline.py:698-735); hard parse-fail -> review_queue" }
    status: live
    evidence: "ingest_pipeline.py:633-766; camera-test/SKILL.md; ingest_pipeline.py:97"
    uncertainties: "SKILL.md text-mutation mode vs orchestrator json; the json is canonical."

  - id: global_bible
    label: "Global Bible (Stage 1 / Breakdown Pass)"
    phase_group: prepro
    node_type: generation
    tool: "/global-bible + ingest_pipeline.run_breakdown_pass (ingest_pipeline.py:772)"
    inputs:
      - { artifact: "all camera_tested/ep_NNN.json in batch", from: camera_test }
      - { artifact: "character bible scripting/bible/characters.md (DNA)", from: script_lock }
      - { artifact: "project_config + existing global_bible.json (incremental merge)", from: external }
    produces:
      - "_pipeline/state/visual/global_bible.json (chars+phases+looks, locations, props, lighting; [OPUS_ENRICHMENT] placeholders)"
      - "episode_look_map.json (--detect-looks)"
      - "brief_visual/brief_wardrobe -> casting_state.json"
    gate:
      validator: "Pydantic GlobalBible + bible validation (global-bible/SKILL.md:110)"
      pass_condition: "all CT-shot chars in bible; every char >=1 wardrobe phase; non-overlapping phase ranges; uppercase snake_case ids; 2 retries then review_queue"
    loops:
      - { kind: reroll, to: global_bible, note: "Pydantic error -> retry x2; merge preserves human edits" }
      - { kind: director_note_route, to: "global_bible.json locations.<id>.sublocations", note: "set-geography (chain A) patches sublocation desc; regen ref (PLAYBOOK:14,37-42)" }
    status: live
    evidence: "ingest_pipeline.py:772; global-bible/SKILL.md:149; ssot_manifest output_layout"
    uncertainties: "SKILL.md cites legacy paths; live run uses _pipeline/state/visual/."

  - id: enrichment
    label: "Opus Enrichment (Stage 1.5)"
    phase_group: prepro
    node_type: generation
    tool: "ingest_pipeline.run_opus_enrichment (ingest_pipeline.py:2027); OPUS_ENRICHMENT_MODEL"
    inputs:
      - { artifact: "global_bible.json [OPUS_ENRICHMENT] placeholders", from: global_bible }
      - { artifact: "scripting/bible/characters.md per-entity DNA", from: script_lock }
    produces: ["global_bible.json placeholders filled; failures stamped [OPUS_ENRICHMENT_FAILED]"]
    gate:
      validator: "_find_placeholders re-scan (ingest_pipeline.py:1069); downstream warn-only"
      pass_condition: "no remaining [OPUS_ENRICHMENT]; failures non-blocking"
    loops:
      - { kind: reroll, to: enrichment, note: "per-entity; failure -> marker, not hard cap (ingest_pipeline.py:2172,2185)" }
    status: live
    evidence: "ingest_pipeline.py:2027-2185; run_full Stage 1.5 (skippable --skip-enrichment)"
    uncertainties: ""

  - id: plan_pass
    label: "Plan Pass (Stage 2 / Storyboard Pass)"
    phase_group: prepro
    node_type: generation
    tool: "/storyboard-pass + ingest_pipeline.run_storyboard_pass (ingest_pipeline.py:1054); STORYBOARD_MODEL (free Opus OAuth)"
    inputs:
      - { artifact: "camera_tested/ep_NNN.json (1 episode)", from: camera_test }
      - { artifact: "global_bible.json (enriched; episode summary)", from: enrichment }
    produces:
      - "_pipeline/state/visual/plans/ep_NNN_plan.json — the SHOT SKELETON (EpisodePlan: ShotRecord x5 consumer groups)"
      - "scene-level axis_plans (SceneAxisPlan: 180-line + jumps/re-establish) LLM-authored"
      - "per-shot spatial_data DERIVED (camera_side, screen_direction, axis_segment_id, cut_relation) via propagate_axis"
    gate:
      validator: "Pydantic CreativeEpisodeOutput -> validate/sanitize_axis_plans (axis_validation.py:120,141) -> _validate_motion_lines -> validate_spatial_data (spatial_validation.py:35)"
      pass_condition: "1:1 shot count vs CT; location_id/char_id exist in bible; phase_id matches; axis structurally valid else neutral-fallback; motion_line present; MAX_RETRIES=2"
    loops:
      - { kind: reroll, to: plan_pass, note: "axis/motion/assembly errors -> retry x2; final-attempt DEGRADE: drop invalid axis_plans -> neutral-fallback not cap (ingest_pipeline.py:1148-1206)" }
      - { kind: director_note_route, to: "raw.prompt_data.prompt_skeleton.motion_line", note: "under-animation (chain D) patches motion_line (PLAYBOOK:16,66-69)" }
    status: live
    evidence: "ingest_pipeline.py:1054-1213; storyboard-pass/SKILL.md; axis_propagation.py:62; axis_validation.py; spatial_validation.py; coverage-pass-strategy.md"
    uncertainties: "spatial validation auto_correct=False (advisory) since screen_direction now propagate_axis-derived (ingest_pipeline.py:1320-1326)."

  - id: blocking_pass
    label: "Blocking Pass (Stage 2.5)"
    phase_group: prepro
    node_type: generation
    tool: "ingest_pipeline.run_blocking_pass -> blocking_pass.BlockingPass.run (ingest_pipeline.py:1808)"
    inputs:
      - { artifact: "plans/ep_NNN_plan.json (REQUIRED, after storyboard pass)", from: plan_pass }
      - { artifact: "scene source text (locked script)", from: script_lock }
      - { artifact: "global_bible.json", from: enrichment }
    produces: ["blocking_metadata per shot (SBD + freeze-frame: gaze_target, head_facing, stance) + updated subject_lines; persisted scene JSON _pipeline/state/orchestration/scenes/"]
    gate:
      validator: "Pydantic SBD schema (Gemini Pro per scene); blocking_pass.py"
      pass_condition: "valid SBD + per-shot blocking JSON per scene"
    loops:
      - { kind: director_note_route, to: "blocking_metadata.characters[].{gaze_target,head_facing,stance,staging_note}", note: "per-shot staging (chain A step 4) patches persisted scene blocking (PLAYBOOK:15,44)" }
    status: live
    evidence: "ingest_pipeline.py:1808-1840; blocking_pass.py; PLAYBOOK:15"
    uncertainties: "not in run_full (Stage 0/1/1.5/2 only); invoked separately per episode."

  - id: casting
    label: "Casting (character refs)"
    phase_group: prepro
    node_type: generation
    tool: "casting API (recoil/pipeline/api/routes/casting.py) via Workspace 8450; prep_character_angles.py"
    inputs:
      - { artifact: "global_bible.json characters.<id>.{visual_description,gender,phases,aesthetic_directives,props}", from: global_bible }
      - { artifact: "brief_visual/brief_wardrobe (casting_state.json)", from: global_bible }
      - { artifact: "episode_look_map.json", from: global_bible }
    produces:
      - "grid_images contact-sheet [generate-grid casting.py:727]"
      - "assets/char/<slug>/hero.* [select-hero casting.py:820]"
      - "assets/char/<slug>/{front,three_quarter,profile,back}.* [generate-turnaround casting.py:898]"
      - "assets/expressions/*_active.png (27-ref matrix) [generate-expressions casting.py:1046]"
      - "assets/char/<slug>/base/pool/{identity,turn,expr}/ (v3 canonical pool)"
    gate:
      validator: "casting_state status-machine + Screen Test verdict (casting.py:921,3114) + bible-synced flag (:1260)"
      pass_condition: "hero_selected before turnaround (:921); Screen Test status empty->generating->generated->locked, verdict in {lock,hold,reject}; set-anchor requires locked (:2862)"
    loops:
      - { kind: reroll, to: casting, note: "reroll-grid (:1880), reroll-phase (:1988), screen-test reroll (:2886); director_note enriched; _v{N}.png" }
    status: live
    evidence: "casting.py routes; pipeline/CLAUDE.md:78-81; global-bible/SKILL.md:43-51"
    uncertainties: "Wardrobe Intent Gate + Screen Test are sub-phases; not in run_full() — interactive via casting API/Workspace."

  - id: wardrobe_intent_gate
    label: "Wardrobe Intent Gate (sub-phase of Casting)"
    phase_group: prepro
    node_type: human_review
    tool: "casting.py wardrobe-intent endpoints (propose/approve philosophy, theses, rewrite phases)"
    inputs:
      - { artifact: "treatment.md + series_bible.md (philosophy)", from: script_lock }
      - { artifact: "global_bible.json wardrobe fields + episode_arc.md", from: global_bible }
    produces: ["global_bible.json: wardrobe_philosophy(+approved), wardrobe_arc_thesis(+approved), phases.<id>.wardrobe_description/arc_delta/arc_carries"]
    gate:
      validator: "approval flags (casting.py:2268 approve-philosophy, 2353 approve-thesis, 2475 apply-rewrite)"
      pass_condition: "philosophy approved -> theses; thesis_approved before rewrite-phases (:2427); apply-rewrite gates writes to matching phase_ids"
    loops:
      - { kind: director_note_route, to: "global_bible.json wardrobe fields", note: "propose->approve cycle; rewrites land in bible SSOT, feed Screen Test" }
    status: live
    evidence: "casting.py:2176-2533"
    uncertainties: ""

  - id: location_refs
    label: "Location Refs"
    phase_group: prepro
    node_type: generation
    tool: "casting.py location endpoints (generate-location :1112, select-location-hero :1162, update-moodboard :1213)"
    inputs:
      - { artifact: "global_bible.json locations.<id>.{description,atmosphere}", from: global_bible }
    produces:
      - "assets/loc/<slug>/base/pool/loc/ moodboard set [generate-location]"
      - "assets/loc/<slug>/hero.* [select-location-hero]"
      - "casting_state.json locations.<id>.moodboard_picks [update-location-moodboard]"
    gate:
      validator: "ref_path existence (casting.py:1162); moodboard_picks list type"
      pass_condition: "location hero requires ref_path on disk; generate-location async"
    loops:
      - { kind: director_note_route, to: "global_bible.json locations.<id>.sublocations + assets/loc/<id>/base/sublocations/", note: "set-geography regen sublocation refs (chain A; gen_sublocations.py restamp trap)" }
    status: live
    evidence: "casting.py:1112-1260; pipeline/CLAUDE.md:79"
    uncertainties: "shares casting.py + casting_state.json; treated as distinct ordered phase from Casting."

  # ===================== phase_group: production =====================
  - id: previz
    label: "Previz Generation (gut-check boards)"
    phase_group: production
    node_type: generation
    tool: "build_previz_context + build_previs_prompt (recoil/pipeline/_lib/previz_context.py:1706, prompt_engine.py:6714)"
    inputs:
      - { artifact: "shot skeleton / script beat (ep_NNN_plan.json shot record)", from: plan_pass }
      - { artifact: "scoped Global Bible text", from: global_bible }
      - { artifact: "3-shot continuity window (N-1,N,N+1)", from: plan_pass }
      - { artifact: "location moodboards (2-3)", from: location_refs }
      - { artifact: "prop ref (1)", from: location_refs }
      - { artifact: "character hero + 1 turnaround per char", from: casting }
      - { artifact: "expression ref (1, non-neutral)", from: casting }
      - { artifact: "cinema-mode constraint/style tokens", from: cinema_mode }
      - { artifact: "behavioral preamble + generative directive", from: external }
    produces: ["previz assets (prep/ep_NNN/)", "previz_inputs_snapshot"]
    gate: null
    loops: []
    status: deprecated
    evidence: "previz_context.py:1706-1775 (8-slot fan-in); take_inputs.py:91 snapshot; pipeline/CLAUDE.md:97-98"
    uncertainties: "DEPRECATED 2026-06-18 (REC-192): code exists but run_episode_batches never invokes previz generation; the live path is plan_pass -> blocking_pass -> keyframes (boards). A prior previz frame may still be loaded as a fallback start_frame, but previz is no longer a live stage."

  - id: previz_review
    label: "Previz Review (HUMAN)"
    phase_group: production
    node_type: human_review
    tool: "Recoil Workspace 8450 (recoil/workspace/server.py)"
    inputs:
      - { artifact: "previz assets + inputs snapshot", from: previz }
    produces: ["status: previs_pending -> (approve|previs_rejected)"]
    gate:
      validator: "Workspace promote_item / reject_shot (server.py:2293, :2369)"
      pass_condition: "human approve; reject sets previs_rejected (server.py:2395-2396)"
    loops:
      - { kind: reroll, to: previz, note: "reject -> regenerate previz" }
      - { kind: director_note_route, to: ssot_layer, note: "physical/blocking/spatial notes -> script/bible/blocking (PLAYBOOK; PIPELINE_SSOT protocol 10)" }
    status: deprecated
    evidence: "server.py:2369-2441; pipeline/CLAUDE.md:98"
    uncertainties: "DEPRECATED 2026-06-18 (REC-192): paired with the deprecated previz stage; not on the live path."

  - id: keyframes
    label: "Keyframe / Board Generation (production frames)"
    phase_group: production
    node_type: generation
    tool: "board_builder.build_and_dispatch_board + keyframe_context (board_builder.py:209, keyframe_context.py)"
    inputs:
      - { artifact: "shot skeleton / shot plan record (5 consumer groups)", from: plan_pass }
      - { artifact: "Global Bible (chars/locations/props/phases)", from: global_bible }
      - { artifact: "character/location/prop refs", from: casting }
      - { artifact: "location refs", from: location_refs }
      - { artifact: "cinema-mode tokens", from: cinema_mode }
    produces: ["proposed storyboard strip (prep/ep_NNN/storyboards/)", "board sidecar status=proposed", "structural_sha + source_sha256"]
    gate:
      validator: "board_builder auto-reroll critic loop (board_builder.py:673 build_with_auto_reroll); plan_pass_critic prereq"
      pass_condition: "board emitted status=proposed when clean; else status=rejected with reroll lineage (board_builder.py:735-821)"
    loops:
      - { kind: reroll, to: keyframes, note: "auto-reroll up to max_board_attempts (default 3); each = new board version (board_builder.py:61)" }
    status: live
    evidence: "board_builder.py:209,673-821; storyboard-pass/SKILL.md; pipeline/CLAUDE.md:99"
    uncertainties: "doc 'Keyframes' maps to board strip + photoreal finish; keyframe_context.py discrete role not fully traced."

  - id: board_decision
    label: "Board/Keyframe Approval Gate (HUMAN)"
    phase_group: production
    node_type: human_review
    tool: "generate.py _run_board_decision (cli/generate.py:879) + Workspace board wall (workspace/board.py:40)"
    inputs:
      - { artifact: "proposed board (status=proposed)", from: keyframes }
    produces: ["board status: approved|rejected", "scene locked", "board SSOT stamp in manifest.execution.boards"]
    gate:
      validator: "_run_board_decision freshness + artifact checks (generate.py:879-1019)"
      pass_condition: "source_sha256 == board.source_sha256 (not stale) AND artifact valid AND human approve (generate.py:949-986)"
    loops:
      - { kind: reroll, to: keyframes, note: "reject -> reject_board; stale sha -> regenerate --storyboard (generate.py:951-953)" }
    status: live
    evidence: "generate.py:879-1019; board.py:98; pipeline/CLAUDE.md:99"
    uncertainties: ""

  - id: photoreal_finish
    label: "Photoreal Board Finish (full-size keyframe)"
    phase_group: production
    node_type: generation
    tool: "board_builder.render_board_finish (board_builder.py:508)"
    inputs:
      - { artifact: "approved board (fresh source_sha256)", from: board_decision }
    produces: ["photoreal_artifact (validated on disk)"]
    gate:
      validator: "render_board_finish + resolve_board_for_spend (board_builder.py:508-560,:101)"
      pass_condition: "board approved AND sha not stale; raises BoardBuilderError if stale -> re-run --storyboard"
    loops: []
    status: live
    evidence: "board_builder.py:508-560; generate.py:1016-1019"
    uncertainties: ""

  - id: video_gen
    label: "Video Generation (coverage passes / takes)"
    phase_group: production
    node_type: generation
    tool: "EpisodeRunner.run_episode_batches -> dispatch() (episode_runner.py:2197); /generate-video"
    inputs:
      - { artifact: "approved board ref (into reference_images)", from: photoreal_finish }
      - { artifact: "canonical plan shots / coverage passes", from: plan_pass }
      - { artifact: "character/location refs (resolved)", from: casting }
      - { artifact: "cinema-mode camera/style tokens", from: cinema_mode }
    produces: ["renders/ep_NNN/*.mp4 (per take)", "boundary_frames/", "GenerationReceipt", "Scene/Beat/Take JSON"]
    gate:
      validator: "pre-dispatch gates in _dispatch_one_beat: board gate (:224), budget (:1725 BudgetGuard), inputs_fingerprint drift (:1401-1420), phantom-success demotion (:460), stale-take recovery (:367)"
      pass_condition: "board gate: r2v_multi beats need approved+fresh board (:224-265); budget: est cost <= remaining; refs fingerprint matches (else demote+rebuild)"
    loops:
      - { kind: reroll, to: video_gen, note: "retry = NEW Take (beat.new_take), never mutate; --new-take/POST reroll preflighted (:736,:2110); StrategyEngine mutates next workflow (:2013)" }
      - { kind: director_note_route, to: ssot_layer, note: "approve board -> ref change -> fingerprint drift -> demote succeeded take -> re-render (:591-598 V-D trigger)" }
    status: live
    evidence: "episode_runner.py:2197-2593,:1674-2106,:224,:1725; coverage-pass-strategy.md"
    uncertainties: ""

  - id: dispatch_audit
    label: "Dispatch-Audit Gate (output-shape, build-time)"
    phase_group: production
    node_type: gate
    tool: "audit_dispatch.py (recoil/pipeline/tools/audit_dispatch.py)"
    inputs:
      - { artifact: "synthetic fixture plan (audit_plan.json)", from: external }
    produces: ["PASS/FAIL per (modality x model x modifier) dispatch shape"]
    gate:
      validator: "audit_assertions.run_all_assertions over build_dispatch_payload(dry_run=True)"
      pass_condition: "exit 0 = every enumerated path passes all 16 assertions; 1=assert fail; 2=infra"
    loops: []
    status: live
    evidence: "audit_dispatch.py:1-30,:49"
    uncertainties: "build-time guardrail over payload-builder contract, NOT a per-render runtime gate."

  - id: cinema_mode
    label: "Cinema Mode preset layer (camera/lens/stock/grade)"
    phase_group: production
    node_type: input
    tool: "cinema_loader.py + CINEMA_MODES.yaml (recoil/config/CINEMA_MODES.yaml)"
    inputs:
      - { artifact: "project_config.json::cinema_mode activation", from: external }
    produces: ["camera-body/lens/stock/grade tokens emitted into prompts via prompt_engine"]
    gate: null
    loops: []
    status: live
    evidence: "recoil/CLAUDE.md CINEMA MODE FRAMEWORK; pipeline/CLAUDE.md:163-174 Cinema Mode Builder Wiring"
    uncertainties: "couples at prompt-build via prompt_engine constraint/style blocks, not a discrete fan-in slot."

  # ===================== phase_group: review_export =====================
  - id: dailies_review
    label: "Dailies Review (HUMAN)"
    phase_group: review_export
    node_type: human_review
    tool: "Recoil Workspace 8450 (server.py)"
    inputs:
      - { artifact: "rendered takes (renders/ep_NNN/*.mp4) + segments", from: video_gen }
    produces: ["status: pending -> (promote|keyframe_rejected|video_rejected|rejected)"]
    gate:
      validator: "reject_shot/reject_segment/regenerate_shot (server.py:2369,:3436,:3488)"
      pass_condition: "human approve/promote; reject -> keyframe/video_rejected (:2397-2402); failure_mode logged to LearningEngine (:2436)"
    loops:
      - { kind: reroll, to: video_gen, note: "reject take/segment -> regenerate (new Take); regenerate_shot patches sidecar replaced_by (:3488-3602)" }
      - { kind: director_note_route, to: ssot_layer, note: "verbal notes -> owning SSOT layer (PLAYBOOK)" }
    status: live
    evidence: "server.py:2369-2441,:3436-3484,:3488-3602; pipeline/CLAUDE.md:100"
    uncertainties: ""

  - id: final_review_export
    label: "Final Review + Export"
    phase_group: review_export
    node_type: human_review
    tool: "Recoil Workspace 8450 (take selection / cut-point confirm) + export"
    inputs:
      - { artifact: "approved takes + confirmed cut points", from: dailies_review }
    produces: ["final episode cut / EDL-FCPXML handoff (Phase C)", "selected primary takes"]
    gate:
      validator: "confirm_timestamps / get_detected_cuts (server.py:3287,:3240)"
      pass_condition: "human confirms cut timestamps; cut-detection alignment within tolerance (:3287-3407)"
    loops:
      - { kind: reroll, to: video_gen, note: "final reject -> re-render affected pass as new take" }
    status: live
    evidence: "server.py:3240-3407; coverage-pass-strategy.md; pipeline/CLAUDE.md:101"
    uncertainties: "discrete Export/EDL writer not located; EDL/FCPXML cited as Phase C goal (pipeline/CLAUDE.md:228)."

  # ===================== phase_group: infra =====================
  - id: dispatch_entry_point
    label: "dispatch() — single generation entry point"
    phase_group: infra
    node_type: decision
    tool: "recoil/pipeline/core/dispatch.py::dispatch (dispatch.py:294)"
    inputs:
      - { artifact: "modality string + payload dict", from: workflow_step }
      - { artifact: "DispatchContext (caller_id, step_runner, project, episode)", from: external }
    produces: ["GenerationReceipt", "_dispatch_logs/receipts.jsonl line"]
    gate:
      validator: "_validate_payload + _validate_model_constraints (dispatch.py:232,:200)"
      pass_condition: "payload shape matches modality contract; model profile flags honored"
    loops: []
    status: live
    evidence: "dispatch.py:294-392; lazy bootstrap :183; JSONL emit :61; ssot_manifest.yaml:88 active"
    uncertainties: "Phase-19 EventBus emit (:80) feature-flagged off (RECOIL_EVENTBUS_ENABLED!=1)."

  - id: modality_registry
    label: "Modality registry — 'any model, one socket'"
    phase_group: infra
    node_type: store
    tool: "recoil/pipeline/core/registry.py::RunnerRegistry (registry.py:138); register_default_runners (dispatch.py:140)"
    inputs:
      - { artifact: "modality_id -> runner instance", from: dispatch_entry_point }
    produces: ["resolved ModalityRunner via get_runner(modality)"]
    gate:
      validator: "RunnerRegistry.get (registry.py:216) — KeyError if unregistered"
      pass_condition: "modality string registered (instance or factory)"
    loops: []
    status: live
    evidence: "registry.py:46-55; dispatch.py:166-180; ssot_manifest.yaml:151. LIVE: image_t2i, video_i2v, r2v_multi, storyboard, audio_t2a, lipsync_post"
    uncertainties: "adding a modality = one-line register_runner() (registry.py:7-11)."

  - id: modality_image_t2i
    label: "image_t2i (keyframe/still) modality"
    phase_group: infra
    node_type: generation
    tool: "ImageRunner wraps StepRunner.execute_keyframe (registry.py:46)"
    inputs: [{ artifact: "keyframe payload", from: dispatch_entry_point }]
    produces: ["RunResult (output_path png)"]
    gate: { validator: "runner-internal gate (gate_verdict in RunResult.metadata)", pass_condition: "generation + attached gates pass" }
    loops: []
    status: live
    evidence: "dispatch.py:167; registry.py:23-26"
    uncertainties: ""

  - id: modality_video_i2v
    label: "video_i2v (i2v/t2v) modality"
    phase_group: infra
    node_type: generation
    tool: "VideoRunner wraps StepRunner.execute_video"
    inputs: [{ artifact: "video payload (start_frame Path or t2v)", from: dispatch_entry_point }]
    produces: ["RunResult (output_path mp4)"]
    gate:
      validator: "_validate_payload video_i2v branch (dispatch.py:249)"
      pass_condition: "start_frame exists if set; legacy 'image' key rejected (Bug-C guard)"
    loops: []
    status: live
    evidence: "dispatch.py:168; ssot_manifest:151"
    uncertainties: ""

  - id: modality_r2v_multi
    label: "r2v_multi (multi-shot scene batching) modality"
    phase_group: infra
    node_type: generation
    tool: "R2VMultiRunner (registry.py:50)"
    inputs: [{ artifact: "reference_images + multi-shot payload", from: dispatch_entry_point }]
    produces: ["RunResult"]
    gate:
      validator: "_validate_payload r2v_multi branch (dispatch.py:268) — WARN only"
      pass_condition: "soft: zero reference_images logs warning (env-only allowed)"
    loops: []
    status: live
    evidence: "dispatch.py:170-171; ssot_manifest:158"
    uncertainties: "post-CP-5 modality (registry.py:57)."

  - id: modality_storyboard
    label: "storyboard (board panel) modality"
    phase_group: infra
    node_type: generation
    tool: "StoryboardRunner (registry.py:51)"
    inputs: [{ artifact: "storyboard payload (shot_id, prompt, model, size_override, save_dir, filename_stem)", from: dispatch_entry_point }]
    produces: ["board PNG RunResult"]
    gate:
      validator: "_validate_payload storyboard branch (dispatch.py:277)"
      pass_condition: "all 6 required payload keys present"
    loops:
      - { kind: director_note_route, to: board_story_gate, note: "board judged by story gate; rejection routes to script/board/prompt layer" }
    status: live
    evidence: "dispatch.py:175-176; live as of REC-124 Phase 1 (registry.py:59)"
    uncertainties: ""

  - id: modality_audio_t2a
    label: "audio_t2a (TTS via ElevenLabs) modality"
    phase_group: infra
    node_type: generation
    tool: "AudioRunner (registry MODALITY_AUDIO_T2A); execution/providers/elevenlabs.py"
    inputs: [{ artifact: "audio payload (text, voice_id, model)", from: dispatch_entry_point }]
    produces: ["RunResult (output_path mp3)"]
    gate: { validator: "provider adapter typed errors (AudioSynthesisError)", pass_condition: "synthesis succeeds; fail-fast 401/402/422/429" }
    loops: []
    status: live
    evidence: "dispatch.py:177-178 (CP-8 promoted from CP-4 stub); recoil/CLAUDE.md:368-433. registry.py:26 docstring stale (STUB)"
    uncertainties: "needs ELEVENLABS_API_KEY."

  - id: modality_lipsync_post
    label: "lipsync_post (sync.so post-process) modality"
    phase_group: infra
    node_type: generation
    tool: "LipSyncPostProcessor (registry MODALITY_LIPSYNC_POST); execution/providers/sync_so.py"
    inputs: [{ artifact: "lipsync payload (video_path, audio_path, model)", from: dispatch_entry_point }]
    produces: ["RunResult (lipsynced mp4)"]
    gate: { validator: "provider adapter typed errors (LipSyncError)", pass_condition: "lipsync job succeeds" }
    loops: []
    status: live
    evidence: "dispatch.py:179-180 (CP-8 promoted); recoil/CLAUDE.md:388. registry.py:27 docstring stale (STUB)"
    uncertainties: "needs SYNC_SO_API_KEY."

  - id: modality_eval
    label: "eval_image_v1 / eval_video_v1 / eval_audio_v1 modalities"
    phase_group: infra
    node_type: gate
    tool: "Eval*Runner wrapping Gemini 3.1 Pro (register_default_eval_runners, dispatch.py:407)"
    inputs: [{ artifact: "artifact path + rubric", from: eval_panel }]
    produces: ["EvalResult / ScoreCard"]
    gate: { validator: "GEMINI_API_KEY presence at EvalNode layer", pass_condition: "key set + artifact scorable" }
    loops: []
    status: substrate_only
    evidence: "dispatch.py:407-448 OPT-IN, NOT called by dispatch()/register_default_runners (:401-406); ssot_manifest.yaml:169-170 'opt-in, not auto-bootstrapped'"
    uncertainties: "registered in manifest list but not auto-bootstrapped; needs explicit register_default_eval_runners()."

  - id: take_store
    label: "Take/Beat/Scene editorial model — reroll = new Take"
    phase_group: infra
    node_type: store
    tool: "recoil/pipeline/core/take.py (Take :51, Beat :187, Scene :499)"
    inputs:
      - { artifact: "Workflow (one per Take)", from: workflow_step }
      - { artifact: "GenerationReceipt per WorkflowStep", from: dispatch_entry_point }
    produces: ["Beat.takes[] (append-only)", "primary_take_id", "derived Beat/Scene status"]
    gate:
      validator: "Beat.is_exhausted (take.py:215) — max_takes(3) + phantom_recovery slots"
      pass_condition: "a take.status==succeeded selectable as primary, or exhausted"
    loops:
      - { kind: reroll, to: take_store, note: "re-attempt = Beat.new_take(workflow=fresh) appended, NOT mutation (take.py:430)" }
    status: live
    evidence: "take.py; EpisodeRunner live consumer (ssot_manifest:108-110); episode_runner.py:357,:188,:430"
    uncertainties: "in-memory model; persistence by EpisodeRunner scene JSON (pipeline/core/persistence.py)."

  - id: select_primary_decision
    label: "Beat.select_primary — primary take strategy"
    phase_group: infra
    node_type: decision
    tool: "Beat.select_primary (take.py:685,:765)"
    inputs: [{ artifact: "Beat.takes[] with statuses/aggregate_scores", from: take_store }]
    produces: ["Beat.primary_take_id"]
    gate: { validator: "strategy dispatch (take.py:709)", pass_condition: "first_success: first succeeded; score: highest aggregate; manual: caller-set" }
    loops: []
    status: live
    evidence: "first_success (default :744) + manual LIVE; 'score' implemented (:709) but depends on eval_scores production never populates"
    uncertainties: "'score' branch callable but inert in production (uses first_success)."

  - id: eval_panel
    label: "PanelOfJudges — multi-judge score-based eval"
    phase_group: infra
    node_type: gate
    tool: "recoil/pipeline/core/eval.py::PanelOfJudges; attach_eval_hooks; Take.compute_aggregate_score (take.py:126)"
    inputs: [{ artifact: "GenerationReceipt artifact path + rubric", from: take_store }]
    produces: ["ScoreCard -> receipt.eval_scores[panel_id]", "receipt.provenance['eval_cost_usd']", "Take.aggregate_score"]
    gate:
      validator: "PanelOfJudges aggregation (median default) + cost_cap_usd (eval.py:67-72)"
      pass_condition: "panel_score computed; cost_cap aborts mid-panel"
    loops:
      - { kind: reroll, to: select_primary_decision, note: "score feeds Beat.select_primary('score')" }
    status: substrate_only
    evidence: "eval.py + take.py:126; ssot_manifest.yaml:147-149 'EpisodeRunner does not attach in production, receipts persist eval_scores as {}'; no PanelOfJudges/attach_eval_hooks in production_loop.py/episode_runner.py (grep empty)"
    uncertainties: "production wire-up GATED on JT review (recoil/CLAUDE.md:498); tournament/CostGate/continuity-critic deferred."

  - id: strategy_score_bridge
    label: "from_score_card — score -> FailureMode retry bridge"
    phase_group: infra
    node_type: decision
    tool: "recoil/pipeline/orchestrator/strategy_registry.py::from_score_card (:1568)"
    inputs: [{ artifact: "PanelOfJudges ScoreCard", from: eval_panel }]
    produces: ["(FailureMode, float)"]
    gate: { validator: "mirrors detect_failure_mode return shape", pass_condition: "n/a" }
    loops: []
    status: substrate_only
    evidence: "strategy_registry.py:1540 'substrate only'; :1551 'does NOT modify production_loop.py' (byte-untouched)"
    uncertainties: "switchover gated on JT sign-off."

  - id: strategy_engine_live
    label: "StrategyEngine — LIVE retry overlay (production reroll brain)"
    phase_group: infra
    node_type: decision
    tool: "recoil/pipeline/orchestrator/strategy_registry.py::StrategyEngine"
    inputs: [{ artifact: "failed Take classified by detect_failure_mode()", from: take_store }]
    produces: ["StrategyDiff overlaid onto next Workflow before Take.execute()"]
    gate: { validator: "detect_failure_mode (pipeline/core/failure_mode.py)", pass_condition: "failure classified to a strategy" }
    loops:
      - { kind: reroll, to: take_store, note: "overlay seed/tier via provider_hints onto next take" }
    status: live
    evidence: "ssot_manifest.yaml:233 active_with_known_wire_gaps — LIVE retry path, distinct from substrate score bridge"
    uncertainties: "many StrategyDiff fields unwired; only seed+tier reach the runner."

  - id: provenance_index
    label: "Provenance / freshness — structural vs content fingerprint"
    phase_group: infra
    node_type: store
    tool: "recoil/pipeline/_lib/derivation_sha.py (plan_structural_sha :57, content_sha :70, shotset_hash :79)"
    inputs:
      - { artifact: "EpisodePlan / ShotRecord dicts", from: plan_pass }
      - { artifact: "GenerationReceipt provenance", from: dispatch_entry_point }
    produces: ["structural_sha (partition fields)", "content_sha (full bytes)", "freshness verdict"]
    gate:
      validator: "structural_sha equality (derivation_sha.py:57)"
      pass_condition: "structural_sha unchanged => locked downstream survive; content_sha change alone does NOT invalidate"
    loops:
      - { kind: director_note_route, to: take_store, note: "structural re-derive invalidates locked downstream; fingerprint drift demotes succeeded takes" }
    status: live
    evidence: "derivation_sha.py:32-48 (structural slice incl camera_side, screen_direction, axis_segment_id, cut_relation); DIRECTOR_NOTES_PLAYBOOK.md:85"
    uncertainties: "GROUPING_PARAM_VERSION=1; D6 sublocation_id forward-compat (:51)."

  - id: board_story_gate
    label: "Story gate — board verdict router (script/board/prompt)"
    phase_group: infra
    node_type: gate
    tool: "recoil/pipeline/_lib/story_gate.py (ROUTES :34); Beat.set_board_story_gate (take.py:266)"
    inputs: [{ artifact: "storyboard PNG + scene context", from: modality_storyboard }]
    produces: ["verdict sidecar prep/ep_NNN/storyboards/<board>.verdict.json", "routing.class label"]
    gate:
      validator: "story_gate panel/transition forced checks (story_gate.py:399)"
      pass_condition: "class==ok; else board_problem|script_problem|prompt_problem|mixed|judge_unavailable"
    loops:
      - { kind: director_note_route, to: script_lock, note: "script_problem -> patch ep_NNN.md" }
      - { kind: director_note_route, to: provenance_index, note: "board_problem -> blocking; prompt_problem -> prompt encode" }
    status: live
    evidence: "story_gate.py:34-40; DIRECTOR_NOTES_PLAYBOOK.md:20-28; take.py:288-300"
    uncertainties: "JT framing overrides gate (PLAYBOOK:26) via --reject-board --reason."

  - id: dispatch_chassis_orchestrator
    label: "Self-healing overnight build chassis — harness_orchestrator.sh"
    phase_group: infra
    node_type: decision
    tool: "recoil/pipeline/tools/harness_orchestrator.sh (v2)"
    inputs: [{ artifact: "BUILD_SPEC.md (phases, validation gates)", from: external }]
    produces: ["per-phase commits", "build-log-*.md", "pushed branch + auto-PR + optional auto-merge"]
    gate:
      validator: "codex spec-review gate (:719) + dispatch-audit gate (:682) + per-phase validation + end-of-build convergence (:1747)"
      pass_condition: "fail-CLOSED: only explicit VERDICT: READY/CONVERGED + deterministic gate guards mark green"
    loops:
      - { kind: reroll, to: dispatch_chassis_orchestrator, note: "per-phase retry up to MAX_RETRIES=3, escalating codex->claude at attempt>=2; IN_PROGRESS phases re-validated/resumed on crash" }
    status: live
    evidence: "harness_orchestrator.sh:1-2191; self-snapshot :16-26; resumable :1235,:1257; frozen-gate :1769; auto-PR :1963; auto-merge :2034"
    uncertainties: "EP001 runtime spend-gates NOT in this substrate (banner :1151) — SAFE for code/tooling builds only, NOT $-spend visual loops."

  - id: dispatch_status_ledger
    label: "status.json SSOT + round/budget ledger"
    phase_group: infra
    node_type: store
    tool: "recoil/pipeline/tools/dispatch_status.py"
    inputs: [{ artifact: "run_dir, attempt, phase, budget rounds", from: dispatch_chassis_orchestrator }]
    produces: ["status.json (locked atomic)", "terminal_status.json per attempt", "heartbeat.json", "failure_signature"]
    gate:
      validator: "budget-check (dispatch_status.py; harness :1026)"
      pass_condition: "codex_rounds_used < max(18); wall_clock < 21600s; concurrent_runs < 4"
    loops: []
    status: live
    evidence: "dispatch_status.py:1-60; CONCURRENT_RUNS_MAX=4 (2026-06-07); harness terminal_status :821, heartbeat :981, budget :1026"
    uncertainties: "stdlib-only; status.json lock via /usr/bin/shlock."

  - id: dispatch_reaper
    label: "Reaper — reclaim stranded claims + stale worktrees"
    phase_group: infra
    node_type: decision
    tool: "recoil/pipeline/tools/autonomy/reaper.py"
    inputs: [{ artifact: "claim_ledger + lease state + worktree TTL", from: session_workspace }]
    produces: ["reclaimed claims (reaper:stranded)", "removed stale worktrees"]
    gate: { validator: "_pid_is_dead + lease TTL check (reaper.py)", pass_condition: "PID dead OR lease expired beyond TTL" }
    loops: []
    status: live
    evidence: "reaper.py:1-50; DEFAULT_BLOCK_AFTER_FAILURES=3, FAILURE_SIGNATURE=reaper:stranded"
    uncertainties: "DEFAULT_WORKTREE_TTL_HOURS=0 (opt-in via --ttl-hours)."

  - id: session_workspace
    label: "session_workspace.sh — per-session worktree + lease (push-to-claim)"
    phase_group: infra
    node_type: store
    tool: "recoil/pipeline/tools/session_workspace.sh"
    inputs: [{ artifact: "actor + issue REC-NN + slug", from: external }]
    produces: ["external worktree (~/Code/recoil-sessions)", "branch-on-origin lease (CAS claim)", "PR handoff metadata"]
    gate:
      validator: "assert_owned (paths under SESSIONS_ROOT) + origin-reachable lease push"
      pass_condition: "branch push to origin succeeds (atomic cross-host claim); origin-unreachable fails CLOSED"
    loops: []
    status: live
    evidence: "session_workspace.sh:1-40; worktrees OUTSIDE repo; lease=branch-on-origin; dirty close = WIP-commit-and-push"
    uncertainties: ""

edges:
  # --- narrative (from narrative.yaml) ---
  - { from: develop, to: validate, label: "5 dev docs + 34/34" }
  - { from: validate, to: promote, label: "READY (hard gates + score>=3.5)" }
  - { from: validate, to: develop, label: "NOT READY -> fill gaps" }
  - { from: promote, to: pre_treatment_gate, label: "bible/ + state created" }
  - { from: pre_treatment_gate, to: treatment, label: "exit 0" }
  - { from: pre_treatment_gate, to: promote, label: "fail -> promote first" }
  - { from: treatment, to: generate_script, label: "treatment.md MASTER" }
  - { from: generate_script, to: episode_metrics_gate, label: "ep_XXX.md written" }
  - { from: episode_metrics_gate, to: generate_script, label: "is_valid:false -> fix (max 3)" }
  - { from: episode_metrics_gate, to: batch_checkpoint_gate, label: "5 eps valid -> checkpoint" }
  - { from: batch_checkpoint_gate, to: generate_script, label: "PASS unlock / FAIL regen" }
  - { from: batch_checkpoint_gate, to: finish, label: "batch 12 complete" }
  - { from: finish, to: finish_script_doctor, label: "preflight OK" }
  - { from: finish_script_doctor, to: finish_extract_annotations, label: "brief.json" }
  - { from: finish_extract_annotations, to: finish_apply_revisions, label: "annotations.json" }
  - { from: finish_apply_revisions, to: finish_revalidate, label: "modified episodes" }
  - { from: finish_revalidate, to: finish_verify, label: "re-validated" }
  - { from: finish_verify, to: finish_apply_revisions, label: "issues remain -> re-revise (loop)" }
  - { from: finish_verify, to: finish_compile, label: "informational pass-through" }
  - { from: finish_compile, to: script_lock, label: "Fountain locked -> visual pipeline (Camera Test)" }
  # --- prepro (from prepro.yaml; casting_refs->casting unified) ---
  - { from: script_lock, to: camera_test, label: "locked ep_NNN.md -> shot boundaries" }
  - { from: camera_test, to: global_bible, label: "all camera_tested/*.json (batch)" }
  - { from: script_lock, to: global_bible, label: "characters.md DNA" }
  - { from: global_bible, to: enrichment, label: "[OPUS_ENRICHMENT] placeholders" }
  - { from: enrichment, to: plan_pass, label: "enriched bible (episode summary)" }
  - { from: camera_test, to: plan_pass, label: "camera_tested/ep_NNN.json (1:1 shots)" }
  - { from: plan_pass, to: blocking_pass, label: "ep_NNN_plan.json (required)" }
  - { from: script_lock, to: blocking_pass, label: "scene source text" }
  - { from: enrichment, to: blocking_pass, label: "bible" }
  - { from: global_bible, to: casting, label: "visual_description + brief_visual/wardrobe + look_map" }
  - { from: global_bible, to: wardrobe_intent_gate, label: "phases + wardrobe_philosophy" }
  - { from: script_lock, to: wardrobe_intent_gate, label: "treatment.md + series_bible.md" }
  - { from: wardrobe_intent_gate, to: casting, label: "approved wardrobe phase descriptions (-> Screen Test)" }
  - { from: global_bible, to: location_refs, label: "locations.<id>.description/atmosphere" }
  - { from: plan_pass, to: previz, label: "shot plan + spatial/axis plan" }
  - { from: casting, to: keyframes, label: "character hero (blueprint, 1-char scenes)" }
  - { from: location_refs, to: keyframes, label: "location hero / moodboard" }
  - { from: global_bible, to: keyframes, label: "bible + sublocation refs" }
  - { from: blocking_pass, to: keyframes, label: "blocking_metadata (staging) -> board" }
  # --- production (from production.yaml) ---
  - { from: previz, to: previz_review, label: "previz assets" }
  - { from: previz_review, to: previz, label: "previs_rejected -> reroll" }
  - { from: plan_pass, to: keyframes, label: "shot skeleton -> board gen (live; previz bypassed)" }
  - { from: keyframes, to: keyframes, label: "auto-reroll (new board version)" }
  - { from: keyframes, to: board_decision, label: "proposed board" }
  - { from: board_decision, to: keyframes, label: "reject / stale-sha -> regenerate" }
  - { from: board_decision, to: photoreal_finish, label: "approved + fresh sha" }
  - { from: photoreal_finish, to: video_gen, label: "approved board ref" }
  - { from: video_gen, to: video_gen, label: "reroll = NEW take / strategy retry" }
  - { from: dispatch_audit, to: video_gen, label: "payload-shape guardrail (build-time)" }
  - { from: video_gen, to: dailies_review, label: "rendered takes" }
  - { from: dailies_review, to: video_gen, label: "reject -> regenerate (new take)" }
  - { from: dailies_review, to: final_review_export, label: "approved" }
  - { from: final_review_export, to: video_gen, label: "final reject -> re-render pass" }
  # --- infra (from infra.yaml) ---
  - { from: workflow_step, to: dispatch_entry_point, label: "WorkflowStep.modality -> dispatch()" }
  - { from: dispatch_entry_point, to: modality_registry, label: "get_runner(modality)" }
  - { from: modality_registry, to: modality_image_t2i, label: "image_t2i" }
  - { from: modality_registry, to: modality_video_i2v, label: "video_i2v" }
  - { from: modality_registry, to: modality_r2v_multi, label: "r2v_multi" }
  - { from: modality_registry, to: modality_storyboard, label: "storyboard" }
  - { from: modality_registry, to: modality_audio_t2a, label: "audio_t2a" }
  - { from: modality_registry, to: modality_lipsync_post, label: "lipsync_post" }
  - { from: dispatch_entry_point, to: provenance_index, label: "receipt provenance/receipts.jsonl" }
  - { from: take_store, to: dispatch_entry_point, label: "Take.execute -> Workflow.run -> dispatch() per step" }
  - { from: take_store, to: select_primary_decision, label: "Beat.select_primary over takes" }
  - { from: take_store, to: take_store, label: "reroll = Beat.new_take (append-only)" }
  - { from: strategy_engine_live, to: take_store, label: "LIVE: StrategyDiff overlay -> next Take" }
  - { from: eval_panel, to: select_primary_decision, label: "SUBSTRATE: score -> select_primary('score')" }
  - { from: eval_panel, to: strategy_score_bridge, label: "SUBSTRATE: ScoreCard -> from_score_card" }
  - { from: provenance_index, to: take_store, label: "structural re-derive invalidates locked downstream" }
  - { from: modality_storyboard, to: board_story_gate, label: "board -> story gate verdict" }
  - { from: board_story_gate, to: script_lock, label: "script_problem route" }
  - { from: board_story_gate, to: provenance_index, label: "board/prompt_problem -> blocking/prompt layer" }
  - { from: dispatch_chassis_orchestrator, to: dispatch_status_ledger, label: "status/budget/terminal_status" }
  - { from: session_workspace, to: dispatch_reaper, label: "claim ledger + lease -> reaper reconcile" }
  - { from: dispatch_chassis_orchestrator, to: session_workspace, label: "auto-PR carries handoff metadata" }
  # --- stitch edges (rule 4; segment realization) ---
  - { from: keyframes, to: modality_storyboard, label: "board gen dispatches storyboard modality" }
  - { from: video_gen, to: take_store, label: "realized via Take/Beat (reroll = new take)" }
  - { from: video_gen, to: dispatch_entry_point, label: "each step -> dispatch()" }
  - { from: plan_pass, to: provenance_index, label: "EpisodePlan structural_sha fingerprint" }
  # --- cinema_mode config fan-out (rule: node added post-merge) ---
  - { from: cinema_mode, to: previz, label: "style/camera tokens (at prompt build)" }
  - { from: cinema_mode, to: keyframes, label: "cinema tokens" }
  - { from: cinema_mode, to: video_gen, label: "camera/style tokens" }

director_note_routes:
  - { note_type: "'X should be elsewhere' / set-geography canon (e.g. pod on a platform)", routes_to: "bible_sublocation + script + blocking", evidence: "DIRECTOR_NOTES_PLAYBOOK.md:31-47 chain A; :11-18" }
  - { note_type: "'no reason for him to do that' / causality", routes_to: "script (ep_NNN.md — articulate the CAUSE)", evidence: "PLAYBOOK:49-55 chain B" }
  - { note_type: "'those two shots don't connect' / continuity jump", routes_to: "script (bridge beat) + batch_shots transition key", evidence: "PLAYBOOK:56-64 chain C" }
  - { note_type: "'not doing what the shot says' / under-animation", routes_to: "motion_line (raw.prompt_data.prompt_skeleton.motion_line, REC-139)", evidence: "PLAYBOOK:66-69 chain D; :16" }
  - { note_type: "per-shot staging (gaze, stance, facing)", routes_to: "blocking (scene beat_metadata.batch_shots[].raw.blocking_metadata)", evidence: "PLAYBOOK:15; chain A step 4 :44-47" }
  - { note_type: "'wrong face'/'too smooth'/artifacts / identity-technical", routes_to: "existing machinery (identity refs/pools, cinema modes, reroll-with-strategy) — NOT a script/bible/blocking patch", evidence: "PLAYBOOK:70-73 chain E" }
  - { note_type: "gate-classified board verdict (story_gate routing.class)", routes_to: "script_problem->script | board_problem->blocking/board | prompt_problem->prompt encode", evidence: "story_gate.py:34-40; PLAYBOOK:20-28" }
