"""파이프라인 Step Manifest v4 + shot-more — 49단계 정의, 의존성, 설정.

이 파일이 파이프라인의 단일 진실 소스(Single Source of Truth).
v4 + shot-more: beat→shot 기반 파이프라인.

정확한 실측 baseline은 `docs/architecture/_step_manifest.generated.md` 참조
(재생성 — 저장소 루트에서:
 `backend/.venv/bin/python backend/scripts/dump_step_manifest.py > docs/architecture/_step_manifest.generated.md`).

구성 (총 50, 2026-04-27 기준)
  - analysis: 42 (always 34, if_planning_doc 1, on_demand 4, disabled 3)
  - image:     6 (always 4, if_has_outlooks 1, on_demand 1)
  - auxiliary: 2 (on_demand 1, disabled 1)

Active (분석 실행 경로, 40): always 38 + if_planning_doc 1 + if_has_outlooks 1

applicability 종류
  - always: 항상 실행
  - disabled: 실험용/비활성
  - on_demand: 사용자 요청 시만 (project_summary 등)
  - if_planning_doc: 기획서 있을 때만
  - if_has_outlooks: 아웃룩 있을 때만

Phase 1 추가 필드 (01-principles-revised.md §4, §5)
  - step_type (실측 3종, 2026-04-22):
      transform (41): 체크포인트 → 체크포인트 (DB 쓰기 금지)
      asset (6): 파일 + DB 메타 (이미지 계열)
      editorial (2): 타 step 체크포인트 수정 (t2i_review, scene_verify)
    (설계 초안의 `projection` 타입은 별도 step_type으로 분리되지 않고
     Phase 2의 CheckpointSyncService 5-way 분해로 흡수됨. step은 여전히
     transform/asset으로 남고 projection은 sync 레이어 책임.)
  - lifecycle: active | deprecated | removed (실측: active 42, deprecated 6, removed 1)
  - resume_sensitive: resume 모드에서도 재실행 필요 (bool, 기본 False)
  - modifies_checkpoints: editorial만 사용. 자신이 덮어쓰는 타 step 체크포인트 목록
  - replaced_by: deprecated만 사용. 대체된 step_id
  - allow_partial_downstream: 본 step 의 ``partial`` 결과가 downstream 으로
      cascade 되어도 안전한지 (bool, 기본 True). False 시 downstream
      cascade stop — analysis_dispatch_service 가 batch 중단, check_gate 가
      blocked. partial 시 incomplete 데이터로 후속 step 진행하면 잘못된 결과
      (asset/entity 누락) 가 발생하는 step 에 명시 False. (problems.md #11 contract)

default_model 컨벤션 (problems.md #5)
  - **반드시 LiteLLM Router 에 등록된 alias** (예: ``"gpt"``, ``"gpt-mini"``,
    ``"gemini-pro"``, ``"gemini-flash"``, ``"gemini-lite"``, ``"gemini-image"``,
    ``"fal-ai"``) 만 사용. 실 model ID (예: ``"gemini-3.1-flash-lite-preview"``)
    는 사용 금지 — alias 가 router 에서 model ID 로 해소되며, alias 통일이
    UI/admin/runtime parity 의 근거.
  - ``llm_client.PIPELINE_STEPS`` 가 본 manifest 의 default_model 을 derived view
    로 build (#5: STEP_MANIFEST 가 single source of truth).

참조: docs/architecture-refactor-final/02-final-roadmap.md §Phase 1
"""

from typing import Any, Dict, List, Optional

STEP_STATUSES = [
    "pending", "blocked", "runnable", "running", "completed",
    "partial", "failed", "not_applicable", "stale", "cancelled",
]


# Block B B5 (resume architecture, plan v2.1.3 / spec V5 §4.5):
# schema_version mismatch (cp_mismatch) 자동 RERUN_SELF 허용 step 한정 allowlist.
# config_hash mismatch 는 어떤 step 도 허용 X (allowlist 미적용).
#
# entity_t2i 만 허용 사유: 다른 step (scene_detail / shot_dependency 등) 은
# rerun_self 시 downstream (shot_dependency_t2i / scene_image_pipeline 등) 보존
# → 새 결과와 기존 downstream 사이 contract drift 위험.
#
# expiry: D3 (manifest.allow_auto_rerun_on_schema_bump flag) 도입 후 제거 예정.
_LEGACY_SCHEMA_BUMP_ALLOWLIST: frozenset = frozenset({"entity_t2i"})

STEP_MANIFEST: Dict[str, Dict[str, Any]] = {
    # ── 분석 Phase ──

    # 0. 기획서 분석 (프로젝트 레벨, 선택적)
    "planning_doc_analysis": {
        "label": "기획서 분석",
        "category": "analysis",
        "order": 0,
        # alias 통일 (problems.md #5) — 다른 step 들과 동일한 alias 표기.
        "default_model": "gemini-lite",
        "provider": "gemini",
        "depends_on": [],
        "fan_out": False,
        "applicability": "if_planning_doc",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 1. PDF 텍스트 정리 (LLM regex 규칙)
    "text_cleanup": {
        "label": "텍스트 정리",
        "category": "analysis",
        "order": 1,
        "default_model": "gemini-lite",
        "provider": "gemini",
        "depends_on": [],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 2. 씬 세그먼테이션 (헤딩 기반)
    "scene_segmentation": {
        "label": "씬 세그먼테이션",
        "category": "analysis",
        "order": 2,
        "default_model": "gemini-flash",
        "provider": "gemini",
        "depends_on": ["text_cleanup"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
        # ★2 = 저작 규칙 분해(2026-08-08). 1 은 후보 하나에 평균 길이 검증
        #  하나였고, 그 방식은 "개수는 맞는데 경계가 틀린" 규칙을 그대로
        #  통과시켰다(실측 4건). 올려 두지 않으면 옛 체크포인트가 조용히
        #  그대로 쓰여, 코드를 바꿨는데 옛 경계가 하류로 계속 내려간다.
        "schema_version": 2,
    },
    # 3. 에피소드 요약
    "episode_summary": {
        "label": "에피소드 요약",
        "category": "analysis",
        "order": 3,
        "default_model": "gpt-mini",
        "provider": "gemini",
        "depends_on": [],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 4. 시각적 세계관 규칙
    "visual_world_rules": {
        "label": "시각적 세계관 규칙",
        "category": "analysis",
        "order": 4,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["episode_summary"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
        "schema_version": 1,  # 2026-05-19 (Area #7a W1): rules_schema.json v7 — rule_type 10-enum strict (possession/transformation/ghost/projection/superpower/body_deformation/time_period/costume/technology/other) + director_notes description abstraction. cp invalidation: incompatible-stamp cp blocked via _check_cp_mismatch; older unstamped/default-stamped cp may need force rerun.
    },
    # 5. 씬 재분할 (v4에서 비활성화 — beat/shot으로 대체)
    "scene_split": {
        "label": "큰 씬 분할 (레거시)",
        "category": "analysis",
        "order": 5,
        "default_model": "gemini-flash",
        "provider": "gemini",
        "depends_on": ["scene_segmentation"],
        "fan_out": True,
        "applicability": "on_demand",
        "step_type": "transform",
        "lifecycle": "deprecated",
        "replaced_by": "beat_extract,shot_extract",
    },
    # 6. 씬 저장 (DB + JSON)
    "scene_save": {
        "label": "씬 저장",
        "category": "analysis",
        "order": 6,
        "default_model": "-",
        "provider": "-",
        "depends_on": ["scene_segmentation"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 6.5 인물 리스트 사전 추출 (beat/shot에서 참조)
    # 2026-07-11 모델 비교 4회차 전수 판정(Claude+Codex 교차 합의): 캐릭터 후보
    # 회수율은 GPT-5.6 Sol 우세(단역·동물·상태변형 승격) — 추출 계열만 Sol 재이관.
    # 확정 계열(beat/shot/director/detail/consistency)은 Gemini Pro 유지.
    "entity_character_list": {
        "label": "인물 리스트 (사전 추출)",
        "category": "analysis",
        "order": 6.5,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["scene_save", "visual_world_rules"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 7. 씬별 요약
    "scene_summary": {
        "label": "씬별 요약 (병렬)",
        "category": "analysis",
        "order": 7,
        "default_model": "gpt-mini",
        "provider": "gemini",
        "depends_on": ["scene_save", "episode_summary", "visual_world_rules"],
        "fan_out": True,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 7.1 Beat 추출 (병렬)
    "beat_extract": {
        "label": "Beat 추출 (병렬)",
        "category": "analysis",
        "order": 7.1,
        "default_model": "gemini-pro",
        "provider": "gemini",
        "depends_on": ["scene_save", "visual_world_rules", "entity_character_list"],
        "fan_out": True,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 7.2 Shot 추출 (순차 — 이전 shot 결과 전달)
    "shot_extract": {
        "label": "Shot 추출 (순차)",
        "category": "analysis",
        "order": 7.2,
        "default_model": "gemini-pro",
        "provider": "gemini",
        "depends_on": ["beat_extract", "visual_world_rules", "entity_character_list"],
        "fan_out": True,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 7.25 Shot 검증 — '한 찰나' 원칙 재작성 (shot_extract 이후 다운스트림 입력)
    "shot_validator": {
        "label": "Shot 검증",
        "category": "analysis",
        "order": 7.25,
        "default_model": "gemini-pro",
        "provider": "gemini",
        "depends_on": ["shot_extract"],
        "fan_out": True,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
        # G4.6 Phase 3 — partial 결과 (validator_status='failed' 씬 포함) 를
        # 다운스트림 silent carry 차단. operator override = project_config
        # ['allow_failed_validator']=True 설정 시 step_runner.check_gate 가 통과
        # 허용 + 각 consumer 의 assert_no_failed_scenes 가 동일 키 인식하여
        # second line of defense 로 작동 (force / resume / 비표준 dispatch 안전망).
        "allow_partial_downstream": False,
        "partial_override_config_key": "allow_failed_validator",
    },
    # 8a. 인물 리스팅 (shot 기반)
    # 2026-07-11 캐릭터 추출 계열 Sol 재이관 (entity_character_list 주석 참조)
    "entity_all_character": {
        "label": "인물 리스팅",
        "category": "analysis",
        "order": 8,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["scene_save", "visual_world_rules", "shot_validator"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 8b. 인물 추출 (상세)
    "entity_extract_character": {
        "label": "인물 추출",
        "category": "analysis",
        "order": 9,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["entity_all_character"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 9a. 배경 리스팅 (shot 기반 1회 호출)
    "entity_all_location": {
        "label": "배경 리스팅",
        "category": "analysis",
        "order": 10,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["scene_save", "visual_world_rules", "shot_validator"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 9b. 배경 추출 (상세)
    "entity_extract_location": {
        "label": "배경 추출",
        "category": "analysis",
        "order": 11,
        "default_model": "gemini-pro",
        "provider": "gemini",
        "depends_on": ["entity_all_location"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 10a. 소품 리스팅 (shot 기반 1회 호출)
    "entity_all_prop": {
        "label": "소품 리스팅",
        "category": "analysis",
        "order": 12,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["scene_save", "visual_world_rules", "shot_validator"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 10b. 소품 추출 (상세)
    "entity_extract_prop": {
        "label": "소품 추출",
        "category": "analysis",
        "order": 13,
        "default_model": "gemini-pro",
        "provider": "gemini",
        "depends_on": ["entity_all_prop"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 13.6 요소 변형 관계 추출 (이름 기반 후보 + LLM 확인)
    "entity_relation": {
        "label": "요소 변형 관계 추출",
        "category": "analysis",
        "order": 13.6,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["entity_merge"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    "entity_filter": {
        "label": "저빈도 요소 필터링",
        "category": "analysis",
        "order": 13.7,
        "default_model": "gpt-mini",
        "provider": "gemini",
        "depends_on": ["entity_relation"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 13.5 요소 중복 병합 (타입 간 중복/유사 요소 판별)
    "entity_merge": {
        "label": "요소 중복 병합",
        "category": "analysis",
        "order": 13.5,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["entity_extract_character", "entity_extract_location", "entity_extract_prop", "scene_summary", "visual_world_rules"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 14. 요소 상세 + enum 확정
    "entity_detail": {
        "label": "요소 상세 + enum",
        "category": "analysis",
        "order": 14,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["entity_filter"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 15. T2I 프롬프트 (병렬)
    # default_model = gemini-pro: entity_t2i step 이 t2i_prompt 외에
    # description / visual_traits 까지 재생성하는 구조 (entity_steps.py 의
    # ENTITY_DETAIL_SCHEMA 호출) 라, 보조 모델이 의미 layer 에서 unsourced
    # trait hallucination 을 만들던 결함 차단. CLAUDE.md 정책: 의미 생성은
    # gpt-5.5 / gemini-pro 만. 추가로 description / visual_traits 자체는
    # entity_detail 결과를 forward 하여 LLM 의 unsourced trait 이 final
    # output 에 반영되지 않도록 막음 (entity_steps.py 참조).
    #
    # schema_version 3 (D6 T2-fix, review iter3 B1): location entity 의 metadata_json
    # (location.space_profile) 을 cp 에 포함 + post-validation. 기존 v2 cp 는
    # metadata_json 부재라 D6 contract 미충족 → stale 처리 + RERUN_SELF cascade.
    # `_LEGACY_SCHEMA_BUMP_ALLOWLIST` (entity_t2i 만) 안이라 자동 재실행 path.
    #
    # schema_version 2: source-forward 정책 + short_description 제거 (Codex iter 1
    # 후속). step_runner._check_cp_mismatch 가 cp.schema_version 을 manifest 의
    # current_schema 와 직접 비교 → schema_version 이 기록된 (cp_schema=1) 기존
    # checkpoint resume 가 stale 처리되며 force 재실행 cascade trigger. 단, schema_version
    # 필드 자체가 없는 legacy no-schema checkpoint 는 호환 위해 invalidation 대상
    # 아님 — legacy 도 강제 재실행이 필요하면 별도 force 절차 사용.
    "entity_t2i": {
        "label": "요소 T2I 프롬프트 (병렬)",
        "category": "analysis",
        "order": 15,
        "default_model": "gemini-pro",
        "provider": "gemini",
        "depends_on": ["entity_detail"],
        "fan_out": True,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
        "schema_version": 3,
    },
    # 15.5 중요 샷 선택
    "shot_selection": {
        "label": "중요 샷 선택 (병렬)",
        "category": "analysis",
        "order": 15.5,
        "default_model": "gpt-mini",
        "provider": "gemini",
        "depends_on": ["shot_validator"],
        "fan_out": True,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 16. 씬 감독 (물리적 존재 판정)
    "scene_director": {
        "label": "씬 감독 (물리적 존재)",
        "category": "analysis",
        "order": 16,
        "default_model": "gemini-pro",
        "provider": "gemini",
        "depends_on": ["scene_save", "entity_t2i"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
        "requires_projection_sync_before_run": True,  # W1-F12: scene_director enum이 DB short_id에 의존
    },
    # 16.5 Shot별 VE 판정 + 변형 확정
    "shot_director": {
        "label": "Shot별 VE 판정",
        "category": "analysis",
        "order": 16.5,
        "default_model": "gpt",
        "provider": "openai",
        # schema_version: shot_director_step.SHOT_DIRECTOR_SCHEMA_VERSION 와
        # 일치 필수. cp(schema=N) 와 manifest(schema=N) 일치해야 step_runner
        # P0-3 mismatch 검증 통과.
        # 2026-05-10 SOT drift fix:
        #   1→2 (off-camera/gaze deterministic 제외 + audit field
        #        excluded_offscreen_entity_ids + prompt v3 → v4 OOS 작품 고유
        #        명사 → synthetic placeholder).
        # 2026-05-13 frame-visible SOT unification:
        #   2→3 (deterministic _resolve_scene_no_variant 영구 폐기 + 항상 LLM
        #        path + prompt v5 (5.202605131800) L/P frame-visible 명시
        #        강화). v2 cp 는 L/P scene-present over-inclusion 상태라
        #        Area B render_contracts 충돌 발생 — schema bump 로 invalidate.
        #   shot_director 는 _LEGACY_SCHEMA_BUMP_ALLOWLIST 외 →
        #   step_runner._check_cp_mismatch 가 BLOCK. 운영자 명시 force 의무.
        "schema_version": 3,
        "depends_on": ["scene_director", "shot_selection", "entity_relation", "shot_validator"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 17. 촬영 감독 (레거시 — shot_cinematography로 대체)
    "scene_cinematography": {
        "label": "촬영 감독 (레거시)",
        "category": "analysis",
        "order": 17,
        "default_model": "gemini-pro",
        "provider": "gemini",
        "depends_on": ["scene_director"],
        "fan_out": False,
        "applicability": "on_demand",
        "step_type": "transform",
        "lifecycle": "deprecated",
        "replaced_by": "shot_staging",
    },
    # 17.05 씬 단위 카메라 연속 흐름 (shot_staging 이전)
    "scene_camera_flow": {
        "label": "씬 카메라 플로우",
        "category": "analysis",
        "order": 17.05,
        # gemini-pro가 일부 씬(예: 옥탑방 폭력 함축 콘텐츠)에 빈 응답을 반환해 partial로 끝나는
        # 사례 발생 (S25). gpt-5.5는 동일 입력에 정상 응답. 안정성 위해 gpt로 변경 (2026-04-27).
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["shot_validator", "shot_selection", "scene_director", "entity_merge", "scene_save"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
        "resume_sensitive": True,
    },
    # 17.1. Shot별 촬영 기법 (deprecated — scene_detail에서 shot_staging 우선)
    "shot_cinematography": {
        "label": "Shot별 촬영 기법 (deprecated)",
        "category": "analysis",
        "order": 17.1,
        "default_model": "gemini-pro",
        "provider": "gemini",
        "depends_on": ["shot_selection", "scene_director"],
        "fan_out": True,
        "applicability": "disabled",
        "step_type": "transform",
        "lifecycle": "deprecated",
        "replaced_by": "shot_staging",
    },
    # 18. 씬 연관 분석 (레거시)
    "scene_dependency": {
        "label": "씬 연관 분석 (레거시 — disabled)",
        "category": "analysis",
        "order": 18,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["scene_director"],
        "fan_out": False,
        "applicability": "disabled",
        "step_type": "transform",
        "lifecycle": "deprecated",
        "replaced_by": "shot_dependency",
    },
    # 18.1. Shot 연관 분석 (배경+인물)
    "shot_dependency": {
        "label": "Shot 연관 분석 (병렬)",
        "category": "analysis",
        "order": 18.1,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["shot_selection", "scene_director"],
        "fan_out": True,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 19a. 아웃룩 Phase1: 목록 추출
    "outlook_phase1": {
        "label": "아웃룩 목록 추출",
        "category": "analysis",
        "order": 19,
        "default_model": "gemini-pro",
        "provider": "gemini",
        "depends_on": ["scene_director"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
        "requires_projection_sync_before_run": True,  # W1-F12: 상류 scene_director projection 필요
    },
    # 19b. 아웃룩 Phase2: 씬별 매핑
    "outlook_phase2": {
        "label": "아웃룩 씬별 매핑",
        "category": "analysis",
        "order": 19.1,
        "default_model": "gemini-pro",
        "provider": "gemini",
        "depends_on": ["outlook_phase1"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
        "requires_projection_sync_before_run": True,  # W1-F12
    },
    # 19c. 아웃룩 Phase3: 병합 정리
    "outlook_phase3": {
        "label": "아웃룩 병합 정리",
        "category": "analysis",
        "order": 19.2,
        "default_model": "gemini-pro",
        "provider": "gemini",
        "depends_on": ["outlook_phase2"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
        "requires_projection_sync_before_run": True,  # W1-F12
    },
    # Legacy: outlook_extraction (하위 호환 — 3단계 순차 실행)
    "outlook_extraction": {
        "label": "아웃룩 추출 (레거시)",
        "category": "analysis",
        "order": 19.3,
        "default_model": "gemini-pro",
        "provider": "gemini",
        "depends_on": ["scene_director"],
        "fan_out": False,
        "applicability": "on_demand",
        "step_type": "transform",
        "lifecycle": "deprecated",
        "replaced_by": "outlook_phase1,outlook_phase2,outlook_phase3",
        "requires_projection_sync_before_run": True,  # W1-F12
    },
    # 19.5. 샷별 촬영 연출 + 배경 핵심 요소
    "shot_staging": {
        "label": "촬영 연출 (DP)",
        "category": "analysis",
        "order": 19.5,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["shot_validator", "shot_selection", "visual_world_rules", "entity_merge", "scene_camera_flow"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
        "resume_sensitive": True,
        "schema_version": 6,  # 2026-05-21 (E2E v1): v15 schema — character_angles.items.gaze_target_id 가 required nullable 로 승격 (이전엔 looks_at_* 일 때만 emit). OpenAI strict 호환 위해 oneOf 조건부 제거. 이전: 5 (Area #2, 3 field 분리).
    },
    # location_consistency — v0.5.10에서 주입 비활성화, v0.5.12에서 manifest도 제거.
    # step 코드(steps/location_consistency_step.py) + 프롬프트 + version_registry는 보존 (향후 재도입 여지).
    # manifest에 넣으면 run-all이 실행해 LLM 비용만 소비하므로 완전 제외.
    # 19.7 shot_essence_extraction — Phase 1b (v3 plan, 2026-04-29).
    # selected shot description을 essence/peripheral/atmospheric 3분류.
    # toggle: settings.shot_essence_enabled (default False) → if_shot_essence_enabled
    # validator. off 시 run-all 정적 필터에서 제외 + not_applicable 체크포인트 미생성
    # (Codex H3 회귀 가드).
    # consumer 통합(scene_detail prepend)은 Phase 2와 함께.
    "shot_essence_extraction": {
        "label": "샷 essence 추출",
        "category": "analysis",
        "order": 19.7,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["shot_validator", "shot_selection"],
        "fan_out": False,
        "applicability": "if_shot_essence_enabled",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 21.5 location_floor_plan — Phase 3 (v3 plan, 2026-04-29).
    # location 단위 top-down architectural floor plan PNG 1장 자동 생성.
    # gpt-5.5 (prompt 자동 작성) + gpt-image-2 (PNG 생성).
    # toggle: settings.background_mode == 'floor_plan_anchored'
    #   → applicability='if_floor_plan_mode'. off/chain_only 모드에선 자동 not_applicable.
    # spike test 6 (backend/scripts/spike_floor_plan_svg/test_06_auto_floor_plan.py)
    # 의 패턴 그대로 production화. 사람 개입 0.
    # DEPRECATED by Phase 7 (T16, 2026-04-29): floor_plan_prompt + floor_plan_render
    # (order 19.60 + 21.55) 가 이 step을 대체한다. background_mode='on' 시 Phase 7
    # step만 동작. 코드는 보류 — Phase 7.1 cleanup PR에서 별도 삭제 예정.
    "location_floor_plan": {
        "label": "위치 도면 생성 (deprecated, Phase 7로 대체)",
        "category": "image",
        "order": 21.5,  # 21.5: scene_verify(21) 후 / world_guide(22) 전. image>analysis 불변식 만족.
        "default_model": "gpt",
        "provider": "openai",
        # Phase 5 (T8): planner-driven 시 background_planner 결과(floor_plan_order /
        # building_group spec)를 입력으로 받아 sequential 처리. 따라서 manifest
        # depends_on에 background_planner 추가. background_chain_planning /
        # background_chain_render는 best-effort 의존(Phase 4 I1 패턴) — 둘 다
        # on_demand applicability이므로 manifest depends_on에는 명시하지 않는다.
        "depends_on": [
            "shot_validator", "shot_selection",
            "scene_save", "entity_merge",
            "visual_world_rules", "scene_director",
            "background_planner",
        ],
        "fan_out": False,
        "applicability": "disabled",  # Phase 7 deprecation
        "step_type": "asset",
        "lifecycle": "deprecated",  # Phase 7 deprecation
    },
    "scene_consistency": {
        "label": "씬 시각적 일관성",
        "category": "analysis",
        "order": 19.9,
        "default_model": "gemini-pro",
        "provider": "gemini",
        # schema_version: scene_consistency_step.SCENE_CONSISTENCY_SCHEMA_VERSION 와
        # 일치 필수. cp(schema=N)와 manifest(schema=N) 일치해야 step_runner P0-3
        # mismatch 검증 통과. G3.1: 1→2 (evidence/inference 4-field). Area #4: 2→3
        # (element_scope enum required — fixed_elements item 에 "full" | "close" SOT).
        # 옛 v6 cp 는 schema_version 2 → mismatch 로 cp invalidation 강제 v7 rerun.
        "schema_version": 3,
        "depends_on": ["shot_staging", "shot_director", "entity_merge", "shot_validator", "shot_selection", "beat_extract", "scene_save", "visual_world_rules"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
        "resume_sensitive": True,
        # G2.1 (visual_pipeline_contracts_plan #4) — partial 시 downstream cascade 차단.
        # scene_detail/shot_dependency_t2i 가 fixed_elements 를 무방비로 빈 array 처리 하면
        # silent fallback 회귀. blocked_no_selection / failed_all_tiers 는 dispatcher 가
        # batch 중단 + episode.analysis_error 에 명시하도록 fail-fast.
        "allow_partial_downstream": False,
    },
    # 21.70 씬 상세 분석 (병렬) — Phase 9.2: gpt-5.5 → gemini-pro 전환 (시각 추론 강화).
    # G3.2 Phase 6 (Task 20, 2026-05-04): order 20 → 21.70.
    # background_prompt(21.60) 직후로 재배치 — owned list 의존성 (Round 2 #1).
    "scene_detail": {
        "label": "씬 상세 분석 (병렬)",
        "category": "analysis",
        "order": 21.70,  # G3.2: 20 → 21.70 (background_prompt 21.60 뒤).
        "default_model": "gemini-pro",
        "provider": "gemini",
        # schema_version: detail_steps.SCENE_DETAIL_SCHEMA_VERSION 와 일치 필수.
        # cp(schema=N)와 manifest(schema=N) 일치해야 step_runner P0-3 검증 통과.
        # Phase 9.1: 1→2 / Phase 9.2: 2→3 (Rule E/F/G/H + 모델 swap) /
        # Group 1 #1: 3→4 (ID 정책 alignment + outlook validity 검사 + outfit cleanup).
        # G3.1: 4→5 (evidence/inference 4-field — t2i_variations item 에 source_facts/
        # visual_inferences/creative_decisions/confidence required).
        # G3.2: 5→6 (cp shape 에 owned_validation post-parse field 추가).
        # G4.1 (Wave 6 iter3 R1-B1 / R3-B1): 6→7 (cp shape 에 render_prompt_card +
        # render_prompt_card_hash top-level field). 운영 안내 필요 — scene_detail 은
        # `_LEGACY_SCHEMA_BUMP_ALLOWLIST` 외 (entity_t2i 만 자동 RERUN_SELF) 라
        # `_check_cp_mismatch` 가 BLOCK (`step_runner.py:763-769`). 운영자 명시 force
        # 의무. (이전 주석의 "mode='force' 자동 escalate" 표현은 정확히 entity_t2i
        # 한정 — T5-fix iter7 정정.)
        # D6 T5d: 7→8 (cp shape 에 consumed_bg_catalog_hash + consumed_shot_binding_hash
        # top-level field). preflight (T8) 가 master_plan cp 의 sibling 과 비교해
        # STALE_UPSTREAM 판단. T5-fix I1: scene_detail 은 _LEGACY_SCHEMA_BUMP_ALLOWLIST
        # 외 → step_runner._check_cp_mismatch 가 BLOCK (`step_runner.py:763-769`).
        # 운영자 명시 force 의무 (자동 escalate 안 됨).
        # 2026-05-14 area-frame-spatial-contract Task 5: 8→9 (t2i_variations[].
        # applied_frame_spatial_constraint_ids required field 추가, per-variation echo).
        # 2026-05-16 Area #1 W5: 9→10 (id_policy.subject_reference_policy required
        # array — per-subject identity policy SOT, 구 2 sub-field 폐기 대체).
        # 2026-05-18 Area #5 W1: 10→11 (t2i_variations[].reference_phrase_kinds
        # array<enum [character|background|prop]> required — per-variation sidecar
        # for ref_contract_validator step 6 phantom guard replacement).
        # 2026-05-20 C2 v1: 11→12 (t2i_variations[].owned_object_usage[] required —
        # per-variation owned echo {owned_token, usage_kind enum [redraw|anchor|absent],
        # source_phrase} — scene_detail_owned_judge verb whitelist 폐기 → producer echo SOT).
        # 2026-05-23 reference-necessity Phase 2: 12→13 (render_prompt_card 가
        # episode_reference_policy overlay 반영 — text_only subject 의 id_policy/
        # asset_requirements 변동 + card hash semantics 변경 → 구 cp invalidation).
        "schema_version": 13,
        "depends_on": [
            "shot_dependency", "shot_director", "outlook_phase3", "entity_t2i",
            "shot_staging", "scene_consistency",
            "background_prompt",  # G3.2: owned list 의존성 (Round 2 #1).
            "episode_reference_policy",  # reference-necessity Phase 2: render_prompt_card overlay.
            # W21B-W7 W-B (2026-06-12): printed_prop anchor 컨텍스트 주입 —
            # ordering 만 강제 (producer not_applicable/flag OFF 시 주입 0 +
            # config_hash 불변 = byte-identical).
            "visual_continuity_anchor",
        ],
        "fan_out": True,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
        # 2-pass 의존성 선언: scene_detail은 scene_context_loader를 통해 하류 step인
        # shot_dependency_t2i의 refined ref_usage(zoom_in_detail 등)를 역참조로 소비한다
        # (9956c27 이후). scene_detail force 시 cascade가 shot_dependency_t2i 파일을
        # 지우면 역참조가 dead code가 되므로, 아래 선언된 하류는 파일 보존(DB는 stale).
        # 주의: scene_detail force 완료 후 shot_dependency_t2i도 수동 재실행 권장 — drift 방지.
        "consumes_downstream": ["shot_dependency_t2i"],
        "requires_projection_sync_before_run": True,  # W1-F12: scene_consistency/shot_staging projection 필요
        # G3.2 (Round 2 #6): contract_violation partial 시 downstream cascade stop.
        # owned judge 가 violations 검출 시 status=partial → shot_dependency_t2i /
        # t2i_review 가 잘못된 owned 가 박힌 t2i_prompt 를 무방비로 소비하지 않도록 차단.
        "allow_partial_downstream": False,
    },
    # 21.71 Shot 연관 재계산 (T2I 기반, scene_detail 이후).
    # G3.2 Phase 6 (Task 20, 2026-05-04): order 20.5 → 21.71 (scene_detail 21.70 직후).
    "shot_dependency_t2i": {
        "label": "Shot 연관 재계산 (LLM)",
        "category": "analysis",
        "order": 21.71,  # G3.2: 20.5 → 21.71.
        "default_model": "gpt-mini",
        "provider": "gemini",
        "depends_on": ["scene_detail", "scene_consistency"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
        # Area D-next (2026-05-14) — keep_elements shape List[str] →
        # List[{label, kind}]. v5 cp 는 schema_version=1 (또는 missing).
        # Area D-next-min (2026-05-15) — kind enum 3종 → 2종 (immobilized_character 폐기,
        # person/character 묘사 keep_elements 에서 금지). v6 cp 는 schema_version=2.
        # C7 (2026-05-20) — keep_elements[] 에 subject_kind required field 추가
        # (output shape change). v8 cp 는 schema_version=4. v7 cp 는 schema_version=3.
        # step_runner._check_cp_mismatch 가 _LEGACY_SCHEMA_BUMP_ALLOWLIST
        # 외 → BLOCK, 운영자 명시 force 의무 (D6 T5d 패턴).
        "schema_version": 4,
    },
    # 21.72 T2I 검수 (entity_t2i + scene_detail 검증 후 치환) — Phase 9.2: gemini-flash → gpt 전환
    # + shot_staging 의존 추가 (camera_direction inject 위함, 신규 close_framing/physical 검증).
    # G3.2 Phase 6 (Task 20, 2026-05-04): order 20.7 → 21.72 (shot_dependency_t2i 21.71 직후).
    "t2i_review": {
        "label": "T2I 프롬프트 검수",
        "category": "analysis",
        "order": 21.72,  # G3.2: 20.7 → 21.72.
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["entity_t2i", "scene_detail", "shot_validator", "shot_staging", "visual_world_rules", "entity_merge", "entity_detail"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "editorial",
        "lifecycle": "active",
        "modifies_checkpoints": ["entity_t2i", "scene_detail"],
        # Area #6 v1 (2026-05-18+): output cp shape 변경 (review_summary 확장 +
        # diagnostics array + regeneration_required 신설). schema_version: 1 신규
        # — `step_runner._check_cp_mismatch` (line 1496) 가 stale cp (no
        # schema_version stamp OR stamp != 1) 감지 시 BLOCK. 운영자 명시 force
        # 의무 (D6 T5d 패턴). step_runner.py:385-386 의 save-time default stamp
        # (`data["schema_version"] = manifest.get("schema_version", 1)`) 행동
        # 으로 v1 bump 이전 cp 가 default 1 stamp 를 갖고 있을 수 있음 — 본
        # bump 의 invalidation 효과는 cp save 시점에 manifest 에 schema_version
        # 가 명시되어 있던 시점 cp 에만 적용. W3 canary 가 effect 검증.
        "schema_version": 1,
    },
    # 21.73 zoom_continuity_anchor — W21B-W7 W-C1 (2026-06-12).
    # zoom(C) continuity anchor 를 visual_continuity_anchor(21.66, printed_prop
    # 전용)에서 분리 — Codex W_C_NARROW_REVIEW=APPROVE_C3_VARIANT. C seed 의
    # SOT 인 refined ref_usage(zoom_in_detail) 는 shot_dependency_t2i(21.71)
    # cp 에만 실재(18.1=None 실측)라 21.66 은 fresh run 1st pass 에 C seed 0 —
    # 이 step 은 21.71 **이후**(t2i_review 21.72 직후 / image 22 이전)라
    # fresh-run 1-pass proof. seed/anchor 는 기존 plan/provider 모듈 재사용 +
    # wide 멤버 t2i 재작문(revised_wide_t2i, 스파이크1 ②) 산출 — W-C2 가
    # image-phase prompt override(scene_detail cp 불변, prompt_used provenance)
    # + crop/i2i-fill 로 소비. revised 는 새 entity ID 토큰/새 ref 요구 추가
    # 금지(deterministic audit, 위반 variation 폐기+진단 — Codex 안전안).
    # opt-in: settings.zoom_continuity_anchor_enabled (default False, D 와
    # 독립). OFF 시 not_applicable 만 — default 경로 영향 0.
    "zoom_continuity_anchor": {
        "label": "zoom continuity anchor (W21B-W7 W-C1)",
        "category": "analysis",
        "order": 21.73,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": [
            "shot_dependency_t2i",  # C seed SOT (refined ref_usage) — hard
            "scene_detail",         # 최종 t2i variations (재작문 입력)
            "t2i_review",           # scene_detail t2i 수정 반영 보장 (ordering)
            "scene_save", "shot_validator", "shot_selection", "shot_staging",
        ],
        "fan_out": False,
        "applicability": "if_zoom_continuity_anchor_enabled",
        "step_type": "transform",
        "lifecycle": "active",
        # zoom_continuity_anchor_step.py 의 SCHEMA_VERSION 와 동기 의무.
        "schema_version": 1,
    },
    # 21.76 outdoor_site_layout — W21B-W8 (2026-06-12, Codex
    # W21B_W8_SITE_LAYOUT_DESIGN_REVIEW=APPROVED_WITH_NARROW_SCOPE).
    # B-run "가는 방향/멀어짐 구분 못함" fix — ablation 실측(spike_gallery):
    # 텍스트 위치 구절이 지배 변수, top-down 이미지는 ref 채널로 무력 → 좌표는
    # 텍스트 생성의 입력으로만. outdoor(background_classify is_indoor=false)
    # × staging character_angles 2인+ selected shot 을 location 별로 묶어
    # LLM1(sparse site layout 좌표, shape/범위 validator) → 코드(카메라-인물
    # 거리/방향 deterministic 요약) → LLM2(위치/깊이/상대크기/카메라거리 구절만
    # 최소 보정 — entity ID 불변 audit, 카메라워크 언어 금지). 소비는
    # image-phase prompt override 만 (scene_detail cp 불변, 우선순위
    # custom > zoom > site > original). layout 이미지는 review 갤러리 전용 —
    # scene ref 부착 금지. opt-in: settings.outdoor_site_layout_enabled
    # (default False) — OFF 시 not_applicable, default 경로 영향 0.
    "outdoor_site_layout": {
        "label": "야외 site layout 좌표 SOT (W21B-W8)",
        "category": "analysis",
        "order": 21.76,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": [
            "scene_detail",          # 최종 t2i variations (재작문 입력)
            "t2i_review",            # scene_detail t2i 수정 반영 보장 (ordering)
            "background_classify",   # is_indoor structured (outdoor seed)
            "shot_dependency_t2i",   # zoom 멤버 제외 신호 (soft read)
            "scene_save", "shot_validator", "shot_selection",
            "shot_staging", "shot_director",
        ],
        "fan_out": False,
        "applicability": "if_outdoor_site_layout_enabled",
        "step_type": "transform",
        "lifecycle": "active",
        # outdoor_site_layout_step.py 의 SCHEMA_VERSION 와 동기 의무.
        "schema_version": 1,
    },
    # 21.77 outdoor_place_spec — W22 직행 체인 ①. building_group(실외 멤버)당
    # 마커 스펙(존 라벨+마커 항목, evidence-bound) LLM 저작. 하류=outdoor_place_canon
    # (캐논 자산)/outdoor_shot_grounding(샷 접지). opt-in:
    # settings.outdoor_direct_compose_enabled (default False) — OFF 시 not_applicable.
    # 설계: docs/w22-outdoor-canon-direct-compose-design-20260709/design.md
    "outdoor_place_spec": {
        "label": "야외 장소 마커 스펙 (W22 직행 ①)",
        "category": "analysis",
        "order": 21.77,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": [
            "background_classify",   # building_groups + is_indoor (실외 그룹 seed)
            "scene_save",            # 씬 원문 전문 (evidence 근거)
            "scene_director",        # scene → primary_location 매핑
            "shot_validator", "shot_selection",
            "shot_staging",          # 선택 샷 연출 데이터 (화면 중요 요소 단서)
            "entity_merge",          # loc 상세 (description/visual_traits)
            "visual_world_rules",    # 세계 물리 규칙 (참고)
        ],
        "fan_out": False,
        # BLOCKING-4 fix (2026-07-13): 직행 OR 레시피 맵 분기 어느 쪽이든
        # 실행 — outdoor_map_conti 가 direct compose 없이 독립 성립.
        # Stage D5 (2026-07-15): lane pipe 도 스펙 소비(레인 판정·seed SOT·
        # SITE PLAN 마커) — OR 확장, 기존 flag 조합 byte-identical.
        "applicability": "if_outdoor_direct_or_map_or_lane",
        "step_type": "transform",
        "lifecycle": "active",
        # outdoor_place_spec_step.py 의 SCHEMA_VERSION 와 동기 의무.
        # E2E 6회차 실측(2026-07-16): step v2 bump(2026-07-11, temporal_scope)
        # 때 이 값이 1 로 방치 — resume drift 검사가 정상 CP 를 BLOCK.
        "schema_version": 2,
    },
    # 21.78 outdoor_place_canon — W22 직행 체인 ②. 스펙 그룹당 캐논 자산 2장
    # (실사 마스터=룩 SOT, 탑다운 맵=배치 SOT) — s29 역방향 체인(nb2 3롤→
    # GPT/Gemini 이중 판정→결함 수정→gpt 재투영). asset_type=place_canon,
    # pipeline_role=outdoor_canon_photo/outdoor_canon_map. floor_plan_render
    # 는 스타일 참조 soft 의존(FP 0 프로젝트=텍스트 계약만으로 작도).
    "outdoor_place_canon": {
        "label": "장소 캐논 자산 (W22 직행 ②)",
        "category": "image",
        "order": 21.78,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": [
            # Stage D: lane pipe ON 시 map_marker 그룹 필터 입력 (CP 부재
            # 시 all-groups fallback — 소프트 소비)
            "outdoor_lane_plan",
            # floor_plan_render 는 soft 의존 — manifest 에 넣으면 FP 부재/실패
            # 시 이 스텝이 게이트에 막힌다. order 21.78(>21.55)로 순서만 보장,
            # 스타일 FP 는 _execute 안 checkpoint soft-load (Codex NARROW).
            "outdoor_place_spec",    # 마커 스펙 (콘텐츠 SOT)
        ],
        "fan_out": False,
        # BLOCKING-4 fix: 직행 OR 레시피 맵 분기 (spec 과 동일).
        "applicability": "if_outdoor_direct_or_map_or_lane",
        "step_type": "asset",
        "lifecycle": "active",
        # outdoor_place_canon_step.py 의 SCHEMA_VERSION 와 동기 의무.
        "schema_version": 1,
    },
    # 21.79 outdoor_shot_grounding — W22 직행 체인 ③. 캐논 그룹의 야외 선택
    # 샷마다 맵+범례+샷+씬 전문 → 존/앵커/카메라 접지 + moment_context_en
    # (씬 원문 세계 사실 보강). 캐논 결측 그룹=skip(기존 라인 fallback).
    "outdoor_shot_grounding": {
        "label": "야외 샷 grounding (W22 직행 ③)",
        "category": "analysis",
        "order": 21.79,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": [
            "outdoor_place_spec",    # 마커 스펙 (동적 enum 소스)
            "outdoor_place_canon",   # 맵 png (첨부 대상)
            "scene_save", "scene_director",
            "shot_validator", "shot_selection", "shot_staging",
        ],
        "fan_out": False,
        "applicability": "if_outdoor_direct_compose",
        "step_type": "transform",
        "lifecycle": "active",
        # outdoor_shot_grounding_step.py 의 SCHEMA_VERSION 와 동기 의무.
        "schema_version": 1,
    },
    # 21.80 outdoor_lane_plan — 야외 3레인 재설계 Stage A (2026-07-14, 설계 v2).
    # outdoor_place_spec 그룹(스펙 있는 그룹만)마다 place_segment 분할+선택 샷
    # 레인 바인딩(map_marker/structure_plate, evidence-bound) LLM 저작 + 보수
    # 라우팅(저신뢰/혼합→structure_plate). 이미지 생성 없음 — canon/맵 불요.
    # opt-in: settings.outdoor_lane_plan_enabled (default False). fresh-run 에서
    # 이 flag 만 ON 이면 outdoor_place_spec 이 not_applicable(=gate satisfied)일
    # 수 있음 — spec cp 부재 그룹은 skipped(비실패). spec applicability 확장은
    # Stage E(flag 전환)에서 결정.
    "outdoor_lane_plan": {
        "label": "야외 3레인 lane plan (설계 v2 ①)",
        "category": "analysis",
        # Codex Stage D 재리뷰 BLOCKING-1: 그룹 실패=partial 이 하류로
        # 조용히 통과하지 않도록 선제 차단 (group parity 검증과 이중 방어)
        "allow_partial_downstream": False,
        # Stage D BLOCKING-1: 21.80 → 21.775 — outdoor_place_canon(21.78)이
        # lane plan CP 로 map_marker 그룹만 타깃하도록 선행 배치.
        "order": 21.775,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": [
            "outdoor_place_spec",    # 존 라벨+마커 스펙 (레인 판정 입력)
            "scene_save",            # 씬 원문 전문 (evidence 근거)
            "scene_director",        # scene → primary_location 매핑
            "shot_validator", "shot_selection", "shot_staging",
        ],
        "fan_out": False,
        "applicability": "if_outdoor_lane_plan",
        "step_type": "transform",
        "lifecycle": "active",
        # outdoor_lane_plan_step.py 의 SCHEMA_VERSION 와 동기 의무.
        "schema_version": 1,
    },
    # 21.78 outdoor_frame_mode — E2E6 피드백 ③ (2026-07-16). lane plan 의
    # structure_plate 샷별 frame_mode(structure_dominant/layout_dominant)
    # LLM 판정 — structure_dominant=마커 맵→스케치 생략(스틸 seed 직참조).
    # evidence 인용 실재+대상 샷 parity fail-closed. 이미지 생성 없음.
    # opt-in: outdoor_frame_mode_enabled(+lane pipe, default False).
    "outdoor_frame_mode": {
        "label": "레인2 구도 판정 (E2E6 ③)",
        "category": "analysis",
        "allow_partial_downstream": False,
        "order": 21.78,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": [
            "outdoor_lane_plan",     # structure_plate 바인딩 (대상 SOT)
            "outdoor_place_spec",    # persisted plan 재검증 입력
            "scene_save",            # 씬 원문 전문 (evidence 근거)
            "shot_validator", "shot_selection", "shot_staging",
            "scene_director",        # reconstructor 입력
        ],
        "fan_out": False,
        "applicability": "if_outdoor_frame_mode",
        "step_type": "transform",
        "lifecycle": "active",
        # outdoor_frame_mode_step.py 의 SCHEMA_VERSION 와 동기 의무.
        "schema_version": 1,
    },
    # 21.915 outdoor_structure_form_reference — 검색 그라운딩 형태 참조
    # (2026-07-28 사용자 지시: "야외 구조물은 모두 검색 기반 + VLM").
    # structure_plate 바인딩 그룹마다 ①원본어 검색 지시문 저작 ②웹 이미지
    # 검색(search_content_types=[image,text] — 기본값은 텍스트만이라 사진이
    # 안 온다) ③안전 다운로드 ④VLM **비교선택 1장**. 선택 사진은
    # structure_form_ref ImageAsset 으로 영속되고 seed 가 형태 참조로 소비한다.
    # 필수 대상 fail-closed — 순수 T2I 자동 degrade 금지(조용한 하강은 "배경이
    # 이상해"를 아무도 모르게 재생산한다). 설계=docs/superpowers/specs/
    # 2026-07-28-search-grounded-seed-and-conti-entity-chain-design.md
    "outdoor_structure_form_reference": {
        "label": "야외 구조물 형태 참조 검색·선택 (Stage D 선행)",
        "category": "image",
        "order": 21.915,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": [
            # 대상 = structure_plate 바인딩 ∪ scope 판정 yes (v2 합집합).
            "outdoor_lane_plan",
            "outdoor_place_spec",    # persistent_site 검증 서술 (검색 대상)
            "background_classify",   # building_groups
            "entity_merge",          # 실내/실외 loc 서술
            "scene_save",            # 씬 원문 (source_language 판정 전용)
            "visual_world_rules",    # 지역·시대 확정 사실
        ],
        "fan_out": False,
        "applicability": "if_outdoor_lane_pipe",
        "step_type": "generate",
        "lifecycle": "active",
        # outdoor_structure_form_reference_step.py 의 SCHEMA_VERSION 와 동기.
        # v2 = 그룹 지문 재사용 / v3 (2026-07-30) = 대상 seed exact parity
        # (판정 제거) + Gemini·GPT 이중 판정 선택.
        # v4 (2026-08-02, A4) = 라운드 불변 저장. group entry 에 round_id ·
        #   path_kind · round_contract_version 이 붙고 최상위에
        #   round_replayed_count 가 생겼다. shape 이 바뀌었으므로
        #   config_hash 만으로 두지 않는다.
        # v5 (2026-08-03) = **사전조사가 이미지 검색보다 먼저** 이 스텝
        #   안에서 돈다(예전에는 씨드 스텝이 검색 뒤에 조사해서 조사 결과가
        #   질의로 이어질 수 없었다). group entry 에 typology · fitting_refs
        #   가 붙는다 — 부속 대상별로 따로 검색해 고른 참조 사진들이다.
        # ★핀이 어긋나면 신규 CP 를 resume 이 구 스키마로 읽는다 —
        # test_step_schema_pins.py 가 두 값을 교차 검증한다.
        "schema_version": 5,
    },
    # 21.92 outdoor_structure_seed — 야외 3레인 Stage D (2026-07-15, 레인2).
    # structure_plate 바인딩 그룹마다 구조물 seed 실사 생성.
    # seed 입력 SOT=outdoor_place_spec persistent_site 검증 서술
    # +excluded_transient negative contract. persisted lane plan 소비 직전
    # 재검증(fail-closed). opt-in: outdoor_lane_pipe_enabled+outdoor_lane_plan_
    # enabled (default OFF = not_applicable, 기존 경로 byte-identical).
    # v3 (2026-07-28): 순수 T2I → **검색 선택 실사 형태 참조 i2i**.
    # 권위 = seed 가 이 작품 구조물의 IDENTITY/LOOK SOT, 웹 사진은 그 종류의
    # 구조·비례·재료 관습만 주는 FORM REFERENCE (dual SOT 방지).
    "outdoor_structure_seed": {
        "label": "야외 레인2 구조물 seed (Stage D)",
        "category": "image",
        "order": 21.92,
        "default_model": "gemini-image",
        "provider": "google",
        "depends_on": [
            "outdoor_lane_plan",     # 레인 바인딩 (structure_plate 그룹 선정)
            "outdoor_place_spec",    # persistent_site 검증 서술 (seed SOT)
            "background_classify",   # building_groups (실내 규모 앵커 조인)
            "entity_merge",          # 실내 loc 서술 (규모 SOT)
            "scene_save",            # 씬 원문 (plan 재검증 evidence)
            "shot_ref_classify",     # world_anchor_en
            # [2026-08-01 Codex BLOCKING 3] 검색 선택 형태 참조의 producer.
            # 의존이 없으면 producer 의 partial·재실행이 seed 를 막지도
            # 무효화하지도 못한다 — 소비자는 CP 를 직접 읽는데 그래프상
            # 남남이었다.
            "outdoor_structure_form_reference",
            # Stage D BLOCKING-2: authoritative 그룹 샷 재구성 입력
            "shot_validator", "shot_selection", "shot_staging",
            "scene_director",
        ],
        "fan_out": False,
        "applicability": "if_outdoor_lane_pipe",
        "step_type": "generate",
        "lifecycle": "active",
        # outdoor_structure_seed_step.py 의 SCHEMA_VERSION 와 동기 의무.
        # v2 (2026-07-16 R7): siteplan 산출 제거.
        # v3 (2026-07-28): 검색 선택 실사 형태 참조 i2i + 필수 대상 fail-closed.
        # v5 (2026-08-01): 감사 계약 shape — seed_decision 무조건 기록 +
        # 재생성 이슈 정책(이름·버전)·보류 결함·수정 생략 사유 typed 영속.
        # v6 (2026-08-03): entry 에 seed_paths 추가(직접/스케치 두 경로 중
        # 무엇이 돌고 무엇이 이겼는지·도해 파일과 자산). 롤 라벨의 뜻이 저작
        # 변형에서 **경로**로 바뀌어 옛 CP 의 seed_selected 를 같은 뜻으로
        # 읽으면 안 된다. ★스텝은 6 이었는데 이 핀이 5 에 남아 있었다 —
        # 어긋난 채로 두면 신규 CP 를 resume 이 구 스키마로 읽는다.
        "schema_version": 6,
    },
    # 21.90 shot_ref_classify — s40/s41 콘티·스틸 레시피 이식 (2026-07-13) ①.
    # 선택 샷 전체를 bgonly(person_visible)/prev v3(배경 동일성 유일 기준,
    # usage_en TAKE/EXCLUDE 동시 저작)/씬별 time_of_day(LLM 저작 — 헤딩
    # substring 매칭 금지 규칙 대체)로 분류. 소비자=shot_conti_light(콘티
    # 대상 선별)+scene_image still_recipe(참조 규칙·시간대 lock).
    # still_recipe_mode="v1" 일 때만 applicable — OFF 면 기존 파이프
    # byte-identical.
    "shot_ref_classify": {
        "label": "샷 참조 분류 (레시피)",
        "category": "analysis",
        "order": 21.90,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": [
            "shot_validator", "shot_selection",  # 샷 목록·선택 필터
            "scene_save",                        # 씬 원문 전문·헤딩
            "scene_director",                    # primary_location (LOCATION 병기)
        ],
        "fan_out": False,
        "applicability": "if_still_recipe",
        "step_type": "transform",
        "lifecycle": "active",
        # shot_ref_classify_step.py 의 SCHEMA_VERSION 와 동기 의무.
        # v4 (2026-08-07): bgonly 번들 `handled_by` 신설 — 손에 든 물건의
        # 주인을 샷 텍스트 근거로 판정한다. 스텝 상수와 함께 승격.
        # v3 (2026-07-23 슬라이스 E 실측): a07f8c84 가 스텝 상수만 3 으로
        # 승격하고 여기를 방치 — completed CP(schema 3) resume 이 contract
        # drift BLOCK 되던 동일 패턴 3번째 재발. 동기 교정.
        "schema_version": 4,
    },
    # 21.905 background_share_plan — 2026-07-19 재설계 B-2: 에피소드 전체
    # 배경 공유·참조 계획(LLM 지휘). 계획=스틸 prev/배경 결정의 상위 권위
    # (부재=classify 판정 fail-safe). flag OFF=no-op.
    "background_share_plan": {
        "label": "배경 공유 계획 (전체 지휘)",
        "category": "analysis",
        "order": 21.905,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": [
            "shot_validator", "shot_selection",  # 샷 목록·선택 필터
            "scene_save",                        # 씬 원문 전문
            "scene_director",                    # primary_location
        ],
        "fan_out": False,
        "applicability": "if_background_share_plan",
        "step_type": "transform",
        "lifecycle": "active",
        "schema_version": 1,
    },
    # 21.91 shot_continuity — s40/s41 레시피 이식 ②. 텍스트 안전망 저작:
    # 자세 정본(움직일 수 없는 인물, 전 등장 샷 글자 그대로 재사용)+중의어
    # 최소 교정(pose_fix)+carried state(양방향 전파·씬 경계 차단=프롬프트
    # 계약). 소비자=shot_conti_light+scene_image still_recipe 프롬프트 조립.
    "shot_continuity": {
        "label": "샷 연속성 저작 (레시피)",
        "category": "analysis",
        "order": 21.91,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": [
            "shot_validator", "shot_selection",  # 샷 목록·선택 필터
            "scene_save",                        # 씬 원문 전문·헤딩
            "entity_merge",                      # EntityCanon(short_id 매핑)
        ],
        "fan_out": False,
        "applicability": "if_still_recipe",
        "step_type": "transform",
        "lifecycle": "active",
        # shot_continuity_step.py 의 SCHEMA_VERSION 와 동기 의무.
        "schema_version": 1,
    },
    # 24.73 shot_conti_light — s40/s41 레시피 이식 ③. 인물 샷(비 bgonly·비
    # prev)마다 배경 플레이트 느슨 참조(BG_REF_CLAUSE) LIGHT_FRAME 단일 프레임
    # 콘티(gpt-image-2, 1롤 — multiroll 미적용, s41 실증). Codex 1차 리뷰
    # BLOCKING-1 fix: order 21.92→24.73(background_render 24.72 뒤) +
    # background_render hard dependency — fresh run 에서 플레이트가 먼저
    # 생성되도록 보장(not_applicable=satisfied 라 BACKGROUND_MODE=off 무해).
    # 소비자=scene_image still_recipe 참조 조립([플레이트+콘티+엔티티]).
    "shot_conti_light": {
        "label": "경량 콘티 (레시피)",
        "category": "image",
        "order": 24.73,
        "default_model": "gpt-image-2",
        "provider": "openai",
        "depends_on": [
            # 2026-07-16 복잡 구조물=A/B 재설계: outdoor_frame_mode 의존
            # 제거 (소비처 소멸 — Codex 열린쟁점④ consumer/hash/dependency
            # 전부 제거)
            # Stage D BLOCKING-2: lane 분기 입력 (flag OFF 시 두 스텝 모두
            # not_applicable=gate satisfied — 기존 스케줄 불변)
            "outdoor_lane_plan", "outdoor_structure_seed",
            "shot_ref_classify",   # bgonly·prev 대상 선별
            "shot_continuity",     # 자세 정본·pose_fix·carried 절
            "shot_validator", "shot_selection",  # 샷 목록·선택 필터
            "scene_save", "scene_director",      # 헤딩·primary_location
            "background_render",   # 플레이트 (fresh-run 순서 보장)
            # 2차 리뷰 H4: 맵 분기 생산자 — map OFF 면 not_applicable=
            # satisfied 라 정적 추가 안전, ON 시 fresh-run 순서 보장.
            "outdoor_place_spec", "outdoor_place_canon",
            # E2E10 fix④ Codex BLOCKING-1: 콘티 스텝이 share_plan CP 로
            # classify prev 를 override(apply_share_plan_prev) — targeted
            # rerun/force 후 stale 전파 보장. flag OFF=not_applicable=
            # satisfied 라 정적 추가 안전.
            "background_share_plan",
        ],
        "fan_out": False,
        "applicability": "if_still_recipe",
        "step_type": "asset",
        "lifecycle": "active",
        # shot_conti_light_step.py 의 SCHEMA_VERSION 와 동기 의무.
        # v2 (2026-07-16 R1/R2): plate_authority + lane ab_select 재편.
        # v3 (2026-07-17): seed-bg typed 배경 권위 (bg_source/bg_path).
        "schema_version": 3,
    },
    # 19.51 background_classify — Phase 7 Step 1. building group 분류 + chain_bg vs prev_shot_ref.
    # order=19.51: 19.5 active shot_staging와 충돌 회피 (T20 fix). 직접 후행 단계는 master_plan(19.52).
    "background_classify": {
        "label": "배경 그룹 분류",
        "category": "analysis",
        "order": 19.51,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": [
            "shot_validator", "shot_selection",
            "entity_merge", "entity_detail",
            "visual_world_rules", "scene_director",
        ],
        "fan_out": False,
        "applicability": "if_background_mode",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 19.52 background_master_plan — Phase 7 Step 2. 그룹별 master plan (그룹간 ThreadPool 병렬).
    # order=19.52: Phase 5 background_planner(19.55)와 collision 방지. background_classify(19.50)
    # 직후, legacy background_planner(19.55) 직전. floor_plan_anchored alias 환경에서
    # 두 step이 동시에 trigger되어도 실행 순서 ambiguous하지 않도록 한다.
    "background_master_plan": {
        "label": "배경 마스터플랜",
        "category": "analysis",
        "order": 19.52,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": [
            "background_classify", "scene_save",
            "shot_validator", "shot_selection",
            "visual_world_rules",
        ],
        "fan_out": False,
        "applicability": "if_background_mode",
        "step_type": "transform",
        "lifecycle": "active",
        # D6 T4: schema_version 2 — raw intent + bg_catalog post-processing 도입.
        # 기존 v1 cp 는 sibling field (background_catalog/bg_catalog_hash) 부재 →
        # _check_cp_mismatch 가 stale 신호. allowlist 외라 BLOCK — 운영자 force 의무.
        # T4-fix3: schema_version 3 — floor_plans[].loc_id + space_key_hint required +
        # raw validator 의 fp ↔ bg link cross-check (FpLinkMismatchError).
        # W20F6 (2026-05-28): schema_version 4 — historical hard floor-plan
        # scope filter (indoor AND shots>=2 AND dep_BG>=1) + transitive BG
        # cascade. Superseded by W21B-wave-2 surface_role diagnostics below.
        # W21B-wave-2 (2026-05-29): schema_version 5 —
        # backgrounds[].surface_role raw intent + catalog/hash carry +
        # data.diagnostics.w21b_surface_role_scope_filter 추가.
        # background_master_plan_step.py:39 의 SCHEMA_VERSION 와 동기 의무.
        "schema_version": 5,
    },
    # 19.61 floor_plan_prompt — Phase 7 Step 3. master_plan의 floor_plans[]별 t2i prompt 생성.
    # 도면간 depends_on_fp DAG는 거의 single-level이지만 compute_dag_levels로 안전 처리.
    # order=19.61: 19.6 deprecated background_chain_planning과 충돌 회피 (T20 fix).
    "floor_plan_prompt": {
        "label": "도면 t2i 프롬프트",
        "category": "analysis",
        "order": 19.61,
        "default_model": "gpt",
        "provider": "openai",
        # schema_version: floor_plan_prompt_step.SCHEMA_VERSION 와 일치 필수.
        # D6 v4 (2026-05-09): 1→2 — camera_recommendations[].bg_id pattern BG_ID_RE
        # 정합 + runtime enum injection + exact-set validator + state_label_raw.
        # 누락 시 step_runner._check_cp_mismatch 가 manifest default 1 vs cp 2 비교
        # → resume 시 cp_mismatch BLOCK.
        "schema_version": 2,
        "depends_on": [
            "background_master_plan", "scene_save",
            "shot_validator", "shot_selection",
            "visual_world_rules",
        ],
        "fan_out": False,
        "applicability": "if_background_mode",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 21.55 floor_plan_render — Phase 7 Step 4. floor_plan_prompt + master_plan(depends_on_fp)
    # 결과를 따라 PNG 생성 (gpt-image-2). 도면간 DAG → compute_dag_levels로 level 병렬.
    # category=image / step_type=asset (이미지 단계 — 분석 단계 뒤에 배치).
    # order=21.55: Phase 5 location_floor_plan(21.5)와 collision 방지. applicability=if_background_mode
    # vs Phase 5 if_floor_plan_mode로 상호 배타이지만 표시 순서를 명확히 분리.
    "floor_plan_render": {
        "label": "도면 이미지 생성",
        "category": "image",
        "order": 21.55,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": [
            "floor_plan_prompt", "background_master_plan",
        ],
        "fan_out": False,
        "applicability": "if_background_mode",
        "step_type": "asset",
        "lifecycle": "active",
    },
    # 21.56 floor_plan_light_sidecar — W21B-w5 STEP5-B (2026-06-01, gpt-image-2 sparse).
    # 설계: ①gpt-5.5 빈도인식 sparse selection — floor_plan_prompt 의
    # numbered_elements + per-element camera_use_count(camera_recommendations.
    # use_numbered_elements 집계 = 샷 등장빈도) 로 8-10 essential 번호마커(고빈도
    # TV·커튼 보호) + 무번호 geometry skeleton + 짧은 room_schematic_prompt(마커배치
    # 제약 포함) → ②gpt-image-2 순수 text-to-image 렌더(ref 일절 금지 — ref 주면
    # 마커번호 누출/중복, v0 ref-edit 은 "11" 중복) → best_of_n draw 중 첫 성공 =
    # primary, 전체 draw 는 visual gate 용 persist → step checkpoint
    # `light_renders/<fp_id>_<i>.png`. (렌더 모델=gpt-image-2: 2026-06-01 사용자
    # 육안 — 동일 프롬프트에서 "번호당 1원"을 Nano Banana 2 보다 잘 지킴, NB2 는
    # office 반복가구 마커 중복. 잔존 STRUCTURE 문제는 upstream floor_plan_prompt
    # skeleton 이슈로 두 모델 공통, 렌더 탓 아님). 상세 PNG 는
    # shot_projection_card / edge-judge substrate 로 불변(sidecar ONLY — W21B-w5
    # partition SOT 불변). background_render consumer 가 rendered 시 FP anchor PNG
    # 를 light sidecar 로 교체, else 상세 FP fallback. validation = STRUCTURAL+render
    # -status only(방 그려짐/overlay essential 금지/prompt 존재) — geometry fidelity
    # + best-of-N 품질은 육안 gate(Codex lock). category=image / step_type=asset.
    # opt-in: settings.floor_plan_light_sidecar_enabled (default False) +
    # floor_plan_prompt_version ∈ {"6","7"}. default 경로 영향 0 (not_applicable cp
    # 만). floor_plan_light_sidecar_step.py 의 SCHEMA_VERSION 와 동기 의무.
    "floor_plan_light_sidecar": {
        "label": "도면 단순화 사이드카 (W21B-w5)",
        "category": "image",
        "order": 21.56,
        "default_model": "gpt",
        "provider": "openai",
        # NB2 sparse path reads floor_plan_prompt (numbered_elements +
        # camera_recommendations) — pure text-to-image, no detailed-FP reference.
        "depends_on": [
            "floor_plan_prompt",
        ],
        "fan_out": False,
        "applicability": "if_background_mode",
        "step_type": "asset",
        "lifecycle": "active",
        "schema_version": 2,
    },
    # 21.57 floor_plan_overlay_payload — W19B-1 (2026-05-26).
    # floor_plan_prompt v6 의 numbered_elements + camera_recommendations
    # (per-bg use/ignore arrays) 와 background_master_plan 의 backgrounds[]
    # DAG 를 deterministic exact-ID join → per-bg overlay payload (base /
    # transient marker partition + target_unit_marker_numbers +
    # dominant_target_unit_marker_number + clean_background_expected).
    # **LLM / image / VLM API 호출 0** — 순수 join. v6 (background_prompt
    # v7 + background_render w18j_overlap) opt-in 시 producer. v5 default
    # path 는 step 내부 floor_plan_prompt_version not in {"6","7"} 분기에서
    # not_applicable 로 떨어진다 (v7 = v6-compatible, W21B-wave-4).
    "floor_plan_overlay_payload": {
        "label": "도면 오버레이 페이로드",
        "category": "analysis",
        "order": 21.57,
        "default_model": "gpt",  # 미사용 (deterministic) — manifest 형식상 필수
        "provider": "openai",
        "depends_on": [
            "floor_plan_prompt", "background_master_plan",
        ],
        "fan_out": False,
        "applicability": "if_background_mode",
        "step_type": "transform",
        "lifecycle": "active",
        "schema_version": 1,
    },
    # 21.58 base_location_dossier — W20A (2026-05-27).
    # floor_plan_prompt v6 numbered_elements + floor_plan_render png_path +
    # background_master_plan backgrounds[] DAG + floor_plan_overlay_payload
    # overlays{} 를 deterministic 하게 join → per-fp dossier artifact.
    # 산출: dwelling identity 5 axes (structure / materials placeholder /
    # fixed_elements_inventory / standard_of_living_band /
    # lighting_identity placeholder) + fp_geometry_candidates shape
    # (camera/look-at/visible-units/visible-openings slots, VLM 의존 빈
    # dict) + fp_geometry_vlm_readback (synthetic_placeholder, real VLM
    # 미호출) + anchor_selection_metadata (LLM 영역 — code 가 anchor 선택
    # 안 함, candidate surface 만 emit). **LLM / image / VLM API 호출 0**.
    # opt-in: settings.base_location_dossier_enabled (default False) +
    # floor_plan_prompt_version ∈ {"6","7"} 동시 만족 시에만 active (v7 =
    # v6-compatible, W21B-wave-4). default 경로 영향 0 (not_applicable cp 만).
    "base_location_dossier": {
        "label": "기초 위치 정보 (W20A)",
        "category": "analysis",
        "order": 21.58,
        "default_model": "gpt",  # 미사용 (deterministic) — manifest 형식상 필수
        "provider": "openai",
        "depends_on": [
            "floor_plan_prompt", "floor_plan_render",
            "background_master_plan", "floor_plan_overlay_payload",
        ],
        "fan_out": False,
        "applicability": "if_background_mode",
        "step_type": "transform",
        "lifecycle": "active",
        # W20F5 (2026-05-28): schema_version 2 — _anchor_candidate_surface
        # ramped fallback. base_location_dossier_step.py:39 의 SCHEMA_VERSION
        # 와 동기 의무.
        "schema_version": 2,
    },
    # 21.59 floor_plan_geometry_readback — W20A2 (2026-05-27).
    # base_location_dossier 의 fp_id 별 base_marker_inventory + fp_image_path
    # 를 consume → synthetic_fixture readback (deterministic placeholder
    # cells; real VLM 미호출) → geometry candidates (camera_cell /
    # look_at_cell candidates per unit, direction_vector / view_cone
    # records, visible_units / visible_openings supersets,
    # wall_door_invalidation diagnostics) → review HTML overlay 산출 +
    # checkpoint dir 안 `review_html/<fp_id>.html` 에 write. **LLM /
    # image / VLM API 호출 0**, sidecar HTML file 만 추가. opt-in:
    # settings.floor_plan_geometry_readback_enabled (default False) +
    # base_location_dossier_enabled=True + floor_plan_prompt_version ∈
    # {"6","7"} 모두 만족 시 active (v7 = v6-compatible, W21B-wave-4).
    # default 경로 영향 0.
    "floor_plan_geometry_readback": {
        "label": "도면 기하 readback (W20A2)",
        "category": "analysis",
        "order": 21.59,
        "default_model": "gpt",  # 미사용 (deterministic) — manifest 형식상 필수
        "provider": "openai",
        "depends_on": [
            "base_location_dossier",
            "floor_plan_render",
        ],
        "fan_out": False,
        "applicability": "if_background_mode",
        "step_type": "transform",
        "lifecycle": "active",
        # W20F7-A (2026-05-28): schema_version 2 — VLM provider
        # max_completion_tokens 2000→16000 bump (stale cp 자동 인식용).
        # floor_plan_geometry_readback_step.py:39 의 SCHEMA_VERSION 와 동기 의무.
        "schema_version": 2,
    },
    # 21.591 floor_plan_semantic_readback — W21B-wave-4 (2026-05-29).
    # base_location_dossier 의 fp_id 별 base_marker_inventory + fp_image_path
    # 를 consume → marker-SEMANTIC readback (도면 PNG 의 각 base marker 에
    # 실제로 그려진 object 가 expected label/class 와 일치하는가) + fail-closed
    # gate (pass / needs_fix / needs_review / synthetic_unverified). real
    # provider selector OFF (default) 시 synthetic fixture (deterministic
    # placeholder verdicts, real VLM 미호출). W20A2 geometry readback
    # (marker number/cell/base-kind 공간 readback) 과 **별도** gate — 비용/
    # 실패정책/acceptance 가 다름. shot_aware_bg_render_plan 의 hard
    # depends_on 에는 넣지 않는다 (semantic 은 관측/품질 gate, planner hard
    # dep 은 geometry+dossier+overlay). opt-in:
    # settings.floor_plan_semantic_readback_enabled (default False) +
    # base_location_dossier_enabled=True + floor_plan_prompt_version ∈
    # {"6","7"} 모두 만족 시 active. default 경로 영향 0.
    "floor_plan_semantic_readback": {
        "label": "도면 마커 의미 fidelity readback (W21B-w4)",
        "category": "analysis",
        "order": 21.591,
        "default_model": "gpt",  # synthetic default; real provider opt-in
        "provider": "openai",
        "depends_on": [
            "base_location_dossier",
            "floor_plan_render",
        ],
        "fan_out": False,
        "applicability": "if_background_mode",
        "step_type": "transform",
        "lifecycle": "active",
        # floor_plan_semantic_readback_step.py 의 SCHEMA_VERSION 와 동기 의무.
        "schema_version": 1,
    },
    # 21.593 shot_projection_card — W21B-wave-4 (2026-05-30, C2).
    # semantic_readback(21.591) 이후 / plan(21.595) 이전. (bg_id, shot_id)
    # 단위로 overlay payload union(base∪transient∪ignored) inventory +
    # source_hashes provenance 를 조립 → projection-card VLM output(카메라
    # pose 안에서 무엇이 어디쯤 보이는지 한국어 prose 2층: 전체 scene +
    # bg-plate-only) → card envelope + v0 deterministic gate (pass /
    # needs_review / blocked). real provider selector OFF (default) 시
    # synthetic fixture (non-authoritative placeholder, gate auto-pass
    # 불가, real VLM 미호출). hard precondition (per-(bg,shot) 누락 시
    # provider 미호출 + card_state=blocked+fallback_reason) = render /
    # overlay / dossier / geometry / camera pose(floor_plan_prompt
    # camera_recommendations). dossier/geometry/overlay/render/camera_rec
    # +shot_context 는 source_hashes 에 포함되어 변경 시 card invalidation.
    # shot_staging/scene_save 는 shot_context(shot intent + scene text)
    # 조립용 soft dep (누락 시 empty context, block 아님). semantic_readback
    # 은 OPTIONAL gate (enabled 면 per-fp gate_state 반영, disabled 면
    # semantic_gate_state=not_available). card 는 background_prompt
    # (C4) 와 scene_detail (C5) 공통 shot-observation SOT. opt-in:
    # settings.shot_projection_card_enabled (default False) +
    # base_location_dossier_enabled=True + applicable (bg, shot) target 존재
    # 시 active. default 경로 영향 0. shot_aware_bg_render_plan hard
    # depends_on 엔 넣지 않는다 (plan vNext 가 card 를 소비하는 건 C3).
    "shot_projection_card": {
        "label": "샷 projection card — 카메라 가시물 서술 SOT (W21B-w4)",
        "category": "analysis",
        "order": 21.593,
        "default_model": "gpt",  # synthetic default; real provider opt-in
        "provider": "openai",
        "depends_on": [
            "base_location_dossier",
            "floor_plan_render",
            "floor_plan_overlay_payload",
            "floor_plan_geometry_readback",
            "floor_plan_prompt",
            "background_master_plan",
            "shot_staging",
            "scene_save",
        ],
        "fan_out": False,
        "applicability": "if_background_mode",
        "step_type": "transform",
        "lifecycle": "active",
        # shot_projection_card_step.py 의 SCHEMA_VERSION 와 동기 의무.
        "schema_version": 1,
    },
    # 21.594 bg_space_partition — W21B-w5 (2026-05-31, D2).
    # shot_projection_card(21.593) 이후 / shot_aware_bg_render_plan(21.595)
    # 이전. per fp_id 로 base_location_dossier checkpoint(base_marker_inventory
    # structural units + per_bg_render_facts_by_bg_id camera-target floors)에서
    # deterministic candidate edge(build_candidate_edges: IDF ubiquity discount +
    # hub 격리 = diagnostic floor, 최종 SOT 아님)를 만들고, borderline edge 를
    # projection-card visible_items 위의 pass-2 edge judge(text LLM, VLM 재호출
    # 없음)로 판정 → space_partition_plan(plate_group / anchor / render_action /
    # ref_tree_parents max2)을 조립한다. 이 plan 이 cross-zone reuse 의 LLM SOT —
    # wave-4 geometry/band PLATE_PARTITION / REFERENCE_DAG mirror 는 candidate /
    # diagnostic 으로 demote(D1). edge judge 의 R3 precondition = 양쪽 bg 가 real /
    # pass / non-synthetic / visible_items 있는 card 일 때만 판정(아니면 skipped,
    # strong parent 절대 금지). R2 strong-parent gate = same_space && conf>=0.75 &&
    # shared_distinctive non-empty(validator 계산, free text 의미판단 0). cost cap
    # (Codex lock ⑤) = per-fp edge cap + global LLM-call cap, 초과 edge 는 skipped
    # (cap reason) 기록(silent drop / auto-merge 금지). geometry presence 는 per-fp
    # hard precondition. opt-in: settings.bg_space_partition_enabled (default
    # False) + base_location_dossier_enabled=True + dossier checkpoint 존재 시
    # active. real provider selector OFF (default) 시 모든 edge skipped
    # (provider_disabled) → strong edge 0 → bg 별 own plate, real LLM 미호출.
    # default 경로 영향 0 (no checkpoint, no consumer change). shot_aware_bg_render
    # _plan hard depends_on 엔 넣지 않는다 (plan 이 partition 을 소비하는 건 A 배선).
    "bg_space_partition": {
        "label": "BG 공간분할 — plate group / anchor / ref DAG SOT (W21B-w5)",
        "category": "analysis",
        "order": 21.594,
        "default_model": "gpt",  # provider OFF default → 미호출
        "provider": "openai",
        # hard deps = dossier(candidate edge 입력) / geometry(per-fp hard
        # precondition) / shot_projection_card(edge judge visible_items 시드).
        # background_master_plan 은 직접 읽지 않는다 (bg_ids 는 dossier 의
        # per_bg_render_facts_by_bg_id 에서 나오고, master_plan readiness 는
        # dossier 가 이미 transitive 로 보장) → depends_on 에 넣지 않음.
        "depends_on": [
            "base_location_dossier",
            "floor_plan_geometry_readback",
            "shot_projection_card",
        ],
        "fan_out": False,
        "applicability": "if_background_mode",
        "step_type": "transform",
        "lifecycle": "active",
        # bg_space_partition_step.py 의 SCHEMA_VERSION 와 동기 의무.
        "schema_version": 1,
    },
    # 21.5945 dwelling_zone_map — W21B (2026-06-08).
    # shot_projection_card(21.593) / bg_space_partition(21.594) 이후 /
    # shot_aware_bg_render_plan(21.595) 이전. FP-이미지 + VLM vision 으로
    # 같은 dwelling 의 bg 들을 zone 으로 묶는다 (edge-judge 우회 — edge-judge
    # 는 같은 dwelling 의 다른 카메라각 bg 를 confidence 로 못 묶어 cross-angle
    # pair 가 strong gate 밑 adjacent_related 로 떨어지는 한계). per fp_id 로
    # interior dwelling 의 bg 를 모아: ①per-bg 구조 묘사 조립(우선순위:
    # shot_projection_card plate prose → freshness 검증된 background_prompt t2i →
    # master_plan catalog; dossier 구조 facts enrichment 첨부) ②구조 SOT=
    # floor_plan_prompt.space_model(전체 dwelling 모든 zone) → clean 흑백 2D FP
    # prompt(gpt-5.5) → NEW clean plan 렌더(gpt-image-2 text-to-image; 왜곡된
    # floor_plan_render PNG 재사용 안 함) → gpt-5.5 VISION 이 각 bg 를 zone +
    # grid focus 로 매핑 ③deterministic join → zone-map contract (zones +
    # bg/shot zone assignments) + SEPARATE annotated(번호 overlay,
    # must_not_be_used_for_render) FP + edge-judge diagnostic 비교. CLEAN
    # 렌더 ref 와 ANNOTATED(번호) ref 는 분리 — 번호누출 가드. zone_map 이
    # grouping SOT, bg_space_partition 은 diagnostic 으로 비교만(이 step 은
    # 수정 안 함 — consumer 재배선은 Phase 2). Phase 1 = contract 산출/검증만
    # (렌더 배선 없음). opt-in: settings.dwelling_zone_map_enabled (default
    # False) + floor_plan_prompt_version ∈ {"6","7"}. real_provider selector
    # OFF (default) 시 dwelling 마다 synthetic fixture (LLM/VLM/image 0). default
    # 경로 영향 0 (not_applicable cp 만). shot_aware_bg_render_plan hard
    # depends_on 엔 넣지 않는다 (consumer 배선은 Phase 2). dwelling_zone_map_step.py
    # 의 SCHEMA_VERSION 와 동기 의무.
    "dwelling_zone_map": {
        "label": "dwelling zone map — FP+VLM 공간 묶기 (W21B)",
        "category": "analysis",
        "order": 21.5945,
        "default_model": "gpt",  # real_provider OFF default → 미호출
        "provider": "openai",
        # hard = master_plan(bg catalog / shot binding) + floor_plan_prompt(spec).
        # soft (enrichment / diagnostic, 누락해도 동작): dossier / geometry /
        # shot_projection_card(plate prose) / bg_space_partition(edge-judge 비교).
        # background_prompt 는 hard dep 아님 — t2i prose 는 있으면 쓰고 없으면
        # projection / catalog 로 degrade (order-robust).
        "depends_on": [
            "background_master_plan",
            "floor_plan_prompt",
            "base_location_dossier",
            "floor_plan_geometry_readback",
            "shot_projection_card",
        ],
        "fan_out": False,
        "applicability": "if_background_mode",
        "step_type": "transform",
        "lifecycle": "active",
        "schema_version": 1,
    },
    # 22.5 space_set_bg — W21B (2026-06-10). frame/addon/life 3층 공간 세트
    # BG 새 경로 (실험 geum_fp_frame_addon_exp 14회 육안 반복 검증의 production
    # 포팅, 사용자 합의). 장소 그룹당: analyze(frame=경계·개구부·access path·
    # construction_character / addon=씬 증거 핵심 가구→FP 에 그림 / life_baseline=
    # 인물 상황→환경 추론 고정물→FP 미표기·BG 생성시만) → place_desc whole/
    # indoor/outdoor 분리 → 실내: frame 2D(t2i)→addon i2i=최종 2D FP→VLM 좌표
    # →★공간당 단일 번호 마킹★(PIL)→마킹 사본 base BG i2i / 옥외(open_air):
    # FP 미경유 공간당 단 한 장 T2I 기준 BG / threshold=connector(plate 생략) /
    # same_space=canonical 사진 참조. ★3D FP 단계 없음(실험 비교로 손실 변환
    # 판정·제거). frame_check 는 VLM 진단(게이트 아님). opt-in:
    # settings.space_set_bg_enabled (default False) → OFF 시 not_applicable 만,
    # 기존 FP/BG 체인 영향 0. 하류(scene_image) 배선은 Phase 2.
    #
    # category=image / order 22.5 (2026-06-10 fresh full E2E 실측 fix,
    # Codex MOVE_TO_IMAGE_CATEGORY_APPROVED_WITH_ORDER_NARROW):
    # 원래 analysis/21.5946 이었으나 hard dep world_guide(image, 22)가 순서상
    # 미래를 가리켜 fresh category=all run 에서 항상 blocked ('선행 단계 미완료').
    # world_guide 직후 asset-prep 슬롯 — ref_image_gen(23)과는 독립이며 단지
    # background_render/scene_image_pipeline 전에만 완료되면 된다(shot_plate_map
    # overlay 가 scene_image 의 background ref 로 소비). image category 이동으로
    # 기존 CAVEAT(analysis category 의 gpt-image-2 호출이 image budget gate 밖)도
    # 해소 — image/all dispatch 의 cap·approve 관할로 정상 편입.
    "space_set_bg": {
        "label": "space set BG — frame/addon/life 3층 공간 세트 (W21B)",
        "category": "image",
        "order": 22.5,
        "default_model": "gpt",  # default OFF → 미호출
        "provider": "openai",
        # hard = master_plan(group/loc 묶음) + world_guide(시각 정체성)
        # + scene_save/shot_validator/shot_selection(_select_scenes_for_group
        # 직접 소비 — force/수동 정리 상황에서도 gate 가 부재를 보호, Codex 리뷰 #4).
        "depends_on": [
            "background_master_plan",
            "world_guide",
            "scene_save",
            "shot_validator",
            "shot_selection",
            "scene_director",
        ],
        "fan_out": False,
        "applicability": "if_background_mode",
        "step_type": "asset",  # image category 불변식 (test_image_steps_are_asset)
        "lifecycle": "active",
        "schema_version": 1,
    },
    # 21.595 shot_aware_bg_render_plan — W20B (2026-05-27).
    # dwelling-scoped reference graph DAG + per-bg camera_decision +
    # reference_decision. LLM planner step; mock/dry 중심 구현 + 실
    # LLM call 0 (default OFF + production llm_provider=None). consumer
    # = future W20C+ (HTML inspectable surface 는 W20A2 가 이미 제공,
    # W20B 는 plan/checkpoint/validation 까지). 입력: dossier + geometry +
    # overlay + master_plan + shot_staging. validators:
    # DAG / same-fp / max_refs<=2 / two-refs distinct physical_space_id /
    # camera-cell ∈ candidate sets / exactly-one anchor / non-empty
    # rationale / synthetic readback → production_clear=False. opt-in:
    # settings.shot_aware_bg_render_plan_enabled (default False) + 4
    # upstream opt-in flags 모두 True 일 때만 active. default 경로 영향 0.
    # W20D (2026-05-27): per-node render_guidance (5 directive strings)
    # 추가; prompt pack v2 = 1.202605271800, schema_version 1->2.
    "shot_aware_bg_render_plan": {
        "label": "shot-aware BG render plan (W20B)",
        "category": "analysis",
        "order": 21.595,
        "default_model": "gpt",  # llm_provider=None default → 미호출
        "provider": "openai",
        "depends_on": [
            "base_location_dossier",
            "floor_plan_geometry_readback",
            "floor_plan_overlay_payload",
            "background_master_plan",
            "shot_staging",
        ],
        "fan_out": False,
        "applicability": "if_background_mode",
        "step_type": "transform",
        "lifecycle": "active",
        # W21B-w3 (2026-05-29): schema_version 7 = wave-3 전체 persisted
        # shape. Commit 1 = v2 prompt pack (same_physical_space_view /
        # related_style_new_space mode split + same-space low-delta reuse
        # candidate 신호). Commit 2a = checkpoint graph node 에 canonical
        # render_action / reuse_target_bg_id router output 추가. 1·2a 한
        # schema 세대(중간 bump 없음). stale cascade.
        # shot_aware_bg_render_plan_step.py 의 SCHEMA_VERSION 와 동기 의무.
        # W21B-w4 C3 (2026-05-31): plan vNext stamps additive per-node
        # projection-card fields (anchor_shot_id / projection_card_* )
        # consuming the shot_projection_card checkpoint (default OFF →
        # not_available). camera_decision demoted (NOT deleted). Persisted
        # node shape changed → 7→8.
        # W21B-w4 3a/3b (2026-05-31): card-aware router
        # (route_render_actions_v2 D5 horizontal-band withhold) + two additive
        # deterministic mirrors of the canonical render_action SOT —
        # mirror_plate_partition (PLATE_PARTITION_FIELDS) and
        # build_reference_dag (REFERENCE_DAG_FIELDS). (A) decision: both run
        # unconditionally (card OFF → geometry-only), so the persisted node
        # shape always carries these fields → 8→9. Allowlist 밖이라
        # _check_cp_mismatch 가 BLOCK (운영자 force 의무, 자동 rerun X).
        # W21B-w5 STEP4 (2026-05-31): A wiring — ⑧ apply_space_partition_plan
        # canonical normalizer runs last, consuming the bg_space_partition
        # (21.594) space_partition_plan. A usable plan OVERRIDES the whole
        # canonical render surface from the LLM partition SOT and demotes the
        # wave4 geometry route/mirror/DAG to geometry_*_diagnostic; default
        # (no plan) → no-op + render_action_source=geometry_route +
        # partition_fallback_reason. Persisted node shape changed → 9→10.
        # shot_aware_bg_render_plan_step.py 의 SCHEMA_VERSION 와 동기 의무.
        "schema_version": 10,
    },
    # 21.60 background_prompt — Phase 7 Step 5. master_plan의 backgrounds[]별 t2i prompt 생성.
    # depends_on_bg DAG → compute_dag_levels로 level batch 병렬. floor_plan_render(21.55) 직후
    # 위치하여 fp PNG 경로를 ref로 전달 가능. 이 step은 prompt만 생성 — PNG는 background_render(T14).
    # category=analysis (LLM text 호출). chain_bg_render(24.7)보다 앞에 위치.
    "background_prompt": {
        "label": "배경 t2i 프롬프트",
        "category": "analysis",
        "order": 21.60,
        "default_model": "gpt",
        "provider": "openai",
        # schema_version: background_prompt_step.SCHEMA_VERSION 와 일치 필수.
        # G3.2 (2026-05-04): 1→2 명시 (objects_owned_by_background field 추가).
        # D6 v6 (2026-05-09): 2→3 (bg_id pattern → BG_ID_RE + state_label_raw 통합).
        "schema_version": 3,
        "depends_on": [
            "background_master_plan", "floor_plan_render",
            # W19B-2 (2026-05-26): v7 opt-in 시 floor_plan_overlay_payload 가
            # required producer (background_prompt_step._execute 내 selector
            # "7" 분기에서 cp 직접 로드). v6 default path 는 producer 가
            # not_applicable 이어도 동작 — manifest dep 추가는 ordering 만 강제.
            "floor_plan_overlay_payload",
            "scene_save", "shot_validator", "shot_selection",
            "visual_world_rules",
        ],
        "fan_out": False,
        "applicability": "if_background_mode",
        "step_type": "transform",
        "lifecycle": "active",
    },
    # 21.65 episode_reference_policy — scene_detail 직전 reference necessity
    # manifest. deterministic (LLM 없음). render_prompt_card producer/verify 가
    # 동일 immutable manifest 를 읽어 text-only subject 의 identity policy 를
    # generic_descriptor_allowed 로 다운그레이드 → reference 과생성 차단.
    # 설계: docs/reference-necessity/index.html.
    "episode_reference_policy": {
        "label": "에피소드 참조 정책",
        "category": "analysis",
        "order": 21.65,
        "default_model": "gpt",   # 미사용 (deterministic) — manifest 형식상 필수
        "provider": "openai",
        "depends_on": [
            "shot_director", "shot_selection", "shot_validator",
            "entity_merge", "entity_relation",
        ],
        "fan_out": False,
        "applicability": "always",
        "step_type": "transform",
        "lifecycle": "active",
        "schema_version": 1,
    },
    # 21.66 visual_continuity_anchor — W21B-W7 (2026-06-12).
    # ★v1 production scope = printed_prop(D) anchors 전용 — zoom continuity(C)
    # 는 zoom_continuity_anchor(21.73)로 이동 (W-C1, Codex C3 변형: refined
    # ref_usage 가 21.71 에만 실재해 21.66 은 fresh run 에 C seed 0 — SOT 1곳
    # 원칙). 이 step 의 zoom/exact 후보는 diagnostics 로만 기록.
    # seed deterministic 탐지(D=prop short_id 의 selected-shot 반복 + framing
    # structured-enum priority score) → 그룹당 1콜 LLM anchor 추출(gpt-5.5,
    # 씬 원문 전체) → immutable anchor manifest cp + review_html 갤러리.
    # consumer (별도 wave): W-B=ref_image_gen(23) prop ref prompt overlay +
    # scene_detail scale 계약 / W-C=scene_detail wide 계약 + zoom crop/i2i-fill.
    # episode_reference_policy(21.65) 의 deterministic immutable manifest 선례.
    # opt-in: settings.visual_continuity_anchor_enabled (default False) — OFF 시
    # not_applicable cp 만, default 경로 영향 0. cap 초과 seed 는
    # skipped(reason="cap") 진단 (silent drop 금지). 설계:
    # docs/w21b-w7-visual-continuity-anchor-production-brief-20260612/.
    "visual_continuity_anchor": {
        "label": "시각 연속성 anchor (W21B-W7)",
        "category": "analysis",
        "order": 21.66,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": [
            "scene_save",          # 씬 원문 전체 (LLM 입력, 자르지 않음)
            "shot_validator", "shot_selection",
            "shot_dependency",     # C seed ordering (refined cp 는 soft 역참조)
            "shot_staging",        # 멤버 샷 staging + framing_scale enum
            "shot_director",       # D seed: per-shot visible_entity_ids
            "entity_t2i",          # 현 canon t2i_prompt (prop + 인물)
        ],
        "fan_out": False,
        "applicability": "if_visual_continuity_anchor_enabled",
        "step_type": "transform",
        "lifecycle": "active",
        # 2-pass 역참조: refined ref_usage(zoom_in_detail) 는 하류
        # shot_dependency_t2i(21.71) cp 에만 실재 — W-B 에서 scene_detail 이
        # 이 step 에 depends_on 하면서 shot_dependency_t2i 가 재귀 downstream
        # 에 들어와 consumes_downstream 불변식 충족 + force cascade 시 파일
        # 보존 (scene_detail 의 동일 선례).
        "consumes_downstream": ["shot_dependency_t2i"],
        # visual_continuity_anchor_step.py 의 SCHEMA_VERSION 와 동기 의무.
        "schema_version": 1,
    },
    # 19.55 background_planner — Phase 5 (v3 plan, 2026-04-29).
    # 단일 LLM 호출로 frequency 분석 + building group + floor_plan_order/chain_bg_order
    # 결정 + prev_shot_only fallback. set_design / location-loop chain_bg_planning 교체.
    # toggle: settings.background_mode == 'floor_plan_anchored'
    #   → applicability='if_floor_plan_mode'. off/chain_only 모드에선 자동 not_applicable.
    # 19.55: shot_essence(19.7) 직전이 아니라 chain_bg_planning(19.6) 직전에 위치 — Phase 5
    # downstream(chain_bg_planning, location_floor_plan, chain_bg_render)이 이 결과를 소비.
    # DEPRECATED by Phase 7 (T16, 2026-04-29): background_classify(19.50) +
    # background_master_plan(19.52) 가 이 step의 frequency 분석 + grouping +
    # ordering 책임을 분리해 대체한다. 코드는 보류 — Phase 7.1 cleanup PR에서
    # 별도 삭제 예정.
    "background_planner": {
        "label": "배경 생성 Planner (deprecated, Phase 7로 대체)",
        "category": "analysis",
        "order": 19.55,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": [
            "shot_validator", "shot_selection", "scene_director",
            "entity_merge", "entity_detail", "visual_world_rules",
        ],
        "fan_out": False,
        "applicability": "disabled",  # Phase 7 deprecation
        "step_type": "transform",
        "lifecycle": "deprecated",  # Phase 7 deprecation
    },
    # 19.6 background_chain_planning — set_design 교체용 신규 step (2026-04-27).
    # on_demand: 자동 실행 X. 사용자가 명시 트리거할 때만.
    # 새 background_chain_render(별도 step)이 이 결과를 입력으로 받아 실제 PNG 생성.
    # DEPRECATED by Phase 7 (T16, 2026-04-29): background_master_plan(19.52) +
    # background_prompt(21.60) 이 chain 플래닝 + per-bg t2i prompt 책임을 분리해
    # 대체한다. 코드는 보류 — Phase 7.1 cleanup PR에서 별도 삭제 예정.
    "background_chain_planning": {
        "label": "배경 chain 플래닝 (deprecated, Phase 7로 대체)",
        "category": "analysis",
        "order": 19.6,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": [
            "shot_validator", "shot_selection", "shot_staging",
            "scene_director", "entity_merge", "entity_detail",
            "visual_world_rules",
        ],
        "fan_out": False,
        "applicability": "disabled",  # Phase 7 deprecation
        "step_type": "transform",
        "lifecycle": "deprecated",  # Phase 7 deprecation
    },
    # 21. 교차 검증 (병렬, 앞2씬 컨텍스트)
    "scene_verify": {
        "label": "교차 검증 (레거시)",
        "category": "analysis",
        "order": 21,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["scene_detail"],
        "fan_out": True,
        "requires_projection_sync_before_run": True,  # W1-F12
        "applicability": "disabled",
        "step_type": "editorial",
        "lifecycle": "deprecated",
    },

    # ── 이미지 Phase (5단계) ──

    "world_guide": {
        "label": "월드 가이드",
        "category": "image",
        "order": 22,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["entity_t2i", "scene_detail"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "asset",
        "lifecycle": "active",
    },
    "ref_image_gen": {
        "label": "요소 참조 이미지",
        "category": "image",
        "order": 23,
        "default_model": "gemini-image",
        "provider": "gemini",
        "depends_on": ["entity_t2i"],
        "fan_out": True,
        "applicability": "always",
        "step_type": "asset",
        "lifecycle": "active",
    },
    "composite_image_gen": {
        "label": "인물+아웃룩 합성 이미지",
        "category": "image",
        "order": 24,
        "default_model": "gemini-image",
        "provider": "gemini",
        "depends_on": ["ref_image_gen", "outlook_phase3"],
        "fan_out": True,
        "applicability": "if_has_outlooks",
        "step_type": "asset",
        "lifecycle": "active",
    },
    "character_state_variant": {
        "label": "인물 상태 변형 참조 이미지",
        "category": "image",
        "order": 24.5,
        "default_model": "gemini-image",
        "provider": "gemini",
        "depends_on": ["composite_image_gen", "shot_staging"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "asset",
        "lifecycle": "active",
    },
    # 24.72 background_render — Phase 7 Step 6. master_plan backgrounds[]별 PNG 생성
    # (gpt-image-2). depends_on_bg DAG → compute_dag_levels로 level 분할 후
    # level 내부 ThreadPool 병렬. ref 우선순위: floor_plan_render PNG 1순위 +
    # 이전 background PNG 2순위. 출력 shape는 Phase 5 chain_bg_render와 호환:
    # data.groups[bg_id].{status, location_id, png_path, t2i_prompt, shot_guides,
    # shot_ids, parent_id, ref_used, ...} → Phase 6 scene_context_loader 변경 0.
    # order=24.72: Phase 5 chain_bg_render(24.7) 와 collision 회피.
    # category=image / step_type=asset.
    "background_render": {
        "label": "배경 이미지 생성",
        "category": "image",
        "order": 24.72,
        "default_model": "gpt",
        "provider": "openai",
        # schema_version: background_render_step.SCHEMA_VERSION 와 일치 필수.
        # Phase 9.1: 1→2 (data.groups[bg_id].camera_recommendations 추가).
        # D6 T5c + T5-fix B2: 2→3 (data.consumed_bg_catalog_hash +
        # data.consumed_shot_binding_hash 추가). _LEGACY_SCHEMA_BUMP_ALLOWLIST 외라
        # 운영자 명시 force 의무 (`step_runner.py:763-769`).
        "schema_version": 3,
        "depends_on": [
            # W19B-3 (2026-05-26): w18j_overlap opt-in path 는
            # background_render_step._execute 안에서 floor_plan_overlay_payload
            # cp 를 직접 로드 → manifest dependency 도 명시. legacy default
            # path 는 overlay step 이 not_applicable 이어도 동작 (cp 부재여도
            # legacy branch 진입 X 영향 0). 추가 의도: pipeline ordering /
            # applicability 가 producer 와 consumer 를 정확히 묶도록.
            "floor_plan_overlay_payload",
            "background_prompt", "floor_plan_render", "background_master_plan",
        ],
        "fan_out": False,
        "applicability": "if_background_mode",
        "step_type": "asset",
        "lifecycle": "active",
    },
    # 24.7 background_chain_render — chain plan → 노드별 PNG (gpt-image-2).
    # on_demand: 사용자 명시 트리거만. PR #5 머지 후 scene_image_pipeline의
    # 첫 번째 ref 슬롯에 노드 PNG가 주입될 예정.
    # DEPRECATED by Phase 7 (T16, 2026-04-29): background_render(24.72) 가
    # 동일한 data.groups shape으로 출력하면서 도면 ref + level 병렬 처리를 통합
    # 수행해 이 step을 대체한다. 코드는 보류 — Phase 7.1 cleanup PR에서 별도
    # 삭제 예정.
    "background_chain_render": {
        "label": "배경 chain 렌더 (deprecated, Phase 7로 대체)",
        "category": "image",
        "order": 24.7,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["background_chain_planning", "location_floor_plan"],
        "fan_out": False,
        "applicability": "disabled",  # Phase 7 deprecation
        "step_type": "asset",
        "lifecycle": "deprecated",  # Phase 7 deprecation
    },
    "scene_image_pipeline": {
        "label": "씬 이미지 생성 (compound)",
        "category": "image",
        "order": 25,
        "default_model": "mixed",
        "provider": "mixed",
        # background_render 추가 (Phase 9.2 사고 fix): scene 이미지가 chain_bg를
        # ref로 사용하므로 dep 명시. background_render가 not_applicable
        # (BACKGROUND_MODE=off)이면 dispatcher가 자동 skip — 위반 없음.
        # 레시피 3스텝 추가 (2026-07-13 Codex 1차 리뷰 BLOCKING-1 fix):
        # still_recipe_mode=v1 소비 데이터 생산자 완료 보장. OFF 시
        # not_applicable=satisfied 라 기존 파이프 무해.
        "depends_on": [
            "composite_image_gen", "world_guide", "character_state_variant",
            "background_render",
            "shot_ref_classify", "shot_continuity", "shot_conti_light",
            # E2E10 fix④ Codex BLOCKING-1: 스틸 서비스가 share_plan CP 를
            # prev 상위 권위+groupbg(장소 단위 배경) 묶음 SOT 로 직접 소비
            # — share plan force/rerun 후 downstream stale 전파 보장.
            # flag OFF=not_applicable=satisfied 라 정적 추가 안전.
            "background_share_plan",
        ],
        "fan_out": True,
        "sub_steps": [
            {"id": "prompt_translation", "label": "T2I 번역", "default_model": "gpt-mini", "provider": "gemini"},
            {"id": "scene_t2i_gen", "label": "T2I 이미지 생성", "default_model": "gemini-image", "provider": "gemini"},
            {"id": "scene_t2i_validation", "label": "이미지 검증", "default_model": "gpt", "provider": "openai"},
            {"id": "prompt_sanitize", "label": "프롬프트 안전화", "default_model": "gpt", "provider": "openai"},
            {"id": "angle_recommend", "label": "앵글 추천", "default_model": "gpt", "provider": "openai"},
            {"id": "fal_angle_apply", "label": "fal.ai 앵글 적용", "default_model": "fal-ai", "provider": "fal"},
            {"id": "final_select", "label": "최종 선택", "default_model": "gpt", "provider": "openai"},
        ],
        "applicability": "always",
        "step_type": "asset",
        "lifecycle": "active",
    },

    # ── 보조 (on-demand) ──

    "outlook_dedup": {
        "label": "아웃룩 중복 판별",
        "category": "auxiliary",
        "order": 100,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["outlook_phase3"],
        "fan_out": False,
        "applicability": "on_demand",
        "step_type": "transform",
        "lifecycle": "active",
    },
    "project_summary": {
        # ProjectSummaryStep 클래스 부재 (summary_steps.py 미구현) — Phase 1 리뷰 반영.
        # manifest 항목 유지는 UI legacy 표시 목적. 호출 시 "Step class not found" 404.
        # 실제 기능 재도입 전까지 lifecycle=removed로 자동 실행 경로에서 제외.
        "label": "프로젝트 요약 (미구현)",
        "category": "auxiliary",
        "order": 101,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["episode_summary"],
        "fan_out": False,
        "applicability": "disabled",
        "step_type": "transform",
        "lifecycle": "removed",
    },
}


# ── Helper Functions ──


def get_step_info(step_id: str) -> Optional[Dict[str, Any]]:
    """주어진 step_id의 매니페스트 항목을 반환. 없으면 None."""
    return STEP_MANIFEST.get(step_id)


# W1-F10: 런타임 소비자 (StepRunner / steps API) 가 STEP_MANIFEST dict를 직접 import하지
# 않도록 제공하는 함수 API. step_catalog에서도 re-export되지만, 순환 import 안전을 위해
# 원형은 여기 둔다.


def contains(step_id: str) -> bool:
    """등록된 step_id인지."""
    return step_id in STEP_MANIFEST


def get_manifest_dict(step_id: str) -> Optional[Dict[str, Any]]:
    """dict 형태의 manifest entry (읽기 전용 관례)."""
    return STEP_MANIFEST.get(step_id)


def get_depends_on(step_id: str) -> List[str]:
    """주어진 step_id의 선행 의존 step_id 목록을 반환."""
    info = STEP_MANIFEST.get(step_id)
    if info is None:
        return []
    return list(info.get("depends_on", []))


def get_downstream_steps(step_id: str) -> List[str]:
    """주어진 step_id를 의존하는 직접 하위 step_id 목록을 반환."""
    return [
        sid for sid, info in STEP_MANIFEST.items()
        if step_id in info.get("depends_on", [])
    ]


def get_all_downstream_recursive(step_id: str) -> List[str]:
    """주어진 step_id의 모든 하위 의존 step_id를 재귀적으로 반환 (BFS)."""
    visited: set = set()
    queue = get_downstream_steps(step_id)
    while queue:
        current = queue.pop(0)
        if current not in visited:
            visited.add(current)
            queue.extend(get_downstream_steps(current))
    return sorted(visited)


def get_ordered_steps() -> List[Dict[str, Any]]:
    """order 기준으로 정렬된 매니페스트 항목 리스트 반환. 각 항목에 step_id 키 포함."""
    result = []
    for sid, info in STEP_MANIFEST.items():
        entry = {"step_id": sid, **info}
        result.append(entry)
    result.sort(key=lambda x: x["order"])
    return result
