# Bug R 真因 Handoff — diff_service annotate 同 party_uid duplicate entry + unlink 越權砍 8 row

| 項目 | 內容 |
|---|---|
| 緣由 | Bug P/Q/R session 走完 fix + user 真實 reproduce 後，發現 Bug R **根本不是 wire 漏 1 行**，是 diff_service.annotate_parse_result 對同 party_uid emit 兩個 entry (一個 `changed`、一個 `gone`)，後續 unlink_parties 一次砍 8 個 link rows（含跨 context）— 嚴重 data corruption side effect |
| Branch | `feature/ssp-oscal-alignment`（接續 H-N + Bug O + P/Q arc，這是 Bug R 第二輪 deeper dive）|
| 接手前必讀 | 本文件自包含 + 上一份 P/Q/R handoff + Bug O FIXED-SUMMARY |
| 預估時間 | 4-8 小時（深度跨 diff_service / write_strategy / unlink 邏輯 + 跨 context_type 影響面）|

---

## 0. 接手讀序

1. **本文件** — 真因 + 現有 evidence + revert 紀錄 + open questions
2. **上一份 handoff**：`docs/features/FR-028-2605-ssp-oscal-alignment/handoff/2026-05-25-bug-p-q-r-parties-form-handoff.md` — 了解 Bug P/Q/R session 的原始假設
3. **Bug O FIXED-SUMMARY**：`docs/features/FR-028-2605-ssp-oscal-alignment/handoff/2026-05-25-bug-o-FIXED-SUMMARY.md` — Bug R 的 cache invalidate fix 是接續 Bug O annotate 改動
4. **關鍵 BE files**：
   - `app/oscal/service/ssp_docx_import_app_service.py` — `confirm_import` (line 540~) / `_filter_parties_for_write` (line 1281~) / `_load_current_parties` / `_collect_parties_to_unlink`
   - `app/oscal/service/ssp_docx_diff_service.py` — `annotate_parse_result` / `_annotate_party` (這條是真因區)
   - `domain/oscal/service/write_strategy/module_frame_write_strategy.py` — `write_parties` / `unlink_parties`
   - `app/oscal/service/party_context_service.py` — `list_parties` / `update_party`
5. **FE 已 ship 的 fix** (本 session commit 但**沒 push**)：
   - `551c7dc` Bug R cache invalidate
   - `fa43b79` Bug Q role Dropdown + Bug P button wording
   - 還有 3 個 file uncommitted：`PartiesSection.vue` (Dropdown 改放 grid 內)、`PartyLinkDialog.vue` (拿掉 code filter)、`SspDocxImportPage.vue` (加 contentOverrides.parties collect)
6. **FE 跨 repo 必讀**：`~/Projects/Billows/Audit-Manager/compliance-manager-fe/CLAUDE.md`

讀完跑 §6 Pre-flight + §7 Verify → §3 開工順位。

---

## 1. 症狀

### 1.1 User-facing

- mf `2c4ba0b8-52e2-4e90-a8fc-df8dcf481002` (AOO AOOA CMMC Level 1 導入計劃) → 批次維護 → 從文件匯入 → 上傳 `~/Desktop/亞航-CMMC-SSP-20260520-1會議討論版.docx`
- STEP 2 每個 tab 都選「全部採用新值」
- STEP 3 「參與人員與單位」tab：對 1 org + 4 person 各選任意 role
- STEP 3 「系統元件與外部授權服務」tab：3 系統依賴元件「狀態」都選「運作中」
- 點右上「確認匯入」→ router push 回 template-edit page
- **預期**：parties 看到 role 已套用、components 「狀態」欄顯示「運作中」
- **實際**：
  1. user image #5（`/Users/chouraymond/.claude/image-cache/bfd49895-5cac-4f87-b6df-94379d7b0a8e/5.png`）顯示**參與人員 8 筆**（原 4 + 新 4 同名但 role 不同的 row）
  2. user image #6 顯示**系統依賴元件 4 筆「狀態」欄全空 (—)**
  3. 整體像是 append + 部分舊資料保留 + STEP 3 inline edit 部分沒生效

### 1.2 BE log 證據（最新一次 18:41:51 confirm）

```
[ssp-confirm] _filter_parties_for_write parties_decisions count=5 map={
  'ecbe22dd-5ee8-4c43-aef3-6017f9eee9bb': 'use_docx',
  '5a1c53b0-97d0-42c3-9d8e-e424ad69c364': 'use_docx',
  '9c2c6408-3094-4dc4-b993-b5bb40800631': 'use_docx',
  'f1d242db-ce61-49d6-866e-14dc97f4c71b': 'use_docx',
  '9ef01730-631f-4c30-8e41-7bbdea8f362b': 'use_docx',
}

# 5 個 parsed parties annotated 第一輪 — diff_status 正常
[ssp-confirm] annotated party uid=ecbe22dd diff_status=unchanged decision=use_docx
[ssp-confirm] annotated party uid=5a1c53b0 diff_status=changed decision=use_docx
[ssp-confirm] annotated party uid=9c2c6408 diff_status=changed decision=use_docx
[ssp-confirm] annotated party uid=f1d242db diff_status=changed decision=use_docx
[ssp-confirm] annotated party uid=9ef01730 diff_status=changed decision=use_docx

# ⚠️ DUPLICATE！同 4 個 person uid 又出現第二次，這次標 diff_status=gone
[ssp-confirm] annotated party uid=5a1c53b0 diff_status=gone decision=use_docx
[ssp-confirm] annotated party uid=9c2c6408 diff_status=gone decision=use_docx
[ssp-confirm] annotated party uid=f1d242db diff_status=gone decision=use_docx
[ssp-confirm] annotated party uid=9ef01730 diff_status=gone decision=use_docx

# 接著 inject target_party_uid 對 5 個 matched parsed (含 1 org + 4 person)
[ssp-confirm] party uid=ecbe22dd: injecting target_party_uid=ecbe22dd for matched update
... (4 person 同樣 inject)

# 然後 4 個 gone person 走 unlink path
[ssp-confirm] party uid=5a1c53b0 gone+use_docx → unlink path (skipped from write)
[ssp-confirm] party uid=9c2c6408 gone+use_docx → unlink path (skipped from write)
[ssp-confirm] party uid=f1d242db gone+use_docx → unlink path (skipped from write)
[ssp-confirm] party uid=9ef01730 gone+use_docx → unlink path (skipped from write)

# resolved 5 parsed parties to write — 但其中 4 個也被排進 unlink
[ssp-confirm] _filter_parties_for_write resolved 5 parsed parties to write
[ssp-confirm] calling ModuleFrameWriteStrategy.write_parties with 5 parties...
[ssp-confirm] strategy.write_parties returned parties_written=5

# ⚠️ Unlink 一次砍 8 row（不是 4 — 為什麼？跨 context 越權砍？）
[ssp-confirm] calling ModuleFrameWriteStrategy.unlink_parties with 4 uids
[ssp-confirm] strategy.unlink_parties removed 8 link rows
```

### 1.3 DB 證據（confirm 後立刻 query）

```sql
-- mf 372 對應的 responsible_parties link：confirm 完只剩 1 個
SELECT rp.id, rp.role_id, rp.party_uuid, p.name, p.party_type
  FROM oscal.oscal_responsible_parties rp
  LEFT JOIN oscal.oscal_parties p ON p.uid::text = rp.party_uuid
 WHERE rp.context_type = 'module_frame' AND rp.context_id = 372;
-- 結果：1 row (id=236, 屏東飛機維修廠, organization)
-- 預期：5 row (1 org + 4 person, 每個帶 user STEP 3 改的 role)

-- oscal_parties 最大 id 仍是 1275（confirm 沒新增 row）
SELECT MAX(id) FROM oscal.oscal_parties;
-- 結果：1275 (1271-1275 是 mf 370 亞航 E2E Excel 測試的，不是本次寫的)
```

→ confirm 對 mf 372 只留 1 個 link (屏東飛機維修廠)，4 個 person link 全被 unlink。
→ BE write_parties 寫了 5 個（但寫到別處或 update 既有 row）+ unlink 8 個 link rows，結果 mf 372 context 內 4 person 全失。

### 1.4 用戶提供完整 confirm POST payload（**真實，請收）**

```
POST /api/1.0/ssp-docx-import/120ab8a5-aad6-422d-8152-2b209d0ef105/confirm
```

**原始 payload（未縮減，直接複製 user 提供內容）**：

```json
{
  "decisions": [
    {
      "control_id": "AC.L1-3.1.1",
      "action": "use_docx",
      "objectives": [
        { "objective_key": "(a)", "action": "use_docx" },
        { "objective_key": "(b)", "action": "use_docx" },
        { "objective_key": "(c)", "action": "use_docx" },
        { "objective_key": "(d)", "action": "use_docx" },
        { "objective_key": "(e)", "action": "use_docx" },
        { "objective_key": "(f)", "action": "use_docx" }
      ]
    },
    {
      "control_id": "AC.L1-3.1.2",
      "action": "use_docx",
      "objectives": [
        { "objective_key": "(a)", "action": "use_docx" },
        { "objective_key": "(b)", "action": "use_docx" }
      ]
    },
    {
      "control_id": "AC.L1-3.1.20",
      "action": "use_docx",
      "objectives": [
        { "objective_key": "(a)", "action": "use_docx" },
        { "objective_key": "(b)", "action": "use_docx" },
        { "objective_key": "(c)", "action": "use_docx" },
        { "objective_key": "(d)", "action": "use_docx" },
        { "objective_key": "(e)", "action": "use_docx" },
        { "objective_key": "(f)", "action": "use_docx" }
      ]
    },
    {
      "control_id": "AC.L1-3.1.22",
      "action": "use_docx",
      "objectives": [
        { "objective_key": "(a)", "action": "use_docx" },
        { "objective_key": "(b)", "action": "use_docx" },
        { "objective_key": "(c)", "action": "use_docx" },
        { "objective_key": "(d)", "action": "use_docx" },
        { "objective_key": "(e)", "action": "use_docx" }
      ]
    },
    {
      "control_id": "IA.L1-3.5.1",
      "action": "use_docx",
      "objectives": [
        { "objective_key": "(a)", "action": "use_docx" },
        { "objective_key": "(b)", "action": "use_docx" },
        { "objective_key": "(c)", "action": "use_docx" }
      ]
    },
    {
      "control_id": "IA.L1-3.5.2",
      "action": "use_docx",
      "objectives": [
        { "objective_key": "(a)", "action": "use_docx" },
        { "objective_key": "(b)", "action": "use_docx" },
        { "objective_key": "(c)", "action": "use_docx" }
      ]
    },
    {
      "control_id": "MP.L1-3.8.3",
      "action": "use_docx",
      "objectives": [
        { "objective_key": "(a)", "action": "use_docx" },
        { "objective_key": "(b)", "action": "use_docx" }
      ]
    },
    {
      "control_id": "PE.L1-3.10.1",
      "action": "use_docx",
      "objectives": [
        { "objective_key": "(a)", "action": "use_docx" },
        { "objective_key": "(b)", "action": "use_docx" },
        { "objective_key": "(c)", "action": "use_docx" },
        { "objective_key": "(d)", "action": "use_docx" }
      ]
    },
    {
      "control_id": "PE.L1-3.10.5",
      "action": "use_docx",
      "objectives": [
        { "objective_key": "(a)", "action": "use_docx" },
        { "objective_key": "(b)", "action": "use_docx" },
        { "objective_key": "(c)", "action": "use_docx" }
      ]
    },
    {
      "control_id": "SC.L1-3.13.1",
      "action": "use_docx",
      "objectives": [
        { "objective_key": "(a)", "action": "use_docx" },
        { "objective_key": "(b)", "action": "use_docx" },
        { "objective_key": "(c)", "action": "use_docx" },
        { "objective_key": "(d)", "action": "use_docx" },
        { "objective_key": "(e)", "action": "use_docx" },
        { "objective_key": "(f)", "action": "use_docx" },
        { "objective_key": "(g)", "action": "use_docx" },
        { "objective_key": "(h)", "action": "use_docx" }
      ]
    },
    {
      "control_id": "SC.L1-3.13.5",
      "action": "use_docx",
      "objectives": [
        { "objective_key": "(a)", "action": "use_docx" }
      ]
    },
    {
      "control_id": "SI.L1-3.14.1",
      "action": "use_docx",
      "objectives": [
        { "objective_key": "(a)", "action": "use_docx" },
        { "objective_key": "(b)", "action": "use_docx" },
        { "objective_key": "(c)", "action": "use_docx" },
        { "objective_key": "(d)", "action": "use_docx" },
        { "objective_key": "(e)", "action": "use_docx" },
        { "objective_key": "(f)", "action": "use_docx" }
      ]
    },
    {
      "control_id": "SI.L1-3.14.2",
      "action": "use_docx",
      "objectives": [
        { "objective_key": "(a)", "action": "use_docx" },
        { "objective_key": "(b)", "action": "use_docx" }
      ]
    },
    {
      "control_id": "SI.L1-3.14.4",
      "action": "use_docx",
      "objectives": []
    },
    {
      "control_id": "SI.L1-3.14.5",
      "action": "use_docx",
      "objectives": [
        { "objective_key": "(a)", "action": "use_docx" },
        { "objective_key": "(b)", "action": "use_docx" },
        { "objective_key": "(c)", "action": "use_docx" }
      ]
    }
  ],
  "parties_decisions": [
    { "party_uid": "ecbe22dd-5ee8-4c43-aef3-6017f9eee9bb", "action": "use_docx" },
    { "party_uid": "5a1c53b0-97d0-42c3-9d8e-e424ad69c364", "action": "use_docx" },
    { "party_uid": "9c2c6408-3094-4dc4-b993-b5bb40800631", "action": "use_docx" },
    { "party_uid": "f1d242db-ce61-49d6-866e-14dc97f4c71b", "action": "use_docx" },
    { "party_uid": "9ef01730-631f-4c30-8e41-7bbdea8f362b", "action": "use_docx" }
  ],
  "manual_assignments": [],
  "skipped_paragraph_idxs": [],
  "predicted_controls_user_selection": [
    "PE.L1-3.10.5", "AC.L1-3.1.22", "SI.L1-3.14.1", "SC.L1-3.13.5",
    "AC.L1-3.1.2", "SI.L1-3.14.5", "PE.L1-3.10.1", "IA.L1-3.5.1",
    "AC.L1-3.1.20", "IA.L1-3.5.2", "MP.L1-3.8.3", "SI.L1-3.14.4",
    "SI.L1-3.14.2", "SC.L1-3.13.1", "AC.L1-3.1.1"
  ],
  "source_uid": "2c4ba0b8-52e2-4e90-a8fc-df8dcf481002",
  "content_overrides": {
    "components": {
      "1": { "status": "operational" },
      "2": { "status": "operational" },
      "3": { "status": "operational" }
    },
    "parties": {
      "0": {
        "address": "No. 330, Shengli Rd., Pingtung City, Pingtung County 900209, Taiwan (R.O.C.)",
        "email_address": null,
        "match_confidence": 0,
        "match_method": "unmatched",
        "matched_org_unit_id": null,
        "matched_org_unit_label": null,
        "matched_user_id": null,
        "matched_user_label": null,
        "name": "屏東飛機維修廠",
        "party_type": "organization",
        "remarks": null,
        "role": "responsible-organization",
        "short_name": null,
        "target_party_uid": null,
        "telephone_number": "08-7656111",
        "title": null,
        "party_uid": "ecbe22dd-5ee8-4c43-aef3-6017f9eee9bb"
      },
      "1": {
        "address": "444 Salomon Circle, Sparks, NV 89434, USA",
        "email_address": "info@sncorp.com",
        "match_confidence": 0,
        "match_method": "unmatched",
        "matched_org_unit_id": null,
        "matched_org_unit_label": null,
        "matched_user_id": null,
        "matched_user_label": null,
        "name": "Sierra Nevada Corporation",
        "party_type": "person",
        "remarks": null,
        "role": "system-owner",
        "short_name": null,
        "target_party_uid": null,
        "telephone_number": "+1 775-356-6888",
        "title": "NA",
        "party_uid": "5a1c53b0-97d0-42c3-9d8e-e424ad69c364"
      },
      "2": {
        "address": "No. 1050, Jichang Rd., Rende Dist., Tainan City 717206, Taiwan (R.O.C.)",
        "email_address": "hmtseng@airasia.com.tw",
        "match_confidence": 0,
        "match_method": "unmatched",
        "matched_org_unit_id": null,
        "matched_org_unit_label": null,
        "matched_user_id": null,
        "matched_user_label": null,
        "name": "曾漢明",
        "party_type": "person",
        "remarks": null,
        "role": "system-security-officer",
        "short_name": null,
        "target_party_uid": null,
        "telephone_number": "886-6-2681911 EXT 2563",
        "title": "Engineer",
        "party_uid": "9c2c6408-3094-4dc4-b993-b5bb40800631"
      },
      "3": {
        "address": "No. 1050, Jichang Rd., Rende Dist., Tainan City 717206, Taiwan (R.O.C.)",
        "email_address": "wywang@airasia.com.tw",
        "match_confidence": 0,
        "match_method": "unmatched",
        "matched_org_unit_id": null,
        "matched_org_unit_label": null,
        "matched_user_id": null,
        "matched_user_label": null,
        "name": "王文堯",
        "party_type": "person",
        "remarks": null,
        "role": "authorizing-official",
        "short_name": null,
        "target_party_uid": null,
        "telephone_number": "886-6-2681911 EXT 2600",
        "title": "Manager Information & Cyber Security",
        "party_uid": "f1d242db-ce61-49d6-866e-14dc97f4c71b"
      },
      "4": {
        "address": "No. 1050, Jichang Rd., Rende Dist., Tainan City 717206, Taiwan (R.O.C.)",
        "email_address": "edward@airasia.com.tw",
        "match_confidence": 0,
        "match_method": "unmatched",
        "matched_org_unit_id": null,
        "matched_org_unit_label": null,
        "matched_user_id": null,
        "matched_user_label": null,
        "name": "張才華",
        "party_type": "person",
        "remarks": null,
        "role": "information-owner",
        "short_name": null,
        "target_party_uid": null,
        "telephone_number": "886-6-2681911 EXT 2630",
        "title": "Manager Information & Cyber Security",
        "party_uid": "9ef01730-631f-4c30-8e41-7bbdea8f362b"
      }
    }
  }
}
```

**Payload 觀察**：
1. `parties_decisions` 5 個 uid 全 `use_docx` ✓
2. `content_overrides.parties[0~4]` 5 個 party 整份內容，**含 role 欄位、含 party_uid、含 target_party_uid:null、含 match_confidence:0 match_method:"unmatched"**
3. FE 是把 `draft.parties` 整份 (含所有欄位) 塞進 `content_overrides.parties`（mirror components / leveraged 既有 idx-keyed pattern）— **這條 wire 是對的**，payload 證明 BE 拿到了完整 user 編輯資料
4. `content_overrides.components` 3 個 idx 都帶 `status: operational` ✓
5. **缺 `system_characteristic_decision`、`components_decisions`、`leveraged_authorizations_decisions`、`inventory_items_decisions` 4 個 v3 decision keys** — 這是 FE store `buildConfirmPayload` 漏帶？還是 user 沒選所以 FE 跳過？需查

### 1.5 user 上次 reproduce 也提供截圖

| Screenshot | 路徑 | 顯示 |
|---|---|---|
| Image #5 (template-edit) | `/Users/chouraymond/.claude/image-cache/bfd49895-5cac-4f87-b6df-94379d7b0a8e/5.png` | 參與人員 (8) — 4 對重複 name 同 person，上半 4 個 role=系統使用者（舊），下半 4 個 role 是 user STEP 3 改的（系統所有者 / ISSO / AO / 資料所有者） |
| Image #6 (template-edit) | `/Users/chouraymond/.claude/image-cache/bfd49895-5cac-4f87-b6df-94379d7b0a8e/6.png` | 系統依賴元件 4 筆，「狀態」欄全部 (—) |

但 user image **跟我 DB query 結果不符** — DB 查 mf 372 link 表只 1 row。可能 image 是某個瞬間 stale，或 FE 從別處抓資料。**這部分待 next session 親手 reproduce 對齊**。

---

## 2. 真正 root cause 候選（推測，待 next session verify）

### 候選 A：diff_service.annotate_parse_result 把同 party_uid emit 兩次

從 log §1.2 確認 — annotated_parse_result.parties 列表內 **person 5a1c / 9c2c / f1d2 / 9ef0 各出現兩次**：
- 第一次：matched parsed party (從 docx 解出來的) → `diff_status=changed`
- 第二次：unmatched current party (DB 既有但 parsed 沒給) → `diff_status=gone`

→ `_filter_parties_for_write` 對 use_docx + changed → write
→ 同個 uid 對 use_docx + gone → 走 `_collect_parties_to_unlink` 加進 unlink list
→ **同 uid 被同時 write + unlink** → 結果 write 完又 unlink → 4 person 從 mf 372 context 消失

**Verify 命令**：
```bash
# 找 ssp_docx_diff_service.annotate_parse_result 邏輯
grep -n "def annotate_parse_result\|def _annotate_party\|diff_status=.gone\|diff_status=.changed" \
  ~/Projects/Billows/Audit-Manager/compliance-manager-be/app/oscal/service/ssp_docx_diff_service.py
```

關鍵讀 `_annotate_party` 看「matched」跟「current orphan」是否該共用同 uid 或不該共用：
- 若 docx parser 給的 5 個 party 的 uid **複用** DB 既有 uid（不該 — parser 不該知道 DB uid），那 annotate 看 current_parties.uid 跟 parsed.uid 重複 → bug
- 若 docx parser uid 是自己 random gen 但**剛好等於 DB 既有 uid**，那是 deterministic uid scheme（依 name hash？）— 也是 bug

### 候選 B：_load_current_parties 對 module_frame source 拿錯 context

`_load_current_parties(effective_source_uid, job.source_type='module_frame')` 拿 mf 372 既有 parties。但實際 DB query mf 372 link 表只 1 row（屏東飛機維修廠）— 為什麼 annotate 看到 4 person 在 current？

可能：
- `_load_current_parties` 不只讀 `responsible_parties` link，還讀 `oscal_parties` 全表
- 或讀 mf metadata_parties 而非 responsible_parties
- 或拿到別的 context（ssp 而不是 module_frame）

**Verify 命令**：
```bash
grep -n "def _load_current_parties\|_load_current" \
  ~/Projects/Billows/Audit-Manager/compliance-manager-be/app/oscal/service/ssp_docx_import_app_service.py
```

### 候選 C：unlink_parties 沒 scope 到 context_type+context_id

Log: `strategy.unlink_parties removed 8 link rows` — caller 給 4 uids，但移除 8 個 link rows。可能：
- unlink 對 party_uid IN (...) 砍**整張表**所有 context — 跨 module_frame + ssp + 其他 context 全砍
- 或對 (context_type='module_frame') 全 mf 都砍（不只 mf 372）

→ 嚴重 data corruption，會把同個 party_uid 在其他 mf / ssp 的 link 也砍掉。

**Verify 命令**：
```bash
grep -n "def unlink_parties\|delete.*party_uuid\|DELETE FROM.*responsible_part" \
  ~/Projects/Billows/Audit-Manager/compliance-manager-be/domain/oscal/service/write_strategy/module_frame_write_strategy.py \
  ~/Projects/Billows/Audit-Manager/compliance-manager-be/app/oscal/service/party_context_service.py
```

```sql
-- 看 unlink 前/後 link 表 row count 變化
-- (上面 log 顯示 confirm 之後 link 表只剩 53-8=45 row？看 53 是 mf 368-378 全部，砍 8 是哪 8 個)
PSQL ... -c "
SET app.is_super_admin='t';
SELECT context_type, context_id, count(*)
  FROM oscal.oscal_responsible_parties
 WHERE party_uuid IN ('5a1c53b0...', '9c2c6408...', 'f1d242db...', '9ef01730...')
 GROUP BY context_type, context_id ORDER BY count desc;
"
```

### 候選 D：BE 寫 5 parties 後沒寫到 mf 372 context 的 link 表

write_parties 寫了 5 個 oscal_parties row，但 `_link_party` (per `party_context_service.py:add_party`) 只在 add_party 才 call — write_parties 是 docx import path，是否漏 link 步驟？

**Verify**：grep `ModuleFrameWriteStrategy.write_parties` 內是否真有寫 responsible_parties 步驟。

---

## 3. 開工順位（建議按此順序）

1. **跑 §6 Pre-flight + §7 Verify 確認 revert 乾淨 + Bug O / H-6 不退化**（10 分鐘）
2. **真實 reproduce 一次**（per §1.1 步驟）→ 收 BE log + DB snapshot
3. **驗候選 A**：read `ssp_docx_diff_service.annotate_parse_result` + `_annotate_party` — 找出為什麼 same uid emit 兩次（30-60 分鐘）
4. **驗候選 B**：read `_load_current_parties` — 確認 module_frame source 真實讀哪張表（15 分鐘）
5. **驗候選 C**：read `unlink_parties` / `delete by party_uuid` — 看 scope 是否帶 context_type+context_id（15 分鐘）
6. **驗候選 D**：read `ModuleFrameWriteStrategy.write_parties` 看是否真寫 link（15 分鐘）
7. **根據真因設計 fix plan**（per CLAUDE.md feature-development-workflow Phase 3）
   - 若 A：修 annotate dedup 邏輯
   - 若 B：修 _load_current_parties scope
   - 若 C：修 unlink scope（**這個最危險，可能已造成過去 confirm 的歷史 data corruption**）
   - 若 D：補 write_parties 內 link 寫入步驟
8. **同時也要看 components inline edit (status: operational) 為什麼回 template-edit 後欄位空** — 可能 BE `_apply_v2_bundle_overrides` 對 components.{idx}.status 沒套，或 components write_strategy 漏存 status 欄位
9. **進 plan mode 寫 plan 給 user approve 才開工**（per CLAUDE.md「不切 branch / 不自動 push / plan 假設先 verify」）
10. **收尾**：design.md §11.38（Bug R deeper analysis）+ changelog + FIXED-SUMMARY + 等 user 下令再 commit 收尾文件（**per memory `feedback_wait_for_user_command_to_close`**）

---

## 4. 該讀的檔案 / 預期改動範圍

### 4.1 BE 端

| 檔案 | 為何 read |
|---|---|
| `app/oscal/service/ssp_docx_diff_service.py` | 候選 A 核心 — `annotate_parse_result` 為何 same uid emit 兩次 |
| `app/oscal/service/ssp_docx_import_app_service.py:_load_current_parties` | 候選 B — current_parties 從哪 load |
| `app/oscal/service/ssp_docx_import_app_service.py:_collect_parties_to_unlink` (line 1392+) | 看 unlink 候選 list 怎麼產生 |
| `app/oscal/service/party_context_service.py:list_parties / remove_party` | GET parties 真實 query + 候選 C unlink scope |
| `domain/oscal/service/write_strategy/module_frame_write_strategy.py` | write_parties / unlink_parties 邏輯 |
| `domain/oscal/service/write_strategy/ssp_write_strategy.py` | 對比 SSP 版本是否同樣問題 |
| `infra/oscal/repo/oscal_responsible_party_repo_impl.py` (or 對應 path) | 看 unlink/delete SQL 是否帶 context_type+context_id WHERE |

### 4.2 FE 端

| 檔案 | 為何 read |
|---|---|
| `compliance-manager-fe/src/components/grc/ssp-docx-import-v2/SspDocxImportPage.vue` (uncommitted change) | 看 contentOverrides.parties wire 是否該保留（**payload 證明 FE 送對了 → 應該保留**） |
| `compliance-manager-fe/src/stores/sspDocxImportStore.js` | 看 `buildConfirmPayload` 為何 v3 decisions 沒送（components_decisions / leveraged_decisions 等都缺） |
| `compliance-manager-fe/src/components/grc/ssp-docx-import-v2/sections/PartiesSection.vue` (uncommitted change) | Bug Q Dropdown grid 內 layout — 保留 |
| `compliance-manager-fe/src/components/grc/ssp-docx-import-v2/sections/PartyLinkDialog.vue` (uncommitted change) | Bug P 拿掉 code filter — 保留 |
| `compliance-manager-fe/src/composables/useModuleFrameParties.js` | cache 邏輯（這條沒問題） |

### 4.3 跨 repo 必做

- 改 FE 前必 read `~/Projects/Billows/Audit-Manager/compliance-manager-fe/CLAUDE.md`
- 改 BE service 層後**必提醒 user 重啟 BE**（per memory `feedback_be_restart_after_service_change`）

---

## 5. Pre-flight Command（必跑）

```bash
# 1. 兩 repo branch + working tree
git -C ~/Projects/Billows/Audit-Manager/compliance-manager-be branch --show-current
git -C ~/Projects/Billows/Audit-Manager/compliance-manager-fe branch --show-current
# 預期：兩個都 feature/ssp-oscal-alignment

git -C ~/Projects/Billows/Audit-Manager/compliance-manager-be status --short
# 預期：M pyproject.toml + 多個 .png screenshots + .playwright-mcp/*.docx — 都是 dev artifact
# 重點：app/oscal/service/ssp_docx_import_app_service.py 應該 NOT 出現（revert 完）

git -C ~/Projects/Billows/Audit-Manager/compliance-manager-fe status --short
# 預期：3 個 M 檔
#   M src/components/grc/ssp-docx-import-v2/SspDocxImportPage.vue
#   M src/components/grc/ssp-docx-import-v2/sections/PartiesSection.vue
#   M src/components/grc/ssp-docx-import-v2/sections/PartyLinkDialog.vue
# 這 3 個是本 session 待 commit 的改動（layout + code filter fix + contentOverrides.parties wire），等 user 拍板再 commit / amend

# 2. local vs origin
git -C ~/Projects/Billows/Audit-Manager/compliance-manager-be log origin/feature/ssp-oscal-alignment..HEAD --oneline
# 預期：1 commit (0dac0d37 Bug O 收尾橫向文件) — 之前已 push 多個

git -C ~/Projects/Billows/Audit-Manager/compliance-manager-fe log origin/feature/ssp-oscal-alignment..HEAD --oneline
# 預期：2 commits (fa43b79 Bug Q+P, 551c7dc Bug R cache) — 上 session ship 沒 push

# 3. BE listener (上 session 砍掉了，user 應重啟過)
lsof -t -i:8000 && echo "BE up" || echo "BE down — restart needed"

# 4. BE pytest smoke
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
poetry run pytest tests/test_ssp_docx_import_app_service.py \
  tests/test_ssp_docx_diff_service.py \
  tests/test_ssp_write_strategy.py -q
# 預期：192 passed (跟上 session 一致)
```

---

## 6. Verify 上一輪 fix + Bug O 不退化（必跑）

```bash
# 6.1 H-6 cleanup verify — 4 OSCAL 表 org_unit_id 應全 NULL
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -A -c "
SET app.is_super_admin='t';
SELECT 'parties' AS tbl, count(*) AS bad FROM oscal.oscal_parties WHERE org_unit_id IS NOT NULL
UNION ALL SELECT 'leveraged', count(*) FROM oscal.ssp_leveraged_authorizations WHERE org_unit_id IS NOT NULL
UNION ALL SELECT 'components', count(*) FROM oscal.ssp_components WHERE org_unit_id IS NOT NULL
UNION ALL SELECT 'inventory', count(*) FROM oscal.ssp_inventory_items WHERE org_unit_id IS NOT NULL;
"
# 預期 4 row all bad=0

# 6.2 確認 mf 372 既有 link 狀態
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -A -c "
SET app.is_super_admin='t';
SELECT rp.id, rp.role_id, rp.party_uuid, p.name, p.party_type
  FROM oscal.oscal_responsible_parties rp
  LEFT JOIN oscal.oscal_parties p ON p.uid::text = rp.party_uuid
 WHERE rp.context_type = 'module_frame' AND rp.context_id = 372
 ORDER BY rp.id;
"
# 預期：1 row only (id=236, 屏東飛機維修廠) — 證實上 session confirm 後 4 person link 全失

# 6.3 看 53 row 全部跨 context 分佈
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -A -c "
SET app.is_super_admin='t';
SELECT context_type, context_id, count(*)
  FROM oscal.oscal_responsible_parties
 WHERE party_uuid IN (
   '5a1c53b0-97d0-42c3-9d8e-e424ad69c364',
   '9c2c6408-3094-4dc4-b993-b5bb40800631',
   'f1d242db-ce61-49d6-866e-14dc97f4c71b',
   '9ef01730-631f-4c30-8e41-7bbdea8f362b'
 )
 GROUP BY context_type, context_id ORDER BY context_id;
"
# 看 unlink 砍了哪些 context（候選 C verify）
```

---

## 7. 行為規範重要提醒（per CLAUDE.md + memory）

- **永不切 branch**（per memory `feedback_no_branch_switch`）
- **可自行 commit，不自動 push**（per memory `feedback_stage_commit_no_ask`）
- **跨 repo 改 FE 前必 read FE CLAUDE.md**
- **改 BE service 層後必提醒 user 重啟 BE**（per memory `feedback_be_restart_after_service_change`）
- **DB 兩張表都 verify 才算 done** — 任何 parties / link 改動必驗 `oscal_parties` + `oscal_responsible_parties` 兩張
- **修 bug 前先驗證 DB / response 真實狀態**（per memory `feedback_verify_db_state_before_writing_fixes`）— 不要靠直覺
- **plan 假設先 verify**（per memory `feedback_plan_vs_reality_verify_first`）— 寫 plan 用的 method / table / signature 都先 grep 確認
- **收尾必須等 user 下命令才做**（per memory `feedback_wait_for_user_command_to_close`）— 寫 design §11.X / changelog / FIXED-SUMMARY 全部等 user 明確下令
- **可人工驗證就切人工**（per CLAUDE.md / 本 session 教訓）— 別再自己跑 playwright 一直改，用人手測
- **不要晶晶體**（per memory `feedback_no_chinglish`）
- **不要把 bug 標 caveat 甩給 user**
- **PrimeVue 3.53 quirks** — Dropdown 對 null value 有 bug 用 sentinel
- **解 bug 前先確認方向再動**（per 本 session 教訓 — 上次方向被 user 喊停 2 次）

---

## 8. 收尾流程（fix 完後，等 user 下令才做）

1. design.md 加 §11.38 (Bug R deeper — diff_service duplicate entry + unlink scope corruption)
2. design.md §11 index 加新段標
3. changelog 寫 (type=fix，按性質分 BE / FE 拆 2 條可考慮)
4. 寫 FIXED-SUMMARY → `docs/features/FR-028-2605-ssp-oscal-alignment/handoff/2026-05-XX-bug-r-deeper-FIXED-SUMMARY.md`
5. 原 handoff (本檔) 標 ✓ FIXED block on top
6. memory feedback — 候選教訓：
   - `feedback_annotate_unique_entity_per_uid.md` — annotate 同 uid 不該 emit 多次 entry
   - `feedback_unlink_scope_must_match_context.md` — delete by uid 必須帶 context_type+context_id WHERE
7. 對話歷史歸檔 → `docs/conversation-history/2026-05-XX/bug-r-deeper/`
8. commit + 問 user 確認 push 規模（**不自動 push**）

---

## 9. 不在本期 scope

- jedi-oscal docx revision normalize tmp file bug（之前 Phase 0.1 標的 follow-up — 跟本 bug 無關）
- BE / FE 版號 bump（user 拍板才動）
- Bug O / H-6 / H-N 回頭 revisit（已 closed）
- Phase 5 OSCAL Export deferred plan（H-N 整段 user-facing complete 後才碰）

---

## 10. 上 session 已 ship 但未 push 的 commits

### BE (0 ahead — 上 session 沒新 BE commit；本 handoff 寫完後可考慮 commit handoff)

無 — BE 改動已 revert 乾淨。

### FE (2 ahead，沒 push)

| Hash | Type | 內容 | 是否該保留 |
|---|---|---|---|
| `551c7dc` | fix | Bug R cache invalidate（confirm 後 invalidateMfParties / invalidateSspParties）| **保留** — payload 證明 wire 正確，cache 確實在 confirm 後被 invalidate |
| `fa43b79` | fix | Bug Q (PartiesSection 補 role Dropdown) + Bug P (button wording 對 type 分流 + 5 i18n keys + icon 分流) | **保留** — UI 改動 user 已確認 layout 1/2 OK |

### FE working tree (3 uncommitted files)

| File | 改動 | 是否該保留 |
|---|---|---|
| `SspDocxImportPage.vue` | confirm payload 加 `if (draft.parties) contentOverrides.parties = {idx: {...p}}` | **保留** — payload 證明 wire 正確帶 5 個 party 的 role + 其他欄位到 BE。但 next session 可能要看是否該瘦身（只送 changed field 而非整份） |
| `PartiesSection.vue` | 角色 Dropdown 從頂部搬進 grid 內地址右側 + read-only mode label 對齊 + Dropdown class 改 w-full | **保留** — user image 確認 OK |
| `PartyLinkDialog.vue` | 拿掉 ORG_UNITS filter 內 `code:` 欄位（Bug P 真實 400 root cause）| **保留** — BE OrgUnitQueryRequest schema 沒 `code` 欄位，留會 400 |

### Memory + CLAUDE.md changes (working tree)

| File | 改動 |
|---|---|
| `~/.claude/projects/-Users-...memory/feedback_wait_for_user_command_to_close.md` | 新檔，新規範「收尾等 user 下令」|
| `~/.claude/projects/-Users-...memory/MEMORY.md` | index 加 1 行 |
| `compliance-manager-be/CLAUDE.md` | 「做 summary」段前面加新規範條文 |

→ 這 3 個改動上 session 未 commit（user 沒明示要 commit）。next session 進 working tree 會看到，可保留或一起 commit。

### 上 session 草寫但未 commit 的 BE docs（待 user 下令才 commit）

| File | 內容 | 動作建議 |
|---|---|---|
| `docs/features/FR-028-2605-ssp-oscal-alignment/design.md` | §11.35 / §11.36 / §11.37 + §11 index 加 3 條 | **可保留**，但 §11.37 內容是「Bug R = cache invalidate fix」**已經錯了**（真因不是 cache）— next session 該改寫 §11.37 為「Bug R 第一輪 fix (cache invalidate) 部分正確，但真因是 diff_service duplicate entry，見 §11.38」|
| `docs/changelog/2026-05-25-fix-bug-pqr-parties-form.md` | 涵蓋 Bug P/Q/R 3 個 fix | **可保留**，但 Bug R 段落該標 partial fix + 引 §11.38 |
| `docs/features/FR-028-2605-ssp-oscal-alignment/handoff/2026-05-25-bug-pqr-FIXED-SUMMARY.md` | 本期 SUMMARY | **可保留**，但 Bug R 段該改 partial + 引本 handoff |
| `docs/features/FR-028-2605-ssp-oscal-alignment/implementation-plan-bug-pqr.md` | plan copy | 保留 archive |

---

## 11. 上 session commits 清單（git log 對照）

### BE
```
0dac0d37 docs(ssp-oscal-alignment): Bug O 收尾橫向文件更新 + CLAUDE.md 收尾流程加「回頭更新文件」步驟  (origin 已有？或 ahead 1 — pre-flight verify)
c502ce08 docs(conversation-history): 2026-05-25 — H-N arc 4 sessions + Bug O 接手 1 session  (已 push)
058df1ab docs(ssp-oscal-alignment): Bug O Phase F 收尾 — design §11.34 closed + 補 5 教訓 + plan / changelog / SUMMARY / handoff P-Q-R
b0032e46 fix(ssp-oscal-alignment): Bug O regression — 4 v3 list annotated 用獨立 _diff key + GET 路徑漏 wire
bedbb91c feat(ssp-oscal-alignment): Bug O Phase B — BE confirm 接 4 個 v3 decisions filter（含 overwrite caveat）
```

### FE
```
fa43b79 fix(ssp-oscal-alignment): Bug Q + Bug P — PartiesSection 補 role Dropdown + button wording 對 party_type 分流  (本 session, 未 push)
551c7dc fix(ssp-oscal-alignment): Bug R — confirm 後 invalidate parties cache  (本 session, 未 push)
5f6458b fix(ssp-oscal-alignment): Bug O regression + 收尾 UI 微調  (已 push)
061a742 feat(ssp-oscal-alignment): Bug O Phase E — DiffResolutionStep TabView refactor
7e34aeb feat(ssp-oscal-alignment): Bug O Phase D — FE 4 個新 DiffSection + 3 個 DiffCard 元件
```

---

## 12. 給 fresh session 的超短 prompt

```
請閱讀交接文件，了解現況，並接手 Bug R deeper dive。
路徑：docs/features/FR-028-2605-ssp-oscal-alignment/handoff/2026-05-25-bug-r-diff-service-duplicate-entry-handoff.md

按本檔 §0 接手讀序執行 — 必跑 §5 Pre-flight + §6 Verify mf 372 link 狀態 + 取 53 row 跨 context 分佈 → §3 開工順位。

簡述：Bug R 真因不是 cache，是 diff_service.annotate_parse_result 對同 party_uid emit 兩次 entry (一次 changed 一次 gone)，加上 unlink_parties 一次砍 8 個 link 跨 context — 嚴重 data corruption。本檔 §1.2 有完整 BE log evidence，§1.4 有 user 真實 confirm payload，§2 有 4 個候選 root cause + verify 命令。

注意 §7 行為規範：不切 branch / 不自動 push / 跨 repo 改 FE 必 read CLAUDE.md / 改 BE service 必提醒重啟 / 收尾等 user 下令 / 人工驗證就切人工。


```
