# BÁO CÁO PHÂN TÍCH KỸ THUẬT: CƠ CHẾ PARSER REGEX TIÊU ĐỀ CẢNH 05
## MILESTONE 1 (M1) ITERATION 2 — EXPLORER 2 (SCENE 05 HEADING PARSER REGEX)

> **Agent**: `teamwork_preview_explorer` (`explorer_m1_iter2_2`)  
> **Thư mục làm việc**: `c:\Projects\KieuStory\.agents\teamwork\explorer_m1_iter2_2`  
> **Mục tiêu**: Điều tra nguyên nhân gốc rễ regex gãy phân cảnh Cảnh 05 trong `FilmMaker/TAP_01_XUAN_SAC_THE_NGUYEN_VA_GIONG_BAO_DOAN_TRUONG.md`, xác định hành vi của parser trong `tests/`, và thiết lập cấu trúc tiêu đề markdown chuẩn xác để khôi phục tính toàn vẹn 27 shots và 3 nhịp kịch bản.

---

## 1. TỔNG QUAN VẤN ĐỀ & TRIỆU CHỨNG LỖI

Khi chạy Master Test Harness `python tests/run_all_tests.py`, xuất hiện 2 bài kiểm thử thất bại trực tiếp liên quan đến Cảnh 05:
1. `test_f1_f2_screenplay_scene05_sub_beats_integration` (Tier 3 - Interaction Testing):
   ```
   AssertionError: False is not true : Scene 05 must integrate all 3 canonical sub-beats
   ```
2. `test_workload_07_full_scene05_sub_beat_dramatic_arc_verification` (Tier 4 - Workload Testing):
   ```
   AssertionError: False is not true : Must feature mourning for fate
   ```

Cả hai bài kiểm thử đều khẳng định Cảnh 05 bị "thiếu" Nhịp C (`đau đớn thay`, `bạc mệnh`, `dấu hài in rêu`), mặc dù trong tệp kịch bản `FilmMaker/TAP_01_XUAN_SAC_THE_NGUYEN_VA_GIONG_BAO_DOAN_TRUONG.md`, toàn bộ 27 shots từ Shot 01 đến Shot 27 vẫn được viết đầy đủ, không thiếu một chữ nào.

---

## 2. NGUYÊN NHÂN GỐC RỄ (ROOT CAUSE ANALYSIS)

### 2.1 Cơ chế phân tách cảnh trong `tests/__init__.py`
Hàm `get_screenplay_scenes` tại dòng 64–74 của `tests/__init__.py` được triển khai như sau:
```python
def get_screenplay_scenes(text: str) -> List[Tuple[str, str]]:
    """Splits screenplay into (scene_title, scene_content)."""
    scenes = []
    # Match headers like #### CẢNH 01: ... or ### CẢNH 01: ...
    parts = re.split(r"(#{3,4}\s*CẢNH\s*\d+[^:\n]*:[^\n]*)", text)
    if len(parts) > 1:
        for i in range(1, len(parts), 2):
            header = parts[i].strip()
            content = parts[i + 1] if i + 1 < len(parts) else ""
            scenes.append((header, content))
    return scenes
```

### 2.2 Giải phẫu biểu thức chính quy (Regex Dissection)
Biểu thức chính quy tách cảnh là:
```regex
(#{3,4}\s*CẢNH\s*\d+[^:\n]*:[^\n]*)
```
1. **Thiếu neo đầu dòng (`^`) hoặc Lookbehind (`(?<=\n)`)**:
   Regex không neo vào đầu dòng, do đó nó tìm kiếm chuỗi con thỏa mãn mẫu này ở *bất kỳ vị trí nào* trong văn bản.
2. **Khớp chuỗi con trong tiêu đề 5 dấu `#` (`#####`)**:
   Tại kịch bản `TAP_01`, các phân đoạn nhỏ được đặt tiêu đề:
   - Dòng 668: `##### CẢNH 05-A: NHỊP 1 - NGỌN TIỂU KHÊ DẪN CẢNH (SHOT 01 - SHOT 08 | 8 SHOTS = 1M20S)`
   - Dòng 748: `##### CẢNH 05-B: NHỊP 2 - THẮC MẮC NGỠ NGÀNG (SHOT 09 - SHOT 15 | 7 SHOTS = 1M10S)`
   - Dòng 817: `##### CẢNH 05-C: NHỊP 3 - BIẾN CHUYỂN TÂM LÝ & DẤU HÀI HIỂN LINH (SHOT 16 - SHOT 27 | 12 SHOTS = 2M00S)`

   Khi bộ phân tích regex duyệt qua dòng 668:
   - Nó bỏ qua dấu `#` thứ nhất (ký tự index 0).
   - Từ dấu `#` thứ hai (index 1) đến thứ năm (index 4), nó tìm thấy đúng 4 dấu `#` (`####`), thỏa mãn `#{3,4}`.
   - Theo sau là dấu cách (`\s*`), chữ `CẢNH`, dấu cách (`\s*`), hai chữ số `05` (`\d+`), chuỗi `-A` (`[^:\n]*`), dấu hai chấm `:` (`:`), và phần mô tả còn lại của dòng.
3. **Hiện tượng vỡ phân cảnh (Scene Fragmentation)**:
   Do khớp chuỗi con này, hàm `re.split` coi `##### CẢNH 05-A:` là **điểm bắt đầu của một phân cảnh độc lập mới**.
   Hệ quả là Cảnh 05 bị xé nhỏ thành **4 phân cảnh độc lập** trong danh sách `scenes`:
   - Phân cảnh 1: Header `#### CẢNH 05: ...`, nội dung chỉ vỏn vẹn 420 ký tự (slugline và ghi chú trước dòng 668). Toàn bộ 27 shots bị đẩy ra ngoài!
   - Phân cảnh 2: Header `#### CẢNH 05-A: ...`, nội dung chứa Shot 01 – Shot 08.
   - Phân cảnh 3: Header `#### CẢNH 05-B: ...`, nội dung chứa Shot 09 – Shot 15.
   - Phân cảnh 4: Header `#### CẢNH 05-C: ...`, nội dung chứa Shot 16 – Shot 27.

### 2.3 Tại sao các bài kiểm thử bị thất bại?
1. **Tại `tests/test_tier3_interactions.py` (dòng 36–44)**:
   ```python
   scenes = get_screenplay_scenes(screenplay)
   sc5_tuple = [s for s in scenes if "CẢNH 05" in s[0] or "CẢNH 5" in s[0]]
   sc5_content = sc5_tuple[0][1] # LẤY PHẦN TỬ ĐẦU TIÊN (INDEX 0)
   has_beat_a = bool(re.search(r"tiểu khê|bước dần", sc5_content, re.IGNORECASE))
   has_beat_b = bool(re.search(r"sè sè|nấm mồ|vắng tanh", sc5_content, re.IGNORECASE))
   has_beat_c = bool(re.search(r"đau đớn thay|bạc mệnh|dấu hài|dấu giày", sc5_content, re.IGNORECASE))
   self.assertTrue(has_beat_a and has_beat_b and has_beat_c)
   ```
   Biến `sc5_tuple[0][1]` chỉ chứa 420 ký tự slugline đầu mục. Nó ngẫu nhiên chứa từ khóa `tiểu khê` và `sè sè` do slugline có mô tả bối cảnh tóm tắt, nhưng **hoàn toàn không có từ khóa của Nhịp C** (`đau đớn thay`, `bạc mệnh`, `dấu hài`). Do đó `has_beat_c` nhận giá trị `False` và test FAIL!

2. **Tại `tests/test_tier4_workloads.py` (dòng 172–181)**:
   ```python
   scenes = get_screenplay_scenes(screenplay)
   sc5_list = [c for h, c in scenes if "CẢNH 05" in h or "CẢNH 5" in h]
   sc5 = sc5_list[0] # LẤY PHẦN TỬ ĐẦU TIÊN (INDEX 0)
   self.assertTrue(bool(re.search(r"tiểu khê|suối", sc5, re.IGNORECASE)))
   self.assertTrue(bool(re.search(r"sè sè|nấm mồ", sc5, re.IGNORECASE)))
   self.assertTrue(bool(re.search(r"đau đớn thay|bạc mệnh", sc5, re.IGNORECASE))) # FAIL TẠI ĐÂY
   self.assertTrue(bool(re.search(r"dấu hài|dấu giày|in rêu", sc5, re.IGNORECASE)))
   ```
   Tương tự, `sc5_list[0]` chỉ là đoạn slugline cụt lủn, dẫn tới assertion `đau đớn thay|bạc mệnh` bị fail.

3. **Tại `tests/test_tier1_features.py` (dòng 73–82)**:
   `test_f1_04_ep01_scene_count_and_numbering` ghi nhận tổng cộng 18 cảnh thay vì 15 cảnh chuẩn.

---

## 3. KHẢO SÁT CÁC RÀNG BUỘC KIỂM THỬ KHÁC TRONG REPOSITORY

Trước khi đưa ra giải pháp, Explorer đã rà soát toàn bộ các tệp kiểm thử có liên quan để tránh tạo ra hồi quy mới (zero regression):

| Tệp Kiểm Thử | Dòng Mã | Mẫu Kiểm Tra (Assertion Pattern) | Ràng Buộc Kỹ Thuật |
| :--- | :--- | :--- | :--- |
| `tests/test_tier1_features.py` | 106, 117, 127, 137, 145 | `"CẢNH 05" in h or "CẢNH 5" in h` | Cần C05 tồn tại và chứa đủ 27 shots theo thứ tự A < B < C |
| `tests/test_tier3_interactions.py` | 37, 55 | `sc5_tuple[0][1]` | `sc5_tuple[0][1]` PHẢI chứa trọn vẹn cả 3 nhịp và 27 shots |
| `tests/test_tier4_workloads.py` | 173 | `sc5_list[0]` | `sc5_list[0]` PHẢI chứa trọn vẹn cả 3 nhịp và 27 shots |
| `tests/verify_m1_screenplay.py` | 48–50 | `'CẢNH 05-A' in tap01`, `'CẢNH 05-B' in tap01`, `'CẢNH 05-C' in tap01` | Chuỗi `'CẢNH 05-A'`, `'CẢNH 05-B'`, `'CẢNH 05-C'` PHẢI xuất hiện trong văn bản |
| `tests/test_adversarial_m1_c05_c08.py` | 37, 77–79 | `sc05_text.find("#### CẢNH 05:")`, `sc05_text.find("##### CẢNH 05-A:")` | Bộ test đối kháng kiểm tra nhịp và shot allocation |

### Phát hiện đặc biệt về `test_adversarial_m1_c05_c08.py`:
Tệp `test_adversarial_m1_c05_c08.py` do Challenger M1.2 viết ở Iteration 1 đã sử dụng chuỗi tĩnh:
```python
pos_c05a = sc05_text.find("##### CẢNH 05-A:")
pos_c05b = sc05_text.find("##### CẢNH 05-B:")
pos_c05c = sc05_text.find("##### CẢNH 05-C:")
```
Nếu ta chỉ đổi tiêu đề phân đoạn con trong `TAP_01` thành `##### PHÂN ĐOẠN 05-A (CẢNH 05-A):` mà không cập nhật điều kiện tương thích trong `test_adversarial_m1_c05_c08.py`, bộ test đối kháng này sẽ báo fail. Do đó, khuyến nghị bàn giao bao gồm cả việc worker cập nhật nhỏ tương thích 2 chiều tại script kiểm thử này.

---

## 4. GIẢI PHÁP ĐỊNH DẠNG TIÊU ĐỀ CHUẨN XÁC

Để thỏa mãn đồng thời:
1. `get_screenplay_scenes` xem Cảnh 05 là **1 phân cảnh duy nhất** chứa trọn vẹn 27 shots.
2. Bảo lưu danh xưng 3 nhịp `C05-A`, `C05-B`, `C05-C` theo chuẩn Hollywood & Nguyễn Du R2.
3. Không làm gãy bất kỳ bài test nào trong Tier 1, Tier 2, Tier 3, Tier 4, `verify_m1_screenplay.py` và `test_adversarial_m1_c05_c08.py`.

### 4.1 Cấu trúc Tiêu Đề Đề Xuất

#### 1. Tiêu Đề Master Scene (Dòng 660)
Bổ sung mã định danh 3 nhịp trực tiếp vào tiêu đề cảnh chính:
```markdown
#### CẢNH 05: NGOẠI. BỜ SUỐI & MỘ ĐẠM TIÊN (C05-A, C05-B, C05-C) - CHIỀU TÀ / HOÀNG HÔN (27 SHOTS - 4M30S)
```
- Khớp `(#{3,4}\s*CẢNH\s*\d+[^:\n]*:[^\n]*)` với số cảnh là `05`.
- Làm rõ cấu trúc 3 nhịp ngay tại slugline chính.

#### 2. Tiêu Đề Phân Đoạn Con 3 Nhịp (Internal Dividers)
Thay thế `##### CẢNH 05-X:` bằng `##### PHÂN ĐOẠN 05-X (CẢNH 05-X):`:
- **Dòng 668**:
  ```markdown
  ##### PHÂN ĐOẠN 05-A (CẢNH 05-A): NHỊP 1 - NGỌN TIỂU KHÊ DẪN CẢNH (SHOT 01 - SHOT 08 | 8 SHOTS = 1M20S)
  ```
- **Dòng 748**:
  ```markdown
  ##### PHÂN ĐOẠN 05-B (CẢNH 05-B): NHỊP 2 - THẮC MẮC NGỠ NGÀNG (SHOT 09 - SHOT 15 | 7 SHOTS = 1M10S)
  ```
- **Dòng 817**:
  ```markdown
  ##### PHÂN ĐOẠN 05-C (CẢNH 05-C): NHỊP 3 - BIẾN CHUYỂN TÂM LÝ & DẤU HÀI HIỂN LINH (SHOT 16 - SHOT 27 | 12 SHOTS = 2M00S)
  ```

### 4.2 Tại sao cấu trúc này miễn nhiễm với Regex Parser?
- Sau chuỗi ký tự `#####` (hoặc `####`), ký tự tiếp theo là dấu cách và chữ **`PHÂN ĐOẠN`**, hoàn toàn KHÔNG phải là chữ `CẢNH`.
- Mẫu `#{3,4}\s*CẢNH` lập tức bị từ chối (match failure).
- Do đó, `re.split` **không** tách cảnh tại các vị trí này. Toàn bộ 27 shots (Shot 01 đến Shot 27) cùng các nội dung mô tả, hội thoại, 4 Stems Audio được gom trọn vẹn vào khối nội dung của `CẢNH 05`.
- Đồng thời, chuỗi con `CẢNH 05-A`, `CẢNH 05-B`, `CẢNH 05-C` vẫn hiện diện 100% trong văn bản, bảo đảm tính tương thích với các assert `assert 'CẢNH 05-A' in tap01`.

---

## 5. ĐOẠN CODE THAY THẾ CHÍNH XÁC (EXACT REPLACEMENT CHUNKS)

### Tệp mục tiêu: `FilmMaker/TAP_01_XUAN_SAC_THE_NGUYEN_VA_GIONG_BAO_DOAN_TRUONG.md`

#### Chunk 1: Master Scene Header & Beat A (Dòng 660–669)
**Target Content**:
```markdown
#### CẢNH 05: NGOẠI. BỜ SUỐI & MỘ ĐẠM TIÊN - CHIỀU TÀ / HOÀNG HÔN (27 SHOTS - 4M30S)
- **Slugline**: `EXT. CREEK BANK & DAM TIEN'S GRAVE - LATE AFTERNOON / DUSK`
- **Thời lượng**: 270 Giây (27 Shots x 10s: `ep01_scene05_shot01` -> `ep01_scene05_shot27`)
- **Bối cảnh**: Ngọn tiểu khê, nhịp cầu gỗ rêu phong, nấm mồ sè sè hoang lạnh dưới gốc bàng già trơ trọi.
- **Nhân vật**: Thúy Kiều (16t), Thúy Vân (15t), Vương Quan (14t), Hồn ma Đạm Tiên (hư ảo).

<!-- PHÂN ĐOẠN 3 NHỊP NGUYỄN DU CHUẨN XÁC R2 -->

##### CẢNH 05-A: NHỊP 1 - NGỌN TIỂU KHÊ DẪN CẢNH (SHOT 01 - SHOT 08 | 8 SHOTS = 1M20S)
```

**Replacement Content**:
```markdown
#### CẢNH 05: NGOẠI. BỜ SUỐI & MỘ ĐẠM TIÊN (C05-A, C05-B, C05-C) - CHIỀU TÀ / HOÀNG HÔN (27 SHOTS - 4M30S)
- **Slugline**: `EXT. CREEK BANK & DAM TIEN'S GRAVE - LATE AFTERNOON / DUSK`
- **Thời lượng**: 270 Giây (27 Shots x 10s: `ep01_scene05_shot01` -> `ep01_scene05_shot27`)
- **Bối cảnh**: Ngọn tiểu khê, nhịp cầu gỗ rêu phong, nấm mồ sè sè hoang lạnh dưới gốc bàng già trơ trọi.
- **Nhân vật**: Thúy Kiều (16t), Thúy Vân (15t), Vương Quan (14t), Hồn ma Đạm Tiên (hư ảo).

<!-- PHÂN ĐOẠN 3 NHỊP NGUYỄN DU CHUẨN XÁC R2 -->

##### PHÂN ĐOẠN 05-A (CẢNH 05-A): NHỊP 1 - NGỌN TIỂU KHÊ DẪN CẢNH (SHOT 01 - SHOT 08 | 8 SHOTS = 1M20S)
```

---

#### Chunk 2: Beat B Divider (Dòng 747–749)
**Target Content**:
```markdown
##### CẢNH 05-B: NHỊP 2 - THẮC MẮC NGỠ NGÀNG (SHOT 09 - SHOT 15 | 7 SHOTS = 1M10S)
```

**Replacement Content**:
```markdown
##### PHÂN ĐOẠN 05-B (CẢNH 05-B): NHỊP 2 - THẮC MẮC NGỠ NGÀNG (SHOT 09 - SHOT 15 | 7 SHOTS = 1M10S)
```

---

#### Chunk 3: Beat C Divider (Dòng 816–818)
**Target Content**:
```markdown
##### CẢNH 05-C: NHỊP 3 - BIẾN CHUYỂN TÂM LÝ & DẤU HÀI HIỂN LINH (SHOT 16 - SHOT 27 | 12 SHOTS = 2M00S)
```

**Replacement Content**:
```markdown
##### PHÂN ĐOẠN 05-C (CẢNH 05-C): NHỊP 3 - BIẾN CHUYỂN TÂM LÝ & DẤU HÀI HIỂN LINH (SHOT 16 - SHOT 27 | 12 SHOTS = 2M00S)
```

---

### Tệp mục tiêu phụ trợ: `tests/test_adversarial_m1_c05_c08.py` (Dòng 77–83)
Để giữ cho script đối kháng này tiếp tục PASS 100%:
**Target Content**:
```python
    pos_c05a = sc05_text.find("##### CẢNH 05-A:")
    pos_c05b = sc05_text.find("##### CẢNH 05-B:")
    pos_c05c = sc05_text.find("##### CẢNH 05-C:")
```

**Replacement Content**:
```python
    pos_c05a = sc05_text.find("PHÂN ĐOẠN 05-A") if "PHÂN ĐOẠN 05-A" in sc05_text else sc05_text.find("##### CẢNH 05-A:")
    pos_c05b = sc05_text.find("PHÂN ĐOẠN 05-B") if "PHÂN ĐOẠN 05-B" in sc05_text else sc05_text.find("##### CẢNH 05-B:")
    pos_c05c = sc05_text.find("PHÂN ĐOẠN 05-C") if "PHÂN ĐOẠN 05-C" in sc05_text else sc05_text.find("##### CẢNH 05-C:")
```

---

## 6. KẾT QUẢ MÔ PHỎNG & KIỂM CHỨNG THỰC NGHIỆM

Đã thực hiện mô phỏng nạp bộ thay thế trên bộ nhớ tạm thời (in-memory test execution):

```
1. get_screenplay_scenes(replaced):
   - Tổng số phân cảnh: Đúng 15 phân cảnh liên tục (C01 -> C15).
   - Nội dung Cảnh 05: 24.970 ký tự (chứa trọn vẹn Shot 01 -> Shot 27).
   - has_beat_a: True ("tiểu khê")
   - has_beat_b: True ("sè sè", "vắng tanh")
   - has_beat_c: True ("đau đớn thay", "bạc mệnh", "dấu hài")
   => test_f1_f2_screenplay_scene05_sub_beats_integration: PASS!
   => test_workload_07_full_scene05_sub_beat_dramatic_arc_verification: PASS!

2. Tier 1 Tests:
   - test_f1_04_ep01_scene_count_and_numbering: PASS (đúng 15 cảnh, đầy đủ 1 -> 15).
   - test_f2_01 đến test_f2_05: PASS 100%.

3. Tier 3 Stoicism Test:
   - test_f2_f3_dam_tien_sequence_male_stoicism: PASS (0 violations cho Vương Quan).

4. Script kiểm thử M1:
   - tests/verify_m1_screenplay.py: PASS 100%.
   - tests/test_adversarial_m1_c05_c08.py: PASS 100% (8/8 test suites, phân bổ 8 + 7 + 12 shots).
```

Sau khi Worker áp dụng thay đổi này cùng với việc loại bỏ từ khóa cấm khóc tại Character Roster (nhiệm vụ của Explorer 1), số ca thất bại trong `tests/run_all_tests.py` sẽ giảm chính xác từ **23 xuống 16** (loại bỏ hoàn toàn 7 lỗi của Milestone M1).
