# KẾ HOẠCH & ĐẶC TẢ KHẮC PHỤC TOÀN DIỆN: PROMPTS & CHARACTER SAFEGUARDS
## MILESTONE M1 ITERATION 2 (THẬP NGŨ NIÊN - THE FIFTEEN SPRINGS)

> **Tài liệu**: Remediation Analysis & Implementation Recipe  
> **Người thực hiện**: Explorer 2 (`explorer_m1_remediation_2`)  
> **Ngày lập**: 2026-10-09  
> **Mục tiêu**: Xử lý dứt điểm 4 khuyết tật nghiêm trọng được phát hiện tại Cổng thẩm định M1 Gate (Reviewer 2 & Challenger 2).

---

## I. TỔNG QUAN VẤN ĐỀ & BỐI CẢNH (EXECUTIVE SUMMARY)

Tại vòng thẩm định Milestone M1 Iteration 1 Gate, Reviewer 2 và Challenger 2 đã phát hiện 4 lỗ hổng nghiêm trọng khiến Milestone M1 bị từ chối phê duyệt (`REQUEST_CHANGES`):

1. **Scene 10 Character Inversion**:
   - Trong quá trình triển khai Feature F3 ("Fix 11 misassigned Kim Trọng entries"), Agent tiền nhiệm đã thực hiện thay thế hàng loạt (wholesale range replacement) từ `ep01_scene10_shot03` đến `ep01_scene10_shot12` thành chân dung Kim Trọng (`kim_trong_18yo_720p.png`).
   - Hậu quả: Hai cú máy cao trào cảm xúc nhất của Tập 01 — **Thúy Kiều độc tấu đàn tỳ bà dưới trăng** (`ep01_scene10_shot11` và `ep01_scene10_shot12`), cùng hai cú máy Kiều sang thư phòng (`shot03`) và Kiều giãi bày tấc lòng son (`shot06`) — bị ép nhận diện khuôn mặt Kim Trọng, vi phạm trực tiếp kịch bản phân cảnh và quy chuẩn R1.
2. **Scene 06 & Scene 05 Character Asset Leaks**:
   - Trong **Scene 06**, `muse_prompts.json` hoàn toàn để trống `character_asset_ref = None` cho cả 14 shot; trong khi `gemini_banana_prompts.json` bị lệch pha và gán nhầm anchor (shot 04 gán `vuong_quan`, shot 06, 10, 12 gán `thuy_kieu_maiden`), khiến các cú máy Kim Trọng xuống ngựa, ghìm cương, bàng hoàng trúng tiếng sét ái tình bị nạp Start Frame Thúy Kiều!
   - Trong **Scene 05**, hai cú máy độc thoại/diễn xuất solo của Vương Quan (`ep01_scene05_shot16` - thuật chuyện Đạm Tiên và `shot24` - can ngăn Kiều theo chuẩn R4 Anti-Male-Tears) bị bỏ trống `character_asset_ref` và gán nhầm anchor `thuy_kieu_maiden`, dẫn tới nạp Start Frame Thúy Kiều cho nhân vật nam.
3. **Bilateral Character Safeguard Gap trong `production_orchestrator.py`**:
   - Hàm `resolve_start_frame()` hiện chỉ có chốt bảo vệ đơn phương (unilateral safeguard): bảo vệ Kim Trọng không bị gán Thúy Kiều, nhưng **hoàn toàn không có chiều ngược lại** (không bảo vệ Thúy Kiều trước Kim Trọng, không bảo vệ Vương Quan / Vương Ông trước Thúy Kiều).
4. **Harness Kiểm thử Tự Chứng Thực (Vacuous Test Harness) trong `test_critic_gate.py`**:
   - `test_critic_gate.py` truyền tham số mock rỗng `{}` vào `po.resolve_start_frame()`, vô hiệu hóa nhánh kiểm tra `shot_data.get("character_asset_ref")` và che giấu toàn bộ các khuyết tật trong tệp prompt thực tế.

---

## II. ĐẶC TẢ CHI TIẾT KHẮC PHỤC 4 KHUYẾT TẬT

---

### 1. KHUYẾT TẬT 1: SCENE 10 CHARACTER INVERSION

#### 1.1. Đối soát Kịch bản Phân cảnh (`episodes/ep01/screenplay.md` lines 1325-1460)
- `shot01`: Toàn cảnh vầng trăng rằm vằng vặc (Extreme Wide Full Moon) -> Khung cảnh thiên nhiên.
- `shot02`: Vườn Thúy ngập ánh trăng bạc (Wide Scenic Thúy Garden) -> Khung cảnh thiên nhiên.
- `shot03`: Kiều qua rào sang thư phòng (Tracking Shot Kiều Night Stroll) -> **Thúy Kiều** (gót sen thoăn thoắt, tà áo lụa xanh).
- `shot04`: Kim Trọng mở cửa đón Kiều (Medium Shot Door Opening) -> **Kim Trọng** (cánh cửa hé mở, Kim Trọng mừng rỡ đón Kiều).
- `shot05`: Kim Trọng nâng niu bàn tay Kiều (Medium Close-Up Hands Clasp) -> **Kim Trọng** (Thoại on-screen: Kim Trọng).
- `shot06`: Kiều giãi bày tấc lòng son (Close-Up Kiều Devotion) -> **Thúy Kiều** (Cận cảnh Thúy Kiều son sắt, thoại on-screen: Thúy Kiều).
- `shot07`: Uống chén rượu đồng tâm (Medium Shot Wine Toast) -> Hai người (Kim Trọng & Thúy Kiều).
- `shot08`: Cắt lọn tóc mây thề nguyền (Close-Up Cutting Hair) -> Bàn tay/đạo cụ dao con & lọn tóc.
- `shot09`: Bện nút đồng tâm (Macro Hands Braiding Hair) -> Bàn tay Thúy Kiều bện tóc.
- `shot10`: Xé lụa đào viết lời ước (Medium Close-Up Writing Oath) -> Kim Trọng & Thúy Kiều.
- `shot11`: Kiều ôm đàn tỳ bà so dây dưới trăng (Medium Shot Kiều with Pipa) -> **Thúy Kiều** (Kiều ôm đàn tỳ bà, so dây).
- `shot12`: Tiếng đàn tỳ bà réo rắt khúc tương tư (Close-Up Pipa Performance) -> **Thúy Kiều** (Cận cảnh ngón tay Kiều thoăn thoắt trên phím đàn).
- `shot13`: Kim Trọng say đắm lắng nghe (MCU Kim Trọng Enamored) -> **Kim Trọng** (Kim Trọng ngồi lắng nghe say đắm).
- `shot14`: Vầng trăng vằng vặc chứng giám lời thề (Wide Night Landscape) -> Khung cảnh thiên nhiên.

#### 1.2. Ma trận Quy chuẩn Chuẩn xác cho Scene 10 (Shots 01 - 14)

| Shot ID | Tiêu đề Kịch bản | Nhân vật Chính | `character_asset_ref` Chuẩn (`muse_prompts.json`) | `character_anchor` Chuẩn (`banana_prompts.json`) |
| :--- | :--- | :--- | :--- | :--- |
| `ep01_scene10_shot01` | Toàn cảnh vầng trăng rằm | Không có (Trời trăng) | `null` | `none` |
| `ep01_scene10_shot02` | Vườn Thúy ngập ánh trăng | Không có (Vườn hoa) | `null` | `none` |
| `ep01_scene10_shot03` | Kiều qua rào sang thư phòng | **Thúy Kiều** | `04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png` | `thuy_kieu_maiden` |
| `ep01_scene10_shot04` | Kim Trọng mở cửa đón Kiều | **Kim Trọng** | `04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png` | `kim_trong` |
| `ep01_scene10_shot05` | Kim Trọng nâng niu bàn tay | **Kim Trọng** | `04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png` | `kim_trong` |
| `ep01_scene10_shot06` | Kiều giãi bày tấc lòng son | **Thúy Kiều** | `04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png` | `thuy_kieu_maiden` |
| `ep01_scene10_shot07` | Uống chén rượu đồng tâm | Kim Trọng & Kiều | `04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png` | `kim_trong + thuy_kieu_maiden` |
| `ep01_scene10_shot08` | Cắt lọn tóc mây thề nguyền | Kim Trọng & Kiều | `04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png` | `none` |
| `ep01_scene10_shot09` | Bện nút đồng tâm | Thúy Kiều (Bàn tay) | `04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png` | `thuy_kieu_maiden` |
| `ep01_scene10_shot10` | Xé lụa đào viết lời ước | Kim Trọng & Kiều | `04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png` | `kim_trong + thuy_kieu_maiden` |
| `ep01_scene10_shot11` | Kiều ôm đàn tỳ bà so dây | **Thúy Kiều** | `04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png` | `thuy_kieu_maiden` |
| `ep01_scene10_shot12` | Tiếng đàn tỳ bà réo rắt | **Thúy Kiều** | `04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png` | `thuy_kieu_maiden` |
| `ep01_scene10_shot13` | Kim Trọng say đắm lắng nghe| **Kim Trọng** | `04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png` | `kim_trong` |
| `ep01_scene10_shot14` | Vầng trăng vằng vặc | Không có (Trăng sao) | `null` | `none` |

---

### 2. KHUYẾT TẬT 2: SCENE 06 & SCENE 05 CHARACTER ASSET LEAKS

#### 2.1. Phân tích Nguyên nhân Gốc rễ (Root Cause Analysis)
1. **Scene 06**:
   - `episodes/ep01/prompts/muse_prompts.json` không khai báo trường `character_asset_ref` cho cả 14 shot của Scene 06.
   - Do đó, pipeline buộc phải fallback về `curr_anchor = get_character_anchor(shot_id)`.
   - Trong `gemini_banana_prompts.json`, các cú máy Scene 06 được tạo dựa trên một bản nháp phân cảnh cũ bị lệch số thứ tự:
     - Shot 04 (Cận cảnh Kim Trọng) bị gán `character_anchor: "vuong_quan"`.
     - Shot 06 (Kim Trọng ghìm cương) bị gán `character_anchor: "thuy_kieu_maiden + thuy_van"`.
     - Shot 10 (Cận cảnh Kim Trọng bàng hoàng) bị gán `character_anchor: "thuy_kieu_maiden + thuy_van"`.
     - Shot 12 (Kim Trọng xuống ngựa cúi chào) bị gán `character_anchor: "thuy_kieu_maiden + vuong_quan + thuy_van"`.
   - Kết quả: Khi hàm `resolve_character_portrait_from_anchor` gặp chuỗi có `"thuy_kieu"`, nó trả về ngay lập tức chân dung Thúy Kiều cho Kim Trọng!
2. **Scene 05**:
   - Các shot 13, 16, 24 không có `character_asset_ref` trong `muse_prompts.json`.
   - Trong `gemini_banana_prompts.json`:
     - Shot 13 có `character_anchor: "thuy_van"`.
     - Shot 16 ("Trung cảnh Vương Quan thuật chuyện Đạm Tiên") có `character_anchor: "thuy_kieu_maiden"`.
     - Shot 24 ("Trung cận Vương Quan can ngăn - R4 Anti-Male-Tears") có `character_anchor: "thuy_kieu_maiden"`.
   - Kết quả: Vương Quan solo thoại và hành động bị gán khuôn mặt Thúy Kiều.

#### 2.2. Ma trận Quy chuẩn Chuẩn xác cho Scene 06 (Shots 01 - 14)

| Shot ID | Tiêu đề Kịch bản | Nhân vật Chính | `character_asset_ref` Chuẩn (`muse_prompts.json`) | `character_anchor` Chuẩn (`banana_prompts.json`) |
| :--- | :--- | :--- | :--- | :--- |
| `ep01_scene06_shot01` | Toàn cảnh hoàng hôn rặng liễu | Thiên nhiên (Hoàng hôn) | `null` | `none` |
| `ep01_scene06_shot02` | Khúc ngoặt đường mòn | Thiên nhiên / Vó ngựa | `null` | `none` |
| `ep01_scene06_shot03` | Kim Trọng cưỡi bạch mã | **Kim Trọng** | `04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png` | `kim_trong` |
| `ep01_scene06_shot04` | Cận cảnh Kim Trọng | **Kim Trọng** | `04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png` | `kim_trong` |
| `ep01_scene06_shot05` | Hai tiểu đồng theo sau | **Tiểu Đồng** | `04_Assets/characters/02_Vuong_Family_And_Fate/tieu_dong_kim_trong_720p.png` | `tieu_dong_kim_trong` |
| `ep01_scene06_shot06` | Kim Trọng ghìm cương ngựa | **Kim Trọng** | `04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png` | `kim_trong` |
| `ep01_scene06_shot07` | Vương Quan bước ra chào | **Vương Quan** | `04_Assets/characters/02_Vuong_Family_And_Fate/vuong_quan_16yo_720p.png` | `vuong_quan` |
| `ep01_scene06_shot08` | Hai chị em e lệ nép dưới hoa | **Thúy Kiều & Thúy Vân** | `04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png` | `thuy_kieu_maiden + thuy_van` |
| `ep01_scene06_shot09` | Bốn mắt chạm nhau qua hoa | **Thúy Kiều** (POV chàng Kim) | `04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png` | `thuy_kieu_maiden` |
| `ep01_scene06_shot10` | Cận cảnh Kim Trọng bàng hoàng | **Kim Trọng** | `04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png` | `kim_trong` |
| `ep01_scene06_shot11` | Cận cảnh Thúy Kiều má ửng hồng | **Thúy Kiều** | `04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png` | `thuy_kieu_maiden` |
| `ep01_scene06_shot12` | Kim Trọng xuống ngựa cúi chào | **Kim Trọng** | `04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png` | `kim_trong` |
| `ep01_scene06_shot13` | Ba chị em tạ từ rảo bước | **Thúy Kiều & Vương Gia** | `04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png` | `thuy_kieu_maiden + vuong_quan + thuy_van` |
| `ep01_scene06_shot14` | Kim Trọng đứng chôn chân nhìn | **Kim Trọng** | `04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png` | `kim_trong` |

#### 2.3. Ma trận Quy chuẩn Chuẩn xác cho Scene 05 (Shots Trọng Điểm)

| Shot ID | Tiêu đề Kịch bản | Nhân vật Chính | `character_asset_ref` Chuẩn (`muse_prompts.json`) | `character_anchor` Chuẩn (`banana_prompts.json`) |
| :--- | :--- | :--- | :--- | :--- |
| `ep01_scene05_shot13` | Trung cận Vương Quan (R4) | **Vương Quan** | `04_Assets/characters/02_Vuong_Family_And_Fate/vuong_quan_16yo_720p.png` | `vuong_quan` |
| `ep01_scene05_shot16` | Trung cảnh Vương Quan thuật chuyện | **Vương Quan** | `04_Assets/characters/02_Vuong_Family_And_Fate/vuong_quan_16yo_720p.png` | `vuong_quan` |
| `ep01_scene05_shot24` | Trung cận Vương Quan can ngăn (R4) | **Vương Quan** | `04_Assets/characters/02_Vuong_Family_And_Fate/vuong_quan_16yo_720p.png` | `vuong_quan` |

---

### 3. KHUYẾT TẬT 3: THIẾT KẾ CƠ CHẾ BẢO VỆ SONG PHƯƠNG & ĐA NHÂN VẬT (BILATERAL CHARACTER SAFEGUARDS)

Trong `05_Production_Pipeline/production_orchestrator.py`, cơ chế `resolve_start_frame` cần được nâng cấp toàn diện để đảm bảo tính bất biến (invariants):
1. **Thúy Kiều Invariant**: Nếu cú máy thuộc về Thúy Kiều (`is_thuy_kieu`), Start Frame TUYỆT ĐỐI KHÔNG mang khuôn mặt Kim Trọng.
2. **Kim Trọng Invariant**: Nếu cú máy thuộc về Kim Trọng (`is_kim_trong`), Start Frame TUYỆT ĐỐI KHÔNG mang khuôn mặt Thúy Kiều.
3. **Vương Tộc Invariant**: Nếu cú máy thuộc về Vương Quan (`is_vuong_quan`) hoặc Vương Ông (`is_vuong_ong`), Start Frame TUYỆT ĐỐI KHÔNG mang khuôn mặt Thúy Kiều.

#### 3.1. Mã nguồn Thiết kế Chi tiết (Implementation Blueprint)

##### A. Nâng cấp `get_character_anchor()` (Dòng 170-176):
Bổ sung cơ chế fallback thông minh phân tích tiêu đề và nội dung prompt khi anchor rỗng hoặc chưa nạp:

```python
def get_character_anchor(shot_id: str, shot_data: Optional[Dict] = None) -> str:
    """Trích xuất character_anchor từ shot_data hoặc từ kho prompt Banana."""
    if shot_data and "character_anchor" in shot_data:
        return str(shot_data["character_anchor"]).strip()
    bdata = get_banana_prompt_data(shot_id)
    if bdata and "character_anchor" in bdata:
        return str(bdata["character_anchor"]).strip()
    
    # Heuristic fallback dựa trên tiêu đề cảnh nếu anchor không tồn tại
    if shot_data:
        title = str(shot_data.get("scene_title", "")).lower()
        prompt = str(shot_data.get("motion_prompt", "")).lower()
        combined = f"{title} {prompt}"
        if "kim trọng" in combined and "thúy kiều" not in combined and "nàng kiều" not in combined:
            return "kim_trong"
        if "vương quan" in combined and "thúy kiều" not in combined:
            return "vuong_quan"
        if "vương ông" in combined and "thúy kiều" not in combined:
            return "vuong_ong"
        if ("thúy kiều" in combined or "nàng kiều" in combined) and "kim trọng" not in combined:
            return "thuy_kieu_maiden"
            
    return ""
```

##### B. Nâng cấp `resolve_start_frame()` (Dòng 319-389):

```python
    curr_anchor = get_character_anchor(shot_id, shot_data)
    is_kim_trong = "kim_trong" in curr_anchor and "thuy_kieu" not in curr_anchor
    is_thuy_kieu = "thuy_kieu" in curr_anchor and "kim_trong" not in curr_anchor
    is_vuong_quan = "vuong_quan" in curr_anchor and "thuy_kieu" not in curr_anchor
    is_vuong_ong = "vuong_ong" in curr_anchor and "thuy_kieu" not in curr_anchor
    is_thuy_van = "thuy_van" in curr_anchor and "thuy_kieu" not in curr_anchor
    resolved_path: Optional[str] = None

    # A. Tìm asset tham chiếu trong prompt data (kèm alias mapping và Bilateral Safeguards)
    asset_ref = shot_data.get("character_asset_ref")
    if asset_ref:
        ref_lower = str(asset_ref).lower()
        # Bilateral & Multi-character Safeguards chống gán nhầm chéo:
        if is_kim_trong and "thuy_kieu" in ref_lower:
            asset_ref = "04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png"
        elif is_thuy_kieu and "kim_trong" in ref_lower:
            asset_ref = "04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png"
        elif is_vuong_quan and "thuy_kieu" in ref_lower:
            asset_ref = "04_Assets/characters/02_Vuong_Family_And_Fate/vuong_quan_16yo_720p.png"
        elif is_vuong_ong and "thuy_kieu" in ref_lower:
            asset_ref = "04_Assets/characters/02_Vuong_Family_And_Fate/vuong_ong_55yo_720p.png"
        elif is_thuy_van and "thuy_kieu" in ref_lower:
            asset_ref = "04_Assets/characters/01_Main_Protagonists/thuy_van_maiden_16yo_720p.png"

        asset_path = BASE_DIR / asset_ref
        if asset_path.exists():
            resolved_path = str(asset_path.resolve())
        else:
            alias_map = {
                "vuong_ong_patriarch_55yo_720p.png": "04_Assets/characters/02_Vuong_Family_And_Fate/vuong_ong_55yo_720p.png",
                "vuong_ba_matriarch_50yo_720p.png": "04_Assets/characters/02_Vuong_Family_And_Fate/vuong_ba_50yo_720p.png",
                "vuong_quan_young_14yo_720p.png": "04_Assets/characters/02_Vuong_Family_And_Fate/vuong_quan_16yo_720p.png",
                "vuong_quan_14yo_720p.png": "04_Assets/characters/02_Vuong_Family_And_Fate/vuong_quan_16yo_720p.png",
                "imperial_guards_brutal_officials_720p.png": "04_Assets/characters/05_Imperial_Court_And_Officials/sai_nha_720p.png",
                "sai_nha_feudal_guards_720p.png": "04_Assets/characters/05_Imperial_Court_And_Officials/sai_nha_720p.png",
            }
            fname = Path(asset_ref).name
            if fname in alias_map:
                alias_path = BASE_DIR / alias_map[fname]
                if alias_path.exists():
                    resolved_path = str(alias_path.resolve())
            elif asset_ref in alias_map:
                alias_path = BASE_DIR / alias_map[asset_ref]
                if alias_path.exists():
                    resolved_path = str(alias_path.resolve())

    # B. Nếu chưa tìm thấy và có character_anchor: tra cứu trực tiếp trong kho Characters
    if not resolved_path and curr_anchor:
        portrait = resolve_character_portrait_from_anchor(curr_anchor)
        if portrait and portrait.exists():
            resolved_path = str(portrait.resolve())

    # C. Kiểm tra reference_start_frame nếu là đường dẫn hợp lệ (chỉ khi không vi phạm nhân vật)
    if not resolved_path:
        ref_sf = shot_data.get("reference_start_frame", "")
        if ref_sf:
            if take_type == "CONTINUOUS_TAKE":
                m_ref = re.search(r"(ep\d+_scene\d+_shot\d+)", ref_sf, re.IGNORECASE)
                if m_ref:
                    ref_shot_id = m_ref.group(1)
                    ref_tail = find_tail_frame(ref_shot_id)
                    if ref_tail:
                        resolved_path = str(ref_tail.resolve())
            ref_path = BASE_DIR / ref_sf
            if ref_path.exists() and ref_path.is_file():
                ref_name = ref_path.name.lower()
                violation = (
                    (is_kim_trong and "thuy_kieu" in ref_name) or
                    (is_thuy_kieu and "kim_trong" in ref_name) or
                    (is_vuong_quan and "thuy_kieu" in ref_name) or
                    (is_vuong_ong and "thuy_kieu" in ref_name)
                )
                if not violation:
                    resolved_path = str(ref_path.resolve())

    # D. Final Bilateral & Multi-character Safeguards: Ghi đè tuyệt đối nếu có vi phạm
    if resolved_path:
        res_lower = resolved_path.lower()
        if is_kim_trong and "thuy_kieu" in res_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
        elif is_thuy_kieu and "kim_trong" in res_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
        elif is_vuong_quan and "thuy_kieu" in res_lower:
            vq_portrait = CHARACTERS_DIR / "02_Vuong_Family_And_Fate" / "vuong_quan_16yo_720p.png"
            resolved_path = str(vq_portrait.resolve()) if vq_portrait.exists() else None
        elif is_vuong_ong and "thuy_kieu" in res_lower:
            vo_portrait = CHARACTERS_DIR / "02_Vuong_Family_And_Fate" / "vuong_ong_55yo_720p.png"
            resolved_path = str(vo_portrait.resolve()) if vo_portrait.exists() else None

    # E. Cinematic Cut invariant: NEVER return prev_tail!
    if take_type == "CINEMATIC_CUT" and resolved_path and scene_prefix and shot_num > 1:
        prev_shot_id = f"{scene_prefix}_shot{shot_num - 1:02d}"
        prev_tail_dir = KEYFRAMES_DIR / prev_shot_id
        if str(prev_tail_dir).lower() in resolved_path.lower():
            resolved_path = None
            if curr_anchor:
                p = resolve_character_portrait_from_anchor(curr_anchor)
                if p and p.exists():
                    resolved_path = str(p.resolve())
```

---

### 4. KHUYẾT TẬT 4: GIA CỐ HARNESS KIỂM THỬ TRONG `test_critic_gate.py`

#### 4.1. Phân tích Lỗ hổng Cũ
Trong `tests/test_critic_gate.py`, các bài test gọi `po.resolve_start_frame(sid, {})`. Việc truyền dictionary rỗng `{}` đã biến test case thành một dạng "unit test tự xướng" (vacuous test):
- Tham số `{}` khiến `shot_data.get("character_asset_ref")` luôn trả về `None`.
- Chương trình bỏ qua hoàn toàn nội dung JSON của `muse_prompts.json`.
- Sự cố Thúy Kiều bị ghi đè Kim Trọng không bị phát hiện.

#### 4.2. Thiết kế Cấu trúc Test Mới trong `test_critic_gate.py`

Thay thế `TestKimTrongSafeguard` (dòng 380-416) bằng nhóm test hoàn chỉnh sử dụng dữ liệu thực tế:

```python
# =============================================================================
# 5. TEST BILATERAL & MULTI-CHARACTER SAFEGUARDS WITH LIVE DATA
# =============================================================================

class TestBilateralAndCharacterSafeguards:
    """Kiểm thử xác thực nhân vật với dữ liệu thực tế từ po.get_all_shots()."""

    @pytest.fixture(scope="class")
    def all_shots(self):
        return po.get_all_shots()

    def test_cinematic_cut_with_live_shot_data_never_returns_tail(self, all_shots):
        """Cinematic Cut với dữ liệu shot thực tế không được phép trả về tail frame của shot trước."""
        sid = "ep01_scene02_shot08"
        sdata = all_shots.get(sid, {})
        resolved = po.resolve_start_frame(sid, sdata)
        assert resolved is not None
        assert "ep01_scene02_shot07" not in resolved
        assert "clean_frame_239" not in resolved
        assert "vuong_quan" in resolved.lower()

    def test_scene10_live_shots_resolve_authentically(self, all_shots):
        """Scene 10: Kiều độc tấu tỳ bà phải ra Thúy Kiều; Kim Trọng tương tư phải ra Kim Trọng."""
        # Kiều solo & climax pipa shots:
        kieu_shots = [
            "ep01_scene10_shot03",  # Kiều qua rào sang thư phòng
            "ep01_scene10_shot06",  # Kiều giãi bày tấc lòng son
            "ep01_scene10_shot11",  # Kiều ôm đàn tỳ bà so dây dưới trăng
            "ep01_scene10_shot12",  # Tiếng đàn tỳ bà réo rắt khúc tương tư
        ]
        for sid in kieu_shots:
            sdata = all_shots.get(sid, {})
            resolved = po.resolve_start_frame(sid, sdata)
            assert resolved is not None, f"Shot {sid} phải resolve được Start Frame!"
            assert "thuy_kieu" in resolved.lower(), f"{sid} phải ra chân dung Thúy Kiều, thực tế: {resolved}"
            assert "kim_trong" not in resolved.lower(), f"{sid} (Kiều) không được phép ra Kim Trọng: {resolved}"

        # Kim Trọng shots:
        kim_shots = [
            "ep01_scene10_shot04",  # Kim Trọng mở cửa đón Kiều
            "ep01_scene10_shot05",  # Kim Trọng nâng niu bàn tay Kiều
            "ep01_scene10_shot13",  # Kim Trọng say đắm lắng nghe
        ]
        for sid in kim_shots:
            sdata = all_shots.get(sid, {})
            resolved = po.resolve_start_frame(sid, sdata)
            assert resolved is not None, f"Shot {sid} phải resolve được Start Frame!"
            assert "kim_trong" in resolved.lower(), f"{sid} phải ra chân dung Kim Trọng, thực tế: {resolved}"
            assert "thuy_kieu" not in resolved.lower(), f"{sid} (Kim Trọng) không được phép ra Thúy Kiều: {resolved}"

    def test_scene06_live_shots_resolve_authentically(self, all_shots):
        """Scene 06: Các cú máy Kim Trọng, Thúy Kiều và Vương Quan resolve chuẩn xác."""
        kim_shots = [
            "ep01_scene06_shot03", "ep01_scene06_shot04", "ep01_scene06_shot06",
            "ep01_scene06_shot10", "ep01_scene06_shot12", "ep01_scene06_shot14"
        ]
        for sid in kim_shots:
            sdata = all_shots.get(sid, {})
            resolved = po.resolve_start_frame(sid, sdata)
            assert resolved is not None, f"Shot {sid} phải resolve được Start Frame!"
            assert "kim_trong" in resolved.lower(), f"{sid} phải ra Kim Trọng, nhận: {resolved}"
            assert "thuy_kieu" not in resolved.lower(), f"Leak Thúy Kiều tại Kim Trọng shot {sid}: {resolved}"
            assert "vuong_quan" not in resolved.lower(), f"Leak Vương Quan tại Kim Trọng shot {sid}: {resolved}"

        # Vương Quan shot 07
        s7_data = all_shots.get("ep01_scene06_shot07", {})
        s7_res = po.resolve_start_frame("ep01_scene06_shot07", s7_data)
        assert s7_res is not None
        assert "vuong_quan" in s7_res.lower()

        # Thúy Kiều shots 08, 09, 11
        kieu_shots = ["ep01_scene06_shot08", "ep01_scene06_shot09", "ep01_scene06_shot11"]
        for sid in kieu_shots:
            sdata = all_shots.get(sid, {})
            res = po.resolve_start_frame(sid, sdata)
            assert res is not None
            assert "thuy_kieu" in res.lower(), f"{sid} phải ra Thúy Kiều, nhận: {res}"

    def test_scene05_live_vuong_quan_shots_resolve_authentically(self, all_shots):
        """Scene 05: Các cú máy Vương Quan thuật chuyện và can ngăn không bị biến thành Thúy Kiều."""
        for sid in ["ep01_scene05_shot13", "ep01_scene05_shot16", "ep01_scene05_shot24"]:
            sdata = all_shots.get(sid, {})
            resolved = po.resolve_start_frame(sid, sdata)
            assert resolved is not None, f"Shot {sid} phải resolve được Start Frame!"
            assert "vuong_quan" in resolved.lower(), f"{sid} phải ra Vương Quan, nhận: {resolved}"
            assert "thuy_kieu" not in resolved.lower(), f"Leak Thúy Kiều tại Vương Quan shot {sid}: {resolved}"

    def test_adversarial_bilateral_safeguards_override(self):
        """Kiểm tra phản ứng đối kháng (Adversarial Robustness): Gán nhầm asset đối nghịch phải bị ghi đè tự động."""
        # 1. Cố tình gán Thúy Kiều vào cú máy Kim Trọng -> Phải tự sửa thành Kim Trọng
        adv_kt = {
            "character_asset_ref": "04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png",
            "scene_title": "Kim Trọng tương tư dưới trăng"
        }
        res_kt = po.resolve_start_frame("ep01_scene07_shot01", adv_kt)
        assert res_kt is not None
        assert "kim_trong" in res_kt.lower()
        assert "thuy_kieu" not in res_kt.lower()

        # 2. Cố tình gán Kim Trọng vào cú máy Kiều đàn tỳ bà -> Phải tự sửa thành Thúy Kiều
        adv_tk = {
            "character_asset_ref": "04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png",
            "scene_title": "Kiều gảy đàn tỳ bà réo rắt khúc tương tư"
        }
        res_tk = po.resolve_start_frame("ep01_scene10_shot11", adv_tk)
        assert res_tk is not None
        assert "thuy_kieu" in res_tk.lower()
        assert "kim_trong" not in res_tk.lower()

        # 3. Cố tình gán Thúy Kiều vào cú máy Vương Quan can ngăn -> Phải tự sửa thành Vương Quan
        adv_vq = {
            "character_asset_ref": "04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png",
            "scene_title": "Trung cận Vương Quan can ngăn"
        }
        res_vq = po.resolve_start_frame("ep01_scene05_shot24", adv_vq)
        assert res_vq is not None
        assert "vuong_quan" in res_vq.lower()
        assert "thuy_kieu" not in res_vq.lower()

        # 4. Cố tình gán Thúy Kiều vào cú máy Vương Ông đạo mạo -> Phải tự sửa thành Vương Ông
        adv_vo = {
            "character_asset_ref": "04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png",
            "scene_title": "Vương Ông đĩnh đạc gia phong"
        }
        res_vo = po.resolve_start_frame("ep01_scene02_shot04", adv_vo)
        assert res_vo is not None
        assert "vuong_ong" in res_vo.lower()
        assert "thuy_kieu" not in res_vo.lower()
```

#### 4.3. Lưu ý Quan trọng về Tệp Thử nghiệm Đối kháng của Challenger 2 (`test_m1_challenger2_probe.py`)
Trong `tests/test_m1_challenger2_probe.py`:
- Test `test_scene06_kim_trong_anomaly_audit` hiện đang **ASSERT RẰNG LỖI CÒN TỒN TẠI** (`assert "ep01_scene06_shot06" in anomaly_shot_ids`).
- Test `test_vuong_quan_scene05_uncovered_defects` hiện đang **ASSERT RẰNG CÓ 2 LỖI** (`assert len(defects) == 2`).
- Test `test_kim_trong_11_fixes_present_in_master` hiện đang kiểm tra rằng cả 11 shot (từ shot 03 đến shot 12) đều có Kim Trọng trong master (`assert "kim_trong" in ref.lower()`).
Khi Implementer áp dụng các bản sửa lỗi chính xác, các test trên của Challenger 2 sẽ tự động báo FAIL vì các lỗ hổng đã được khắc phục hoàn toàn. Implementer cần cập nhật lại các câu lệnh assert trong probe file để kiểm tra trạng thái **0 ANOMALIES & 0 DEFECTS**.

---

## III. QUY TRÌNH THỰC THI DÀNH CHO IMPLEMENTER (STEP-BY-STEP RECIPE)

Để Implementer (hoặc Worker) triển khai chính xác 100%, thực hiện theo đúng trình tự sau:

### Bước 1: Sửa đổi `episodes/ep01/prompts/muse_prompts.json`
1. Cập nhật Scene 10:
   - Đặt `"character_asset_ref": "04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png"` cho:
     - `ep01_scene10_shot03`
     - `ep01_scene10_shot06`
     - `ep01_scene10_shot09`
     - `ep01_scene10_shot11`
     - `ep01_scene10_shot12`
   - Đặt `"character_asset_ref": "04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png"` cho:
     - `ep01_scene10_shot04`
     - `ep01_scene10_shot05`
     - `ep01_scene10_shot07`
     - `ep01_scene10_shot08`
     - `ep01_scene10_shot10`
     - `ep01_scene10_shot13`
2. Cập nhật Scene 06:
   - Bổ sung `character_asset_ref` cho toàn bộ các shot theo Ma trận Phần II §2.2:
     - Kim Trọng: `shot03`, `shot04`, `shot06`, `shot10`, `shot12`, `shot14` -> `kim_trong_18yo_720p.png`.
     - Tiểu Đồng: `shot05` -> `tieu_dong_kim_trong_720p.png`.
     - Vương Quan: `shot07` -> `vuong_quan_16yo_720p.png`.
     - Thúy Kiều: `shot08`, `shot09`, `shot11`, `shot13` -> `thuy_kieu_maiden_16yo_720p.png`.
3. Cập nhật Scene 05:
   - Bổ sung `"character_asset_ref": "04_Assets/characters/02_Vuong_Family_And_Fate/vuong_quan_16yo_720p.png"` cho:
     - `ep01_scene05_shot13`
     - `ep01_scene05_shot16`
     - `ep01_scene05_shot24`

### Bước 2: Sửa đổi `02_AI_Prompts/gemini_banana_prompts.json` & `episodes/ep01/prompts/banana_prompts.json`
1. Cập nhật Scene 10:
   - `ep01_scene10_shot04`: `"character_anchor": "kim_trong"`
   - `ep01_scene10_shot06`: `"character_anchor": "thuy_kieu_maiden"`
   - `ep01_scene10_shot09`: `"character_anchor": "thuy_kieu_maiden"`
   - `ep01_scene10_shot11`: `"character_anchor": "thuy_kieu_maiden"`
   - `ep01_scene10_shot12`: `"character_anchor": "thuy_kieu_maiden"`
2. Cập nhật Scene 06:
   - Hiệu chỉnh lại toàn bộ `character_anchor` theo Ma trận Phần II §2.2.
3. Cập nhật Scene 05:
   - `ep01_scene05_shot13`: `"character_anchor": "vuong_quan"`
   - `ep01_scene05_shot16`: `"character_anchor": "vuong_quan"`
   - `ep01_scene05_shot24`: `"character_anchor": "vuong_quan"`

### Bước 3: Đồng bộ Master Prompts
Chạy lệnh đồng bộ 2 chiều:
```bash
python 05_Production_Pipeline/episode_manager.py --sync-to-master
```

### Bước 4: Nâng cấp `05_Production_Pipeline/production_orchestrator.py`
Triển khai cơ chế Bilateral Safeguards theo mã nguồn mục II §3.1.

### Bước 5: Cập nhật & Gia cố `tests/test_critic_gate.py`
Thay thế lớp test cũ bằng lớp test mới `TestBilateralAndCharacterSafeguards` theo mục II §4.2.

### Bước 6: Chạy Bộ Thẩm Định Kiểm Thử Toàn Diện
1. `python -m pytest tests/test_critic_gate.py -v` (Yêu cầu: 100% PASS).
2. `python -m pytest tests/test_m2_hygiene.py -v` (Yêu cầu: 6/6 PASS).
3. `python -m pytest tests/test_tier1_features.py -k "not test_render" -v` (Yêu cầu: 65/65 PASS).
4. `python -m pytest tests/test_m1_challenger2_probe.py -v` (sau khi đồng bộ assert thành 0 anomalies: 100% PASS).
