# HANDOFF REPORT: Milestone M1 Gate Verification Review & Adversarial Audit

**Agent ID**: `reviewer_m1_2`  
**Role**: Reviewer & Adversarial Critic  
**Milestone**: M1 Gate Verification  
**Workspace**: `c:\Projects\KieuStory`  
**Timestamp**: 2026-10-09T04:25:00Z  
**Verdict**: **REQUEST_CHANGES**

---

## 1. Observation

### 1.1 Direct Code & Test Observations
1. **Workspace Cleanliness & Hygiene Tests**:
   - Command: `python -m pytest tests/test_m2_hygiene.py -v`
   - Result: 6/6 tests passed in 0.15s (`test_archive_ep01_video_count`, `test_videos_ep01_purged`, `test_keyframes_ep01_purged`, `test_archive_readme_legal_documentation`, `test_character_portraits_unharmed`, `test_non_ep01_assets_unharmed`).
   - Verbatim check on disk: `04_Assets/videos/` contains 0 premature ep01 video files; `04_Assets/archive/ep01_legacy_v1/` contains 184 files and legal `README.md`; `04_Assets/characters/` has all 14 character portraits intact.

2. **Existing Feature Regression Suite**:
   - Command: `pytest tests/test_tier1_features.py -k "not test_render" -v`
   - Result: 65/65 passed in 2.51s (0 regressions).

3. **Critic Gate Test Suite**:
   - Command: `python -m pytest tests/test_critic_gate.py -v`
   - Result: 23/23 passed in 7.61s.

4. **Classifier & Take Resolution (`05_Production_Pipeline/production_orchestrator.py`)**:
   - `classify_shot_take()` (lines 178-208):
     - Correctly identifies `shot_num == 1` as `CINEMATIC_CUT`.
     - Compares `curr_anchor` and `prev_anchor` from `gemini_banana_prompts.json` or `shot_data`.
     - Returns `CINEMATIC_CUT` on actor switches and scenery/none anchors (`anchor == 'none'`).
     - Returns `CONTINUOUS_TAKE` when consecutive shots share the identical character anchor.
   - `resolve_start_frame()` (lines 286-393):
     - Correctly loads `clean_frame_239.jpg` (Tail Frame) ONLY when `take_type == CONTINUOUS_TAKE`.
     - Enforces invariant that `CINEMATIC_CUT` never returns `prev_tail`.
     - Contains a unilateral Kim Trọng safeguard:
       ```python
       is_kim_trong = "kim_trong" in curr_anchor and "thuy_kieu" not in curr_anchor
       if is_kim_trong and "thuy_kieu" in str(asset_ref).lower():
           asset_ref = "04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png"
       ```
       and in step D:
       ```python
       if is_kim_trong and resolved_path and "thuy_kieu" in resolved_path.lower():
           kt_portrait = CHARACTERS_DIR / "01_Main_Protagonists" / "kim_trong_18yo_720p.png"
           resolved_path = str(kt_portrait.resolve()) if kt_portrait.exists() else None
       ```

### 1.2 Adversarial Finding: Character Inversion in Scene 10 Climax
During deep inspection of prompt registries and screenplay text:
1. In `episodes/ep01/screenplay.md` (lines 1150-1310):
   - `ep01_scene10_shot03`: "Kiều qua rào sang thư phòng (Tracking Shot Kiều Night Stroll)" -> Action: Thúy Kiều walking alone through the night garden to the study.
   - `ep01_scene10_shot06`: "Kiều giãi bày tấc lòng son (Close-Up Kiều Devotion)" -> Action: Thúy Kiều speaking on-screen to Kim Trọng.
   - `ep01_scene10_shot11`: "Kiều ôm đàn tỳ bà so dây dưới trăng (Medium Shot Kiều with Pipa)" -> Action: Thúy Kiều holding and tuning the Pipa lute.
   - `ep01_scene10_shot12`: "Tiếng đàn tỳ bà réo rắt khúc tương tư (Close-Up Pipa Performance)" -> Action: Thúy Kiều's fingers and emotional face plucking the Pipa lute strings.
2. In `02_AI_Prompts/gemini_banana_prompts.json`:
   - `ep01_scene10_shot03`: `character_anchor` = `"thuy_kieu_maiden"`
   - `ep01_scene10_shot06`: `character_anchor` = `"kim_trong + thuy_kieu_maiden"`
   - `ep01_scene10_shot11`: `character_anchor` = `"thuy_kieu_maiden"`
   - `ep01_scene10_shot12`: `character_anchor` = `"thuy_kieu_maiden"`
3. In `episodes/ep01/prompts/muse_prompts.json` and `02_AI_Prompts/muse_ai_video_prompts.json`:
   - `worker_m1_critic_1` applied a wholesale range replacement from `ep01_scene10_shot03` through `ep01_scene10_shot12` to `04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png`.
   - As a result:
     - `ep01_scene10_shot03` `character_asset_ref` = `kim_trong_18yo_720p.png`
     - `ep01_scene10_shot06` `character_asset_ref` = `kim_trong_18yo_720p.png`
     - `ep01_scene10_shot11` `character_asset_ref` = `kim_trong_18yo_720p.png`
     - `ep01_scene10_shot12` `character_asset_ref` = `kim_trong_18yo_720p.png`
4. Empirical Resolution under Actual Production Calls:
   - When `production_orchestrator.py` or `run_shot.py` calls `resolve_start_frame(shot_id, shot_data)` with the shot's actual prompt data:
     ```python
     all_shots = po.get_all_shots()
     s11 = po.resolve_start_frame('ep01_scene10_shot11', all_shots['ep01_scene10_shot11'])
     s12 = po.resolve_start_frame('ep01_scene10_shot12', all_shots['ep01_scene10_shot12'])
     ```
   - **Output**:
     `s11` -> `04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png`
     `s12` -> `04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png`
   - **Impact**: Thúy Kiều's legendary Pipa solo under the moon (the emotional climax of Episode 1) is assigned the portrait of Kim Trọng.

### 1.3 Adversarial Finding: Vacuous Test Harness in `tests/test_critic_gate.py`
In `tests/test_critic_gate.py`:
```python
def test_scene10_shots_resolve_to_kim_trong_not_kieu(self):
    kim_shots = ["ep01_scene07_shot01", "ep01_scene09_shot04", "ep01_scene10_shot05", "ep01_scene10_shot13"]
    for sid in kim_shots:
        resolved = po.resolve_start_frame(sid, {})
        if resolved:
            assert "thuy_kieu" not in resolved.lower()
            assert "kim_trong" in resolved.lower()
```
- The test explicitly passes an empty dictionary `{}` instead of `all_shots[sid]`.
- Passing `{}` strips `character_asset_ref`, causing `resolve_start_frame` to ignore the corrupted prompt registry and fall back to `curr_anchor`.
- The test selectively tests only `shot05` and `shot13`, omitting shots 03, 06, 11, and 12, masking the character contamination.

---

## 2. Logic Chain

1. **Root Cause Analysis of Prompt Defect**:
   - Task F3 in `PROJECT.md` stated: *"Correct 11 misassigned character_asset_ref entries in episodes/ep01/prompts/muse_prompts.json (Kim Trọng vs Thúy Kiều)"*.
   - The upstream agent interpreted "11 entries" as a contiguous index range: `ep01_scene09_shot04` (1 shot) + `ep01_scene10_shot03` through `ep01_scene10_shot12` (10 shots) = 11 shots.
   - However, in Scene 10, Kim Trọng is the sole character on screen only in shots 04, 05, and 13.
   - Shots 03, 06, 11, and 12 feature **Thúy Kiều**, particularly shot 11 and 12 where Kiều plays the Pipa lute.
   - The bulk replacement corrupted these 4 shots by assigning them Kim Trọng's portrait.

2. **Root Cause Analysis of Orchestrator Safeguard Gap**:
   - `resolve_start_frame()` was designed with a unilateral guard:
     `is_kim_trong = "kim_trong" in curr_anchor and "thuy_kieu" not in curr_anchor`
     This prevents `thuy_kieu` from leaking into Kim Trọng's shots.
   - However, there is NO reciprocal guard for Thúy Kiều:
     When `curr_anchor` is `thuy_kieu_maiden`, but `shot_data["character_asset_ref"]` points to `kim_trong_18yo_720p.png`, Step A in `resolve_start_frame()` naively accepts `kim_trong_18yo_720p.png` because `is_kim_trong` is `False`.
   - This architectural asymmetry allows Kim Trọng to overwrite Thúy Kiều whenever prompt metadata is erroneous.

3. **Production Blast Radius**:
   - If Milestone M1 is approved without fixing this defect, Milestone M3 (Production Pipeline Execution) will ingest `all_shots` from `muse_prompts.json`.
   - Scene 10 shots 03, 06, 11, 12 will feed Kim Trọng's face into Muse.ai while the motion prompt asks for "Kiều gảy đàn tỳ bà", producing a deformed or cross-gender mutant character, directly violating Acceptance Criterion §R1: *"Thẩm định đúng nhân vật (không nhầm vai giữa Thúy Kiều và Vương Ông/Vương Bà/Kim Trọng)"*.

---

## 3. Caveats

- **Scope of Defect**: The classifier (`classify_shot_take`) and the M2 workspace hygiene (`test_m2_hygiene.py`) are solidly implemented and verified. The defect is localized to:
  1. Incorrect `character_asset_ref` values in `episodes/ep01/prompts/muse_prompts.json` and `02_AI_Prompts/muse_ai_video_prompts.json` for Scene 10 shots 03, 06, 11, 12.
  2. Lack of reciprocal Thúy Kiều protection in `resolve_start_frame()` in `05_Production_Pipeline/production_orchestrator.py`.
  3. Vacuous `{}` parameter in `tests/test_critic_gate.py`.
- No other caveats.

---

## 4. Conclusion & Review Verdict

**VERDICT**: **REQUEST_CHANGES**

### Required Remediation (Action Items for Worker Agent):
1. **Fix Prompt Registries (`ep01/prompts/muse_prompts.json` and `02_AI_Prompts/muse_ai_video_prompts.json`)**:
   - Revert `character_asset_ref` to `04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png` for:
     - `ep01_scene10_shot03` (Kiều qua rào)
     - `ep01_scene10_shot06` (Kiều giãi bày tấc lòng son)
     - `ep01_scene10_shot11` (Kiều ôm đàn tỳ bà)
     - `ep01_scene10_shot12` (Kiều gảy đàn tỳ bà)
   - Ensure `ep01_scene10_shot04`, `ep01_scene10_shot05`, `ep01_scene10_shot13` retain `04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png`.
   - Run `python 05_Production_Pipeline/episode_manager.py --sync-to-master` to guarantee 100% sync.
2. **Add Bilateral Safeguard in `05_Production_Pipeline/production_orchestrator.py`**:
   - Update `resolve_start_frame()` so that:
     ```python
     is_thuy_kieu = "thuy_kieu" in curr_anchor and "kim_trong" not in curr_anchor
     if is_thuy_kieu and "kim_trong" in str(asset_ref).lower():
         asset_ref = "04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png"
     ```
     and in final check:
     ```python
     if is_thuy_kieu and resolved_path and "kim_trong" in resolved_path.lower():
         tk_portrait = CHARACTERS_DIR / "01_Main_Protagonists" / "thuy_kieu_maiden_16yo_720p.png"
         resolved_path = str(tk_portrait.resolve()) if tk_portrait.exists() else None
     ```
3. **Harden `tests/test_critic_gate.py`**:
   - Test `resolve_start_frame` with ACTUAL `shot_data` from `po.get_all_shots()`.
   - Add explicit assertions verifying:
     - `po.resolve_start_frame('ep01_scene10_shot11', sdata)` resolves to `thuy_kieu` and NOT `kim_trong`.
     - `po.resolve_start_frame('ep01_scene10_shot12', sdata)` resolves to `thuy_kieu` and NOT `kim_trong`.
     - `po.resolve_start_frame('ep01_scene10_shot05', sdata)` resolves to `kim_trong` and NOT `thuy_kieu`.

---

## 5. Verification Method

To reproduce and independently verify the issues identified in this report:

```powershell
# 1. Reproduce the Scene 10 character inversion on Kiều Pipa shots:
python -c "
import sys
sys.path.insert(0, '05_Production_Pipeline')
import production_orchestrator as po
all_shots = po.get_all_shots()
for sid in ['ep01_scene10_shot03', 'ep01_scene10_shot06', 'ep01_scene10_shot11', 'ep01_scene10_shot12']:
    res = po.resolve_start_frame(sid, all_shots.get(sid, {}))
    print(f'{sid}: {res}')
"
# Observed Output: ALL four shots return 'kim_trong_18yo_720p.png' instead of 'thuy_kieu_maiden_16yo_720p.png'.

# 2. Verify that test_critic_gate.py masked this bug by passing empty dict:
python -c "
import sys
sys.path.insert(0, '05_Production_Pipeline')
import production_orchestrator as po
# With empty dict (what the test currently does):
print('With empty dict:', po.resolve_start_frame('ep01_scene10_shot11', {}))
# With actual prompt data (what production does):
print('With actual prompt data:', po.resolve_start_frame('ep01_scene10_shot11', po.get_all_shots().get('ep01_scene10_shot11', {})))
"
```

**Invalidation Conditions**:
- The verdict `REQUEST_CHANGES` stands until `ep01_scene10_shot11` and `shot12` resolve strictly to `thuy_kieu_maiden_16yo_720p.png` with actual prompt data loaded, and bilateral safeguards are in place.

---

## 6. Detailed Quality Review & Adversarial Challenge Matrix

### Review Findings Summary
- **Finding 1 [Critical]**: Character Inversion in Scene 10 Climax. Kiều Pipa shots (10-11, 10-12) assigned Kim Trọng portrait due to indiscriminate range replace.
- **Finding 2 [Critical]**: Self-certifying / vacuous test harness in `tests/test_critic_gate.py` masking the defect by passing `{}` instead of actual prompt dictionaries.
- **Finding 3 [Major]**: Unilateral Kim Trọng safeguard in `resolve_start_frame()` without reciprocal Thúy Kiều safeguard.
- **Finding 4 [Positive]**: `classify_shot_take()` correctly distinguishes cut vs take without hardcoded IDs. `test_m2_hygiene.py` (6/6) and `test_tier1_features.py` (65/65) pass cleanly.

### Challenge Summary
- **Overall Risk Assessment**: **CRITICAL** (High probability of corrupting core video renders if released to M3 production without fix).
- **Challenge 1**: What happens when `run_shot.py` renders `ep01_scene10_shot11`?
  - Attack Scenario: Pipeline feeds Kim Trọng's portrait to Muse.ai with prompt "Kiều gảy đàn tỳ bà".
  - Blast Radius: Climax of Episode 1 ruined with male face / cross-character hybrid.
  - Mitigation: Fix prompt registries for shots 03, 06, 11, 12, and add bilateral check in `resolve_start_frame()`.
