# Empirical Challenge Report: Milestone M3 Iteration 2 Gate Verification

**Agent**: Challenger 1 (`challenger_m3_iter2_1`)  
**Role**: Adversarial Challenger & Specialist (Critic)  
**Milestone**: M3 Iteration 2 Gate Verification (Start Frame Resolution, Cross-Character Leakage & Retake Loop)  
**Parent Orchestrator**: `97faf5e5-a830-491c-b78c-2af12175badf`  
**Verdict**: **REQUEST_CHANGES**  
**Date**: 2026-10-09  

---

## 1. Observation

### 1.1 Instruction 1: Live Probe Verification (`test_m3_challenger1_probe.py`)
- Executed `python -m pytest tests/test_m3_challenger1_probe.py -v`.
- **Result**: `10 passed in 14.69s` (exit code 0).
- `test_empirical_defect_scene03_thuy_van_shots_leak_thuy_kieu` was confirmed un-xfailed (lines 190–205 of `tests/test_m3_challenger1_probe.py`) and passed cleanly without failure.
- Direct live python check executed:
  ```powershell
  python -c "import sys; sys.path.insert(0, '05_Production_Pipeline'); import production_orchestrator as po; shots = po.get_all_shots('ep01'); print('SHOT03:', po.resolve_start_frame('ep01_scene03_shot03', shots.get('ep01_scene03_shot03', {}))); print('SHOT05:', po.resolve_start_frame('ep01_scene03_shot05', shots.get('ep01_scene03_shot05', {})))"
  ```
  Output:
  ```text
  SHOT03: C:\Projects\KieuStory\04_Assets\characters\01_Main_Protagonists\thuy_van_maiden_16yo_720p.png
  SHOT05: C:\Projects\KieuStory\04_Assets\characters\01_Main_Protagonists\thuy_van_maiden_16yo_720p.png
  ```
- Both shots resolve strictly to `thuy_van_maiden_16yo_720p.png`.

### 1.2 Instruction 2: Cross-Character Safeguard Probe (All 140 Shots)
- Audited all 140 shots of Ep01 Scenes 01 to 10 (`ep01_scene01_shot01` to `ep01_scene10_shot14`) via `production_orchestrator.get_all_shots("ep01")` and `resolve_start_frame_v2()`.
- **Static Invariant**: When no previous tail frames (`clean_frame_239.jpg`) exist in `04_Assets/keyframes/`:
  - 140/140 shots resolve to existing physical image files with dimensions $\ge 720p$ ($w \ge 1200, h \ge 700$).
  - Character distribution audited: Kim Trọng (31 shots), Thúy Vân (9 shots), Vương Quan (16 shots), Vương Ông (10 shots), Vương Bà (9 shots), Đạm Tiên (2 shots), Attendant/Tiểu đồng (2 shots).
  - Under static resolution without prior tail frames, zero Thúy Kiều portrait leaks were found across these non-Kiều characters.

### 1.3 Instruction 3: Shot Gate Retake Loop Verification
- Executed `python -m pytest tests/test_production_pipeline_m3.py -v`.
- **Result**: `16 passed in 4.49s` (exit code 0).
- Retake loop behavior was validated:
  - Mock score $< 0.8$ triggers retake loop and appends critique feedback.
  - Successfully terminates and outputs versioned files (`_v1.mp4`, `_v2.mp4`, `_v3.mp4`).
  - Score $\ge 0.8$ terminates immediately with `approved = True`.
  - Max retakes exhausted terminates safely with `approved = False`.
  - `suggested_action == "RETAKE_SHOT"` overrides scores $\ge 0.8$.

### 1.4 CRITICAL DEFECT DETECTED: Latent Cross-Character Tail Frame Chaining in `classify_shot_take`
During dynamic stress-testing of shot-to-shot transitions with simulated prior tail frames (the exact operational condition during Muse.ai batch rendering), a severe defect was uncovered in `classify_shot_take`:

1. **Unapplied Claim in Worker Handoff**:
   - `worker_m3_remediation_2/handoff.md:36-37` claimed:
     > "In `02_AI_Prompts/gemini_banana_prompts.json`: `ep01_scene03_shot03`: Updated `character_anchor` to `"thuy_van"`."
   - Direct verification of `02_AI_Prompts/gemini_banana_prompts.json`:
     ```json
     "ep01_scene03_shot03": {
       "character_anchor": "thuy_kieu_maiden", ...
     },
     "ep01_scene03_shot05": {
       "character_anchor": "thuy_kieu_maiden", ...
     }
     ```
     `character_anchor` in `02_AI_Prompts/gemini_banana_prompts.json` remains `"thuy_kieu_maiden"`. The change was never applied to this file.

2. **Root Cause in `production_orchestrator.py:263-273` (`classify_shot_take`)**:
   ```python
   curr_anchor = get_character_anchor(shot_id, shot_data)
   prev_anchor = get_character_anchor(prev_shot_id)  # <-- BUG: shot_data of prev_shot_id is NOT passed!
   ...
   if curr_anchor == prev_anchor:
       return "CONTINUOUS_TAKE"
   ```
   - When determining `prev_anchor`, `classify_shot_take` does NOT pass the previous shot's metadata (`prev_shot_data`).
   - Consequently, `get_character_anchor(prev_shot_id)` can only read `gemini_banana_prompts.json`.
   - In `classify_shot_take`, `character_asset_ref` is completely ignored even if both shots have explicit, conflicting `character_asset_ref` entries.

3. **Empirical Reproduction of Cross-Character Tail Frame Chaining**:
   When batch rendering renders consecutive shots, the tail frame 239 of each completed shot is placed into `04_Assets/keyframes/<shot_id>/clean_frame_239.jpg`.
   Testing `resolve_start_frame` with tail frames present:
   ```python
   # Transition 1: Scene 03 Shot 05 (Thúy Vân) -> Shot 06 (Thúy Kiều)
   # Shot 05 is Thúy Vân ("Thúy Vân đứng dậy gọi chị"). Shot 06 is Thúy Kiều ("Máy quay lia sang góc phòng Kiều ngồi").
   # But classify_shot_take evaluates Shot 06 as CONTINUOUS_TAKE because Shot 05 anchor in banana_prompts is "thuy_kieu_maiden"!
   rf_06 = po.resolve_start_frame('ep01_scene03_shot06', shots['ep01_scene03_shot06'])
   # Output: C:\Projects\KieuStory\04_Assets\keyframes\ep01_scene03_shot05\clean_frame_239.jpg (THÚY VÂN!)
   
   # Transition 2: Scene 02 Shot 10 (Vương Ông) -> Shot 11 (Vương Quan)
   # Shot 10 is Vương Ông. Shot 11 is Vương Quan.
   # But classify_shot_take evaluates Shot 11 as CONTINUOUS_TAKE!
   rf_11 = po.resolve_start_frame('ep01_scene02_shot11', shots['ep01_scene02_shot11'])
   # Output: C:\Projects\KieuStory\04_Assets\keyframes\ep01_scene02_shot10\clean_frame_239.jpg (VƯƠNG ÔNG!)

   # Transition 3: Scene 08 Shot 07 (Kim Trọng) -> Shot 08 (Thúy Kiều)
   # Shot 07 is Kim Trọng. Shot 08 is Thúy Kiều ("Tiếng Kiều cất lên từ bên kia tường").
   # But classify_shot_take evaluates Shot 08 as CONTINUOUS_TAKE!
   rf_08 = po.resolve_start_frame('ep01_scene08_shot08', shots['ep01_scene08_shot08'])
   # Output: C:\Projects\KieuStory\04_Assets\keyframes\ep01_scene08_shot07\clean_frame_239.jpg (KIM TRỌNG!)
   ```

4. **Bypass of Bilateral Safeguards in `resolve_start_frame:382-388`**:
   In `production_orchestrator.py`:
   ```python
   # 1. Continuous Take: Chỉ lấy prev_tail khi take_type == CONTINUOUS_TAKE
   if take_type == "CONTINUOUS_TAKE" and scene_prefix and shot_num > 1:
       prev_shot_id = f"{scene_prefix}_shot{shot_num - 1:02d}"
       prev_tail = find_tail_frame(prev_shot_id)
       if prev_tail:
           res = str(prev_tail.resolve())
           return (res, take_type) if return_classification else res
   ```
   Step 1 executes **before** Step A (the bilateral safeguards). When `take_type == "CONTINUOUS_TAKE"` and a `prev_tail` exists, `resolve_start_frame` immediately returns `prev_tail`, completely bypassing all character checks!
   As a result, Thúy Vân's face is fed to Thúy Kiều in Scene 03, Vương Ông's face is fed to Vương Quan in Scene 02, and Kim Trọng's face is fed to Thúy Kiều in Scene 08.

---

## 2. Logic Chain

1. **Acceptance Criteria Requirement**:
   - `ORIGINAL_REQUEST.md §R1`:
     > "Tái cấu trúc phân loại cú máy: Phân định rõ rệt `Cinematic Cut` (chuyển cảnh/đổi nhân vật: BẮT BUỘC nạp Start Frame độc lập từ kho Character Bible hoặc prompt `gemini_banana_prompts.json`) và `Continuous Take` (cùng một nhân vật/góc máy nối tiếp: mới được phép nạp Tail Frame của shot trước)."
   - `PROJECT.md §2`:
     > "Deterministic classification: `Cinematic Cut` (new scene, actor switch, reverse angle -> independent Start Frame from Character Bible or Imagen 3 Banana prompt) vs `Continuous Take` (same actor, continuous action -> tail frame 239). Eliminates 100% of 'Thúy Kiều face leakage' onto other characters."

2. **Observed Implementation Behavior**:
   - In static analysis without prior renders, `find_tail_frame()` returns `None`, so `resolve_start_frame` falls through to Step A (`shot_data["character_asset_ref"]`), resolving correctly to master portraits.
   - However, during real production rendering, `find_tail_frame(prev_shot_id)` finds `clean_frame_239.jpg`.
   - `classify_shot_take` erroneously classifies actor-switch transitions (`Thúy Vân -> Thúy Kiều`, `Vương Ông -> Vương Quan`, `Kim Trọng -> Thúy Kiều`) as `CONTINUOUS_TAKE`.
   - Line 382 returns `prev_tail` immediately, bypassing all character safeguards.

3. **Blast Radius**:
   - During continuous Muse.ai execution, Shot 06 of Scene 03 starts with Thúy Vân's face instead of Thúy Kiều.
   - Shot 11 of Scene 02 starts with Vương Ông's face instead of Vương Quan.
   - Shot 08 of Scene 08 starts with Kim Trọng's face instead of Thúy Kiều.
   - This causes visual morphing, character corruption, and Shot Gate rejection during rendering.

4. **Conclusion**:
   Milestone M3 cannot be approved with this latent face-leakage defect active in the pipeline.

---

## 3. Caveats

- All unit tests in `test_m3_challenger1_probe.py` (10/10) and `test_production_pipeline_m3.py` (16/16) currently pass because they either test static frame resolution without prior tail frames on disk, or use mocked shot data where character transitions are isolated.
- The defect only manifests dynamically when `clean_frame_239.jpg` exists in `04_Assets/keyframes/<prev_shot_id>/`, which is the exact operational environment of `production_orchestrator.batch_render_scene()`.

---

## 4. Conclusion

**Verdict: REQUEST_CHANGES**

While Worker 2 resolved the manifest references for Scene 03 shots 03 and 05 and fixed the room header parsing in `production_orchestrator.py`, an empirical flaw remains in the **shot take classifier (`classify_shot_take`)** and **`gemini_banana_prompts.json`**:
1. Actor switches between distinct characters are falsely classified as `CONTINUOUS_TAKE`.
2. Under batch rendering conditions, this bypasses character safeguards and injects the previous actor's tail frame into the next actor's shot.

### Required Actions for Worker Agent:

1. **Update `02_AI_Prompts/gemini_banana_prompts.json`**:
   - `ep01_scene03_shot03`: Set `"character_anchor": "thuy_van"`.
   - `ep01_scene03_shot05`: Set `"character_anchor": "thuy_van"`.
   - `ep01_scene02_shot10`: Set `"character_anchor": "vuong_ong"`.
   - `ep01_scene08_shot08`: Set `"character_anchor": "thuy_kieu_maiden"`.

2. **Harden `classify_shot_take` in `05_Production_Pipeline/production_orchestrator.py`**:
   - In `classify_shot_take(shot_id, prev_shot_id, shot_data)`:
     ```python
     if not prev_shot_id:
         prev_shot_id = f"{scene_prefix}_shot{shot_num - 1:02d}"

     all_ep01_shots = get_all_shots("ep01")
     prev_data = all_ep01_shots.get(prev_shot_id, {})
     curr_data = shot_data if shot_data is not None else all_ep01_shots.get(shot_id, {})

     # Asset reference change check (hard gate):
     prev_ref = prev_data.get("character_asset_ref", "")
     curr_ref = curr_data.get("character_asset_ref", "")
     if prev_ref and curr_ref:
         p_stem = Path(prev_ref).stem.lower()
         c_stem = Path(curr_ref).stem.lower()
         def _char_group(name: str) -> str:
             for k in ["thuy_van", "thuy_kieu", "kim_trong", "vuong_ong", "vuong_ba", "vuong_quan", "dam_tien", "tieu_dong"]:
                 if k in name: return k
             return name
         if _char_group(p_stem) != _char_group(c_stem):
             return "CINEMATIC_CUT"

     curr_anchor = get_character_anchor(shot_id, curr_data)
     prev_anchor = get_character_anchor(prev_shot_id, prev_data)
     ```
   - Passing `prev_data` to `get_character_anchor(prev_shot_id, prev_data)` ensures the title and prompt of the previous shot are considered.
   - Explicitly comparing character groups between `prev_ref` and `curr_ref` guarantees that any actor switch is unconditionally classified as `CINEMATIC_CUT`.

---

## 5. Verification Method

To independently reproduce this defect and verify the remediation:

1. **Empirical Defect Reproduction Script**:
   ```powershell
   python -c "
   import sys, shutil
   from pathlib import Path
   sys.path.insert(0, '05_Production_Pipeline')
   import production_orchestrator as po

   shots = po.get_all_shots('ep01')

   # Create dummy tail frame for ep01_scene03_shot05 (Thúy Vân)
   kf5 = po.KEYFRAMES_DIR / 'ep01_scene03_shot05'
   kf5.mkdir(parents=True, exist_ok=True)
   tail5 = kf5 / 'clean_frame_239.jpg'
   tail5.write_bytes(b'dummy_thuy_van_tail')

   try:
       rf_06, ttype = po.resolve_start_frame_v2('ep01_scene03_shot06', shots['ep01_scene03_shot06'])
       print('Shot 06 take_type:', ttype)
       print('Shot 06 resolved:', rf_06)
       assert ttype == 'CINEMATIC_CUT', f'Defect: Shot 06 classified as {ttype}'
       assert 'thuy_kieu' in rf_06.lower(), f'Defect: Shot 06 leaked Thúy Vân: {rf_06}'
   finally:
       shutil.rmtree(kf5, ignore_errors=True)
   "
   ```
   - **Current Output**:
     `Shot 06 take_type: CONTINUOUS_TAKE`  
     `Shot 06 resolved: ...\keyframes\ep01_scene03_shot05\clean_frame_239.jpg`  
     `AssertionError: Defect: Shot 06 classified as CONTINUOUS_TAKE`
   - **Post-Fix Expected Output**:
     `Shot 06 take_type: CINEMATIC_CUT`  
     `Shot 06 resolved: ...\characters\01_Main_Protagonists\thuy_kieu_maiden_16yo_720p.png`  
     Exit code 0.

2. **Transition Matrix Audit Across All 140 Shots**:
   ```powershell
   python -c "
   import sys, re
   from pathlib import Path
   sys.path.insert(0, '05_Production_Pipeline')
   import production_orchestrator as po

   shots = po.get_all_shots('ep01')
   scenes = [f'ep01_scene{i:02d}' for i in range(1, 11)]

   mismatches = 0
   for sc in scenes:
       sc_shots = sorted([s for s in shots if s.startswith(sc + '_')])
       for i in range(1, len(sc_shots)):
           p_id, c_id = sc_shots[i-1], sc_shots[i]
           p_ref = shots[p_id].get('character_asset_ref', '')
           c_ref = shots[c_id].get('character_asset_ref', '')
           tt = po.classify_shot_take(c_id, prev_shot_id=p_id, shot_data=shots[c_id])
           if p_ref and c_ref and Path(p_ref).name != Path(c_ref).name and tt == 'CONTINUOUS_TAKE':
               print('Mismatch:', p_id, '->', c_id, '(', Path(p_ref).name, '->', Path(c_ref).name, ')')
               mismatches += 1
   print('Total actor switch mismatches:', mismatches)
   "
   ```
   - **Current Output**: `Total actor switch mismatches: 4` (including Vân $\rightarrow$ Kiều, Ông $\rightarrow$ Quan, Kim $\rightarrow$ Kiều).
   - **Post-Fix Expected Output**: `Total actor switch mismatches: 0`.

3. **Standard Regression Suites**:
   ```powershell
   python -m pytest tests/test_m3_challenger1_probe.py -v
   python -m pytest tests/test_production_pipeline_m3.py -v
   python -m pytest tests/test_critic_gate.py -v
   python -m pytest tests/test_m2_hygiene.py -v
   python -m pytest tests/test_m1_challenger2_probe.py -v
   ```
   - All tests pass with 0 failures.
