# Handoff Report — Milestone M3 Iteration 3 Remediation

**Date**: 2026-10-09  
**Agent**: worker_m3_remediation_3  
**Role**: implementer, qa, specialist  
**Status**: Hard Handoff (Task Complete)  

---

## 1. Observation

### 1.1. Upstream Discrepancies and Root Cause Analysis
During investigation of the latent face-leakage defect between adjacent shots:
1. **Prompt Buckets & Anchor Misalignment**:
   - In `02_AI_Prompts/gemini_banana_prompts.json` and `episodes/ep01/prompts/banana_prompts.json`, `ep01_scene03_shot03` and `ep01_scene03_shot05` originally had:
     ```json
     "character_anchor": "thuy_kieu_maiden"
     ```
     even though the shot describes Thúy Vân ("Khuôn trăng đầy đặn nét ngài nở nang...").
   - `ep01_scene02_shot10` had `character_anchor="thuy_kieu_maiden"` instead of `"vuong_ong"`.
   - `ep01_scene08_shot08` had `character_anchor="vuong_thuy_kieu"` instead of `"thuy_kieu_maiden"`.
   - Data exists in dual buckets: `ep01_start_frames` sub-object and top-level shot keys. Prior edits only patched one bucket in some files, leaving discrepancies when queried under different access patterns.

2. **Transition Classifier Gap (`05_Production_Pipeline/production_orchestrator.py:276`)**:
   - `classify_shot_take(curr_shot_id)` previously called `get_character_anchor(prev_shot_id)` without passing `prev_data`. Without shot metadata, fallback heuristics could fail to resolve the previous character anchor.
   - Furthermore, `classify_shot_take` lacked a hard gate checking `character_asset_ref` equality, and did not check for actor name switches in textual prompts.
   - Consequently, when adjacent shots swapped actors (e.g. `ep01_scene03_shot05` Thúy Vân -> `ep01_scene03_shot06` Thúy Kiều), the system could return `CONTINUOUS_TAKE`.

3. **Vulnerability in Start Frame Resolution (`05_Production_Pipeline/production_orchestrator.py:346`)**:
   - In `resolve_start_frame_v2`, Step 1 previously trusted `take_type == "CONTINUOUS_TAKE"` blindly:
     ```python
     if take_type == "CONTINUOUS_TAKE":
         prev_tail = KEYFRAMES_DIR / prev_shot_id / "clean_frame_239.jpg"
         if prev_tail.exists():
             return prev_tail, "CONTINUOUS_TAKE", "tail_frame"
     ```
     If a physical tail frame existed on disk for `prev_shot_id` and the classifier erroneously returned `CONTINUOUS_TAKE`, the system would return `prev_tail`, inheriting the previous actor's face into the current actor's shot (face-leakage / visual contamination).

4. **Scene Gate Abort Condition (`05_Production_Pipeline/production_orchestrator.py:972`)**:
   - In `batch_render_scene`, line 972 evaluated:
     ```python
     if not scene_verdict.approved and scene_verdict.overall_score < 0.8:
         return False
     ```
     allowing an unapproved verdict with overall score >= 0.8 to proceed without aborting.

### 1.2. Verification Test Outputs
- Command: `pytest tests/test_m3_challenger1_probe.py tests/test_production_pipeline_m3.py tests/test_critic_gate.py tests/test_m2_hygiene.py tests/test_m1_challenger2_probe.py`
  - Output: `105 passed in 32.36s`
- Command: `pytest tests/test_tier1_features.py -k "not test_render"`
  - Output: `65 passed, 1 warning in 2.83s`

---

## 2. Logic Chain

1. **Step 1 (Prompt Synchronization)**:
   - We updated `02_AI_Prompts/gemini_banana_prompts.json` and `episodes/ep01/prompts/banana_prompts.json` across both buckets (`ep01_start_frames` and top-level/direct dictionary).
   - Specifically:
     - `ep01_scene03_shot03`: `character_anchor="thuy_van"`, prompt updated with Thúy Vân description.
     - `ep01_scene03_shot05`: `character_anchor="thuy_van"`.
     - `ep01_scene02_shot10`: `character_anchor="vuong_ong"`.
     - `ep01_scene08_shot08`: `character_anchor="thuy_kieu_maiden"`.
   - Now, whether loaded via `banana_prompts.json` or `gemini_banana_prompts.json`, anchor lookups yield consistent and correct character identities.

2. **Step 2 (Orchestrator Classifier Hardening)**:
   - In `05_Production_Pipeline/production_orchestrator.py` (`classify_shot_take`):
     - Loaded `prev_data` from `get_all_shots(ep_id)` using `prev_shot_id`.
     - Hard Gate 1 (`character_asset_ref` comparison): Extracted character group prefixes via `_char_group`. If `prev_group != curr_group`, immediately return `CINEMATIC_CUT`.
     - Passed `prev_data` directly to `get_character_anchor(prev_shot_id, prev_data)`.
     - Hard Gate 2: If `curr_anchor != prev_anchor`, return `CINEMATIC_CUT`.
     - Hard Gate 3: Checked text prompts via `_detect_actor_from_text`. If different actors are detected, return `CINEMATIC_CUT`.

3. **Step 3 (Defense-in-Depth in `resolve_start_frame`)**:
   - In `resolve_start_frame_v2` Step 1:
     - Even if `take_type == "CONTINUOUS_TAKE"`, we added bilateral safeguard checks:
       - Compare `_char_group(curr_ref)` vs `_char_group(prev_ref)`.
       - Compare `curr_anchor` vs `prev_anchor`.
     - If any safeguard fails, log a warning, downgrade `take_type = "CINEMATIC_CUT"`, and bypass returning `prev_tail`.
   - This ensures that even if a physical tail frame exists on disk, it can NEVER leak across character boundaries.

4. **Step 4 (Scene Gate Tightening)**:
   - In `batch_render_scene` (line 972), tightened the gate check to:
     ```python
     if not scene_verdict.approved:
         print(f"[!] Scene Gate từ chối phê duyệt cảnh {scene_id}: {scene_verdict.critique_notes}")
         return False
     ```

5. **Step 5 (Empirical Probing & Regression Tests)**:
   - In `tests/test_m3_challenger1_probe.py`:
     - Added `test_classify_shot_take_actor_switches_are_cinematic_cuts`:
       - Explicitly verifies `ep01_scene03_shot06` (Thúy Vân -> Thúy Kiều), `ep01_scene02_shot11` (Vương Ông -> Thúy Kiều), and `ep01_scene08_shot08` (Kim Trọng -> Thúy Kiều).
       - Iterates over all 140 contiguous shot pairs across all scenes in Episode 1. Verified **0 mismatches** between character asset changes and `take_type`.
     - Added `test_simulated_tail_frame_actor_switch_prevents_face_leak`:
       - Creates a physical `clean_frame_239.jpg` on disk in `04_Assets/keyframes/ep01_scene03_shot05/` (Thúy Vân).
       - Calls `resolve_start_frame_v2("ep01_scene03_shot06")` (Thúy Kiều).
       - Verifies `take_type == "CINEMATIC_CUT"`, resolves to Thúy Kiều master portrait (`04_Assets/characters/thuy_kieu_720p.png`), and completely ignores the physical tail frame.
       - Cleans up temporary artifacts cleanly.

---

## 3. Caveats

1. **Dry-Run / Mock Rendering in Test Suites**: Tests use `dry_run=True` and mock FFmpeg/browser sessions. Actual production rendering requires active browser session `agent-browser --session muse` as documented in `AGENTS.md`.
2. **Read-Only Character Masters**: `04_Assets/characters/` was preserved strictly read-only; no portraits were altered.
3. **Legacy Archive Preservation**: `04_Assets/archive/ep01_legacy_v1/` remains untouched.
4. **No Other Caveats**: All 140 shot transitions pass cleanly without edge-case regressions.

---

## 4. Conclusion

- **Final Assessment**: Milestone M3 Iteration 3 Remediation is 100% complete.
- **Vulnerabilities Resolved**:
  - The latent face-leakage bug is eliminated at both the classification level and the resolution level (defense-in-depth).
  - Dual-bucket prompt files are fully synchronized.
  - The Scene Gate strictly enforces quality standards.
  - 105 verification tests pass with 0 failures.

---

## 5. Verification Method

To independently verify this implementation, run the following commands from repository root (`c:\Projects\KieuStory`):

### 5.1. Run Full Regression Suite
```powershell
pytest tests/test_m3_challenger1_probe.py tests/test_production_pipeline_m3.py tests/test_critic_gate.py tests/test_m2_hygiene.py tests/test_m1_challenger2_probe.py -v
```
**Expected Result**: `105 passed`.

### 5.2. Run Actor Switch Probe Specifically
```powershell
pytest tests/test_m3_challenger1_probe.py -k "actor_switch" -v
```
**Expected Result**: `2 passed` (`test_classify_shot_take_actor_switches_are_cinematic_cuts` and `test_simulated_tail_frame_actor_switch_prevents_face_leak`).

### 5.3. Run Tier 1 Feature Suite
```powershell
pytest tests/test_tier1_features.py -k "not test_render"
```
**Expected Result**: `65 passed`.

### 5.4. Files to Inspect
1. `05_Production_Pipeline/production_orchestrator.py`:
   - Lines 276–322: `classify_shot_take` implementation (asset ref check, metadata passing, text check).
   - Lines 346–370: `resolve_start_frame_v2` Step 1 bilateral safeguards.
   - Line 972: `batch_render_scene` strict gate check (`if not scene_verdict.approved: return False`).
2. `02_AI_Prompts/gemini_banana_prompts.json` & `episodes/ep01/prompts/banana_prompts.json`:
   - Check `ep01_scene03_shot03`, `ep01_scene03_shot05`, `ep01_scene02_shot10`, `ep01_scene08_shot08` in both buckets.
3. `tests/test_m3_challenger1_probe.py`:
   - Inspect tests `test_classify_shot_take_actor_switches_are_cinematic_cuts` and `test_simulated_tail_frame_actor_switch_prevents_face_leak`.

### 5.5. Invalidation Conditions
- If any contiguous shot pair in Episode 1 with different characters is classified as `CONTINUOUS_TAKE`.
- If `resolve_start_frame_v2` returns `clean_frame_239.jpg` when transitioning from a different character.
- If `batch_render_scene` continues after `scene_verdict.approved == False`.
