# Area D-min — prop attach `required_refs` 단일 SOT (design)

- Date: 2026-05-14
- Umbrella: `docs/superpowers/specs/2026-05-12-llm-structured-sot-migration-design.md` v1.2 §4.1 row 4 (audit C9 subset)
- Predecessor: Area B-min (commits `e1f4ab6..e400e9c` + visibility fix `e0bbf38..fe105ea`)
- Successors: Area D-next (C8 keep_elements kind enum), 별도 follow-up patch (U2 location_consistency name fallback)

---

## 1. 본질 / 원칙

### 1.1 본질

Area D-min 은 `scene_reference_service.resolve_refs_for_prompt` 의 **legacy prop attach text matching 분기 (line 549-590) 폐기** patch 다. Area B-min 이 신설한 `render_contracts[] → required_refs_from_render_contracts → asset_requirements.required_refs` producer SOT 가 prop attach 의 **단일 source-of-truth** 가 되도록 consumer 를 격상한다.

새로운 producer / schema / LLM step 신설 없음 — 본 patch 는 **순수 consumer cleanup** + signature cascade.

### 1.2 원칙

- **단일 SOT**: prop attach 결정은 오직 `required_refs` (Area B producer) 에서 온다. t2i_prompt 자유 텍스트의 P## / prop name 매칭은 producer 의 사후-신호로 보고 폐기.
- **silent fallback 금지** (umbrella §3.4 Gate 4): text matching fallback 폐기. `required_refs` 미포함 prop 은 attach 안 됨 (silent skip — visible_entities prop 부재 시 자연스러운 동작과 동일).
- **signature cascade 의무**: `shot_description` parameter 가 prop attach 용도로만 쓰이므로, consumer signature + caller 2 곳 + 내부 변수 모두 동시 제거. deprecated 잔재 금지.
- **Area B reference_required=false 의도된 변화**: LLM 이 `visual_identity.reference_required=false` 라고 emit 한 prop 은 ref attach 안 됨. Area B SOT 신뢰 — 옛 text matching 으로 attach 되던 결과는 회귀가 아님.

### 1.3 Out-of-scope

- **C8** (`scene_reference_service.py:50-107` `_KEEP_ELEMENT_PERSON_TOKENS` regex + `_assert_keep_elements_are_environment_only`) → Area D-next 별도 spec.
- **U2** (`location_consistency_step.py:70-106` name substring fallback) → 별도 follow-up patch (Area D scope 밖).
- character / outlook / location ref binding 경로 (line 437-547) → 변경 없음 (B-min scope 밖).
- `state_variant` ref 분기 (line 443-459) → 변경 없음.

---

## 2. Architecture

### 2.1 현재 wire (Area B-min closure + Patch A Tier 2 결합 상태)

```
[Producer — Area B-min, 이미 신설]
entity_t2i + EntityCanon.metadata_json
  → visible_entity_details[i].metadata_json
  → build_render_contracts(...)  # visible prop ∩ visual_identity.reference_required=true
  → card["render_contracts"]
  → build_asset_requirements(..., render_contracts=...)
     └─ required.extend(required_refs_from_render_contracts(render_contracts))
       → [{"kind":"prop", "id":"P##", "policy":"required"}, ...]
  → card["asset_requirements"]["required_refs"]

[Wiring — coordinator, Patch A Tier 2 이미 wire]
scene_generation_coordinator
  → required_refs = card["asset_requirements"]["required_refs"]
  → reference_svc.resolve_refs_for_prompt_set(
        t2i_prompts, visible_entities, scene_ref_image_map, entity_lookup,
        state_variant_sids=..., shot_description=...,  # ← shot_description 제거 대상
        required_refs=required_refs,
    )

[Consumer — scene_reference_service.resolve_refs_for_prompt, 변경 대상]
  분기 1) char short_id (C##O##) 매칭                       # 유지
  분기 2) legacy [[name]+[outlook]] 매칭                     # 유지
  분기 3) prop text matching (line 549-590)                  # ← 폐기
        (a) P## word-boundary in t2i_prompt + shot_desc
        (b) prop_name ASCII word-boundary / CJK substring
  분기 4) required_refs forced attach (line 592-622)         # ← 단일 prop 경로로 격상
```

### 2.2 변경 후 wire

```
[Producer — 변경 없음, Area B-min 동일]

[Wiring — coordinator signature 변경]
  → reference_svc.resolve_refs_for_prompt_set(
        t2i_prompts, visible_entities, scene_ref_image_map, entity_lookup,
        state_variant_sids=...,           # 유지
        required_refs=required_refs,      # 단일 prop SOT
    )
  shot_description kwarg 제거 (caller 2 곳 다).

[Consumer]
  분기 1) char short_id (C##O##) 매칭                       # 변경 없음
  분기 2) legacy [[name]+[outlook]] 매칭                     # 변경 없음
  분기 3) (폐기 — 본문 완전 제거)
  분기 4) required_refs prop attach                          # 단일 prop attach 경로
```

### 2.3 핵심 invariant

- **prop attach 단일 경로**: 변경 후 `resolve_refs_for_prompt` 의 모든 prop attach 는 `required_refs` 분기에서만 발생. text matching 분기 없음.
- **char/outlook/state_variant 분기 보존**: B-min scope 밖이므로 본 patch 가 건드리지 않음.
- **(labeled_refs, attached_meta) length 일치** (Patch A invariant 1): 모든 attach site 가 동시 append, length match 가드. 변경 후도 동일.
- **dedup 보존** (`_used_ref_ids`): char_id / outlook_id / prop_id 모든 ID 가 _used_ref_ids 에 들어가 double attach 차단. text matching 폐기로 dedup 표면 좁아짐 (prop 은 required_refs forced attach 분기에서만 _used_ref_ids 추가).

---

## 3. Code 변경 위치 (file + line)

### 3.1 `backend/app/services/scene_reference_service.py`

#### 3.1.1 폐기 (line 549-590, 본문 전체)

```python
# 3) prop — visible_entities 등재 prop을 다음 셋 중 하나가 만족하면 inject:
#    (a) t2i_prompt 또는 shot_description 에 short_id (P##) 명시
#    (b) t2i_prompt 또는 shot_description 에 prop name 이 word-boundary 매칭
# ... (주석 + _haystack + _MIN_NAME_LEN + for ve in visible_entities prop loop)
```

전체 제거.

#### 3.1.2 signature 변경

```python
def resolve_refs_for_prompt(
    self,
    t2i_prompt: str,
    visible_entities: list,
    scene_ref_image_map: Dict[str, bytes],
    entity_lookup: Dict[str, Dict],
    state_variant_sids: Optional[Dict[str, Dict]] = None,
    shot_description: Optional[str] = None,  # ← 제거
    *,
    required_refs: Optional[List[Dict[str, Any]]] = None,
) -> Tuple[List[Tuple[str, bytes]], List[Tuple[str, str]]]:
```

`shot_description` kwarg 제거 (default arg + docstring 동시).

`resolve_refs_for_prompt_set` 도 동일 (line 341-377). forwarding kwarg 도 제거.

#### 3.1.3 유지 + 격상 (line 592-622)

`required_refs` forced attach 분기 변경 없음 (이미 정확). 단 주석을 갱신:

```python
# Area D-min: required_refs(kind='prop') 가 prop attach 의 단일 SOT.
# legacy text matching 분기 (옛 line 549-590) 는 Area D-min 에서 폐기.
# Area B render_contracts → required_refs_from_render_contracts → asset_requirements
# → coordinator → 본 keyword arg 까지 producer SOT 일관.
```

### 3.2 `backend/app/services/scene_generation_coordinator.py`

#### 3.2.1 line 321-327 (entity-only batch)

```python
labeled_refs, attached_meta = reference_svc.resolve_refs_for_prompt_set(
    t2i_prompts=...,
    visible_entities=...,
    scene_ref_image_map=...,
    entity_lookup=...,
    state_variant_sids=...,
    shot_description=still_data.get("shot_description"),  # ← 제거
    required_refs=...,
)
```

`shot_description` kwarg 제거.

#### 3.2.2 line 614-620 (target_variations batch)

```python
labeled_refs, attached_meta = self._reference_svc.resolve_refs_for_prompt_set(
    ...,
    shot_description=still_data.get("shot_description") or still_data.get("still_frame_prompt"),  # ← 제거
    required_refs=...,
)
```

`shot_description` kwarg 제거.

### 3.3 Test fixtures + helpers

기존 test 가 `shot_description` kwarg 로 호출하는 site 전수 제거. grep + cascade.

### 3.4 Comment / docstring cleanup

본 patch 의 명시 acceptance — 코드 변경과 함께 다음 영역의 주석/docstring 도 정리:

- `scene_reference_service.py` module-level docstring (line 1-16): `resolve_refs_for_prompt(t2i_prompt, ...)` 의 "prop" 언급이 `required_refs` SOT 로 옮겨졌다는 점 명시.
- `resolve_refs_for_prompt` docstring (line 379-400): "T2I 프롬프트에서 참조 이미지 매칭 — short_id(C01O02) + 레거시([[name]+[outlook]]) 동시 지원" → text matching path 폐기 반영. prop 분기는 `required_refs` SOT 명시.
- `resolve_refs_for_prompt_set` docstring (line 341-363): join 위임 설명에서 prop text matching 함의 제거.
- 함수 안 inline 주석 (옛 line 549-590 의 "(a) ... short_id 명시 / (b) ... word-boundary 매칭" 설명 블록) → 본문과 함께 완전 제거.
- 폐기되는 변수 (`_haystack`, `_shot_desc`, `_MIN_NAME_LEN`) 주석도 본문과 함께 제거.

→ acceptance criteria (§7) 항목 1 에 "본문 + 관련 주석/docstring 동시 제거" 명시.

---

## 4. 4 Gate compliance (umbrella §3)

### 4.1 Gate 1 — Semantic Regex Ban

본 patch 는 regex / _PATTERNS / _TOKENS 신규 0. `_KEEP_ELEMENT_PERSON_TOKENS` (line 50) 은 C8 영역 — D-min scope 밖. legacy regex 제거 (text matching `_re.search(rf'\b{...}\b', _haystack)`) 가 net 변화. **PASS**.

### 4.2 Gate 2 — Prompt Closed-List Ban

prompts/_base/ 변경 0. **PASS** (무관).

### 4.3 Gate 3 — Structured SOT Required

prop attach SOT 는 Area B-min 의 `render_contracts[].requirements[].dimension == "visual_identity"` + `operation == "preserve"` + `reference_policy == "use_entity_reference"` 조합 → `required_refs[i].kind == "prop"` enum. 본 patch 가 그 SOT 만 신뢰. **PASS**.

### 4.4 Gate 4 — No Silent Fallback

text matching fallback 폐기. **`required_refs` 에 없는 prop 은 attach contract 가 없으므로 attach 하지 않는다 — 이는 fallback 이 아니라 contract absence 처리다.** visible_entities prop 이 LLM 에 의해 reference_required=false 로 emit 되면 Area B 가 `render_contracts` 에서 누락 → `required_refs` 미발생 → contract 없음 → attach 안 함. silent fallback (LLM SOT 무시하고 code 가 자체 판단) 이 아님. **PASS**.

`required_refs` 가 emit 됐는데 visible_entities 에 그 prop 이 없는 경우 (sid 매칭 실패) → Patch A Tier 1 validator 가 이미 image stage 직전 fail-fast (`RefContractError`). 본 patch 는 그 layer 영향 없음.

---

## 5. Test scenarios (acceptance)

본 patch 의 test 는 `backend/tests/services/test_scene_reference_service.py` + `backend/tests/services/test_scene_generation_coordinator.py` (signature cascade 검증) 에 추가/수정.

### 5.1 신규 test (D-min 회귀 가드)

1. **T1**: `t2i_prompt` 에 `P05` 박혀 있어도 `required_refs=[]` 이면 prop attach 0 — text matching 폐기 검증.
2. **T2**: `required_refs=[{"kind":"prop", "id":"P05"}]` 면 `t2i_prompt` 에 P05 없어도 attach 됨 — forced attach 검증.
3. **T3**: prop name "cup" 이 visible_entities 에 있고 t2i_prompt 에 "cupboard" 있어도, `required_refs` 없으면 attach 0 — legacy substring false-positive path 삭제 회귀 가드.
4. **T4**: prop name 한국어 "잔" 이 t2i_prompt 에 substring 등장해도 `required_refs` 없으면 attach 0 — CJK substring path 삭제 회귀 가드.
5. **T5**: `_used_ref_ids` dedup invariant — `required_refs` 가 같은 P05 를 중복 포함해도 single attach (invariant 보존).
6. **T6**: `(labeled_refs, attached_meta)` length 일치 (Patch A invariant 1) — 모든 시나리오에서 검증.

### 5.2 cascade test

7. **T7**: `resolve_refs_for_prompt_set` (multi-variation) 가 `required_refs` 만으로 동일 동작 — text matching 분기 없이도 모든 variation 이 동일 ref set 공유.
8. **T8**: signature cascade — `shot_description` kwarg 가 사라진 후 `resolve_refs_for_prompt` 호출 site (`coordinator` 2 곳 + test fixtures) 회귀 0.

### 5.3 broader regression

- Area A (`directionality_class`) 회귀 0.
- Area B (`render_contracts` producer + asset_requirements) 회귀 0.
- char short_id (분기 1) + legacy `[[name]+[outlook]]` (분기 2) 회귀 0.
- state_variant 분기 회귀 0.

---

## 6. Out-of-scope (carry note)

### 6.1 C8 (Area D-next 별도 spec)

`scene_reference_service.py:50-107` 의 `_KEEP_ELEMENT_PERSON_TOKENS` regex + `_assert_keep_elements_are_environment_only`. 이 영역은 producer schema 변경 (shot_dependency_t2i `keep_elements: List[str]` → `List[{label, kind: enum}]`) 이 필요해 D-min scope 밖. 별도 spec:

- `docs/superpowers/specs/2026-05-14-area-d-next-keep-elements-kind-sot.md`
- D-min closure + push 후 진입.

### 6.2 U2 (별도 follow-up)

`backend/app/core/steps/location_consistency_step.py:70-106` 의 `present_entity_ids` 부재 시 name substring fallback. spec §2.5 권고 = "fail-fast 우선". scene_reference_service 와 모듈 다름 — D-min scope 밖.

D-next 와 별개 별도 follow-up patch 로 처리.

---

## 7. Acceptance criteria (closure)

1. `scene_reference_service.py` line 549-590 본문 + 관련 주석/docstring 동시 제거 (`_haystack` / `_MIN_NAME_LEN` / `for ve in visible_entities` prop loop + module/function docstring 의 prop text matching 설명, §3.4 cleanup 영역).
2. `resolve_refs_for_prompt` / `resolve_refs_for_prompt_set` signature 에서 `shot_description` kwarg 제거.
3. `scene_generation_coordinator.py` 의 caller 2 곳 (line 321-327, line 614-620) 에서 `shot_description=` kwarg 제거.
4. Test fixtures + helpers 전수 `shot_description` kwarg 정리 (회귀 0).
5. §5 test scenarios T1-T8 PASS.
6. broader regression (Area A + Area B + char/legacy/state_variant 분기) 0.
7. **Hard gate canary**: resolver contract canary — `required_refs` 있음 → attach, `required_refs` 없음 + prompt 에 P##/prop name 있음 → attach 0. unit/integration test 영역에서 검증.
8. **Booster canary** (hard gate 아님): PID `02829fe8` ep1 fresh scene_image_pipeline force 실행 — `render_contracts` 단일 SOT 하에서 image regen 의 시각 결과 확인. failure 시 D-min closure 차단 X — 별도 결함 영역으로 격리.
9. Codex review APPROVED (Gate 1-4 + dual review 통과).

---

## 8. Risk + carry (Area B-min lesson 흡수)

### 8.1 signature cascade risk (Area B-min lesson 1)

`shot_description` kwarg 제거 시 caller 2 곳 + test fixtures + monkeypatch site 전수 grep 의무. 누락 시 TypeError 또는 unused kwarg 가 production silent drift.

→ patch 안에 grep evidence 첨부 의무 (plan task 단위).

### 8.2 dedup symmetry (Area B-min lesson)

`_used_ref_ids` set 은 분기 1/2/4 모두에서 append site 와 함께 갱신. 분기 3 폐기 후 dedup 표면 좁아짐. 하지만 분기 4 (required_refs) 가 `_used_ref_ids` 검사 + 추가 모두 보존하므로 invariant 유지.

### 8.3 visible_entities prop 의 LLM 누락 case

LLM 이 prop entity 의 `metadata_json.visual_identity.reference_required` 를 false 로 emit 하면, Area B 가 `render_contracts` 에 포함 안 함 → `required_refs` 에 포함 안 함 → ref attach 안 됨. 옛 코드는 t2i_prompt 에 prop name 박히면 text matching 으로 attach 했음.

**이는 회귀가 아닌 의도된 변화**. Area B SOT 신뢰. LLM 이 reference_required 를 잘못 판단하면 Area B 영역 결함이며 D-min scope 밖. Tier 1/3 validator (image stage) 는 변경 없음.

### 8.4 Codex 외부 review 의무 (Area B-min lesson)

closure 직전 외부 Codex review 의무. plan 의 마지막 Task 에 외부 review checkpoint 명시.

---

## 9. Self-review

### 9.1 placeholder scan

- "TBD" / "TODO" 0. 모든 결정 §1.3 / §3 / §5 / §6 에 확정.

### 9.2 internal consistency

- §1.2 "단일 SOT" ↔ §2.2 wire diagram ↔ §3 code 변경 위치 ↔ §5 test scenarios — 일관.
- §4.4 Gate 4 "silent skip" 정책 ↔ §8.3 의도된 변화 — 일관.
- §6 Out-of-scope (C8/U2) ↔ §1.3 Out-of-scope — 일관.

### 9.3 scope

- D-min 은 consumer cleanup + signature cascade 만. producer / schema / LLM step 변경 없음.
- 단일 file 본문 변경 ≈ 50 라인 + caller 2 곳 + test cascade. 단일 plan 으로 closure 가능 크기.
- C8 / U2 별도 spec — scope drift 차단.

### 9.4 ambiguity

- shot_description "완전 제거" 의 정확한 의미 (§1.2 + §3.2): kwarg + 내부 변수 + caller + test fixture 전수 제거. deprecated 잔재 0.
- "required_refs 단일 SOT" 의 의미 (§1.2 + §2.2): prop attach 분기 한정. char/outlook/state_variant 는 별도 SOT 유지.

---

## 10. 함정 carry (Area B-min + 본 spec 신규)

1. **signature cascade 의무** (Area B-min lesson): kwarg 제거 시 caller + test + monkeypatch 전수 grep. 부분 제거 시 TypeError 또는 silent drift.
2. **dedup invariant** (Patch A invariant 1): `(labeled_refs, attached_meta)` length 일치. prop attach 분기 폐기 후도 분기 4 (required_refs) 가 append site dedup + length 보장.
3. **visible_entities prop 의 LLM SOT 신뢰**: reference_required=false 인 prop 은 attach 안 됨 (§8.3). 옛 코드의 text matching 결과 회복 안 함. Area B SOT 영역.
4. **scope drift 차단**: C8 (keep_elements regex) 와 U2 (location_consistency) 는 D-min 본문 변경 0. 별도 patch.
