# Bug D — Leveraged Services 寫入 1/4 Handoff（2026-05-25）

| 項目 | 內容 |
|---|---|
| 緣由 | Bug B 收尾後 user retry docx import，發現 mf template-edit「外部利用服務 (1)」但預覽顯示 4 筆。BE 寫了 1 筆，漏 3 筆 |
| Branch | `feature/ssp-oscal-alignment`（兩 repo 一致，user 已 push 到 origin）|
| Source mf | `5eb137c8-84b6-4894-b0dd-a25ae092a8d2` (id=368)；SSP shell id=266 |
| 接手前必讀 | **本文件自包含** — 不必另開其他 prompt；按 §0 順序讀本檔 + reference 文件即可 |
| 預估時間 | 1.5-3 小時（追 v2-bundle confirm 完整 path + DB / FE 雙端 verify） |

---

## 0. 接手讀序（fresh session 必照順序）

1. **本文件** — Bug D 完整 context（症狀 / verify / 兩條候選 / 開工順位 / 行為規範 / 收尾流程）
2. **`2026-05-25-bug-b-FIXED-SUMMARY.md`** — Bug B 兩段 fix 收尾 + 教訓（必讀，避免回頭追 Bug B；以及學 silent-failure pattern 教訓）
3. **`design.md` §11.17 + §11.18 + §11.22~§11.24** — Bug A SspShellService 拆分 / v2-bundle confirm 整體流程 / Bug B 兩段 reconciliation
4. **`app/oscal/service/ssp_docx_import_app_service.py:_run_v2_bundle_confirm`** (~line 703) — Bug D 主嫌路徑
5. **`SspImportConfirmService.confirm`** 實作（grep 找）— LA / Component / Inventory write 核心

讀完後跑 §6 Pre-flight + §7 Verify Bug B → §3 Verify root cause 候選 → §4 開工順位。

---

## 1. 症狀

### 1.1 User-facing

mf `5eb137c8` template-edit 「外部利用服務」tab 顯示 (1) — 1 筆「MDR、病毒碼與威脅情資同步更新」。但 docx 匯入預覽「外部利用服務 (4)」— 4 筆。**寫了 1 筆，漏 3 筆**。

### 1.2 DB 證據（cmmgr `SET app.is_super_admin='t'`）

```sql
-- mf 368 → profile 134 → ssp 266
SELECT id, oscal_profile_uid FROM public.module_frames
 WHERE uid='5eb137c8-84b6-4894-b0dd-a25ae092a8d2';
-- id=368, profile_uid='ad02f785-...'

SELECT id FROM oscal.profiles WHERE uid::text='ad02f785-fab0-45b7-91e7-ca80ac0ea0e8';
-- profile_id=134

SELECT id, uid::text FROM oscal.system_security_plans WHERE profile_id=134;
-- ssp id=266, uid='10829f1d-04f1-469c-988b-3d66e9e27040', status=draft

SELECT id, ssp_id, title FROM oscal.ssp_leveraged_authorizations WHERE ssp_id=266;
-- 20 | 266 | MDR、病毒碼與威脅情資同步更新   ← 只 1 筆，預期 4 筆
```

### 1.3 BE log 證據（confirm payload，2026-05-25 01:11:53）

```json
{
  "decisions": [...15 controls...],
  "parties_decisions": [...5 parties...],
  "manual_assignments": [],
  "skipped_paragraph_idxs": [],
  "predicted_controls_user_selection": [...],
  "source_uid": "5eb137c8-84b6-4894-b0dd-a25ae092a8d2",
  "content_overrides": {}
}
```

**完全沒有 `leveraged_services` / `leveraged_authorizations` 欄位**。BE 寫的 1 筆是 `_run_v2_bundle_confirm` 從 `parse_job.parsed_result` 自動寫的（不靠 FE 決策），但只寫 1 筆 — 4 筆少了 3。

---

## 2. 前次教訓（避免重蹈覆轍）

Bug B 從 2026-05-20 追到 2026-05-25，跨 2 sessions / 5 天才完整 close。本期 Bug D 別重複以下錯誤：

1. **「寫成功」計數會騙人** — `parties_written=5` 騙了兩段時間，root cause 在 `_upsert_responsible_party` silent-skip 卻沒在計數體現。**Bug D fix 後必查 DB `ssp_leveraged_authorizations` 真有 4 筆，不要相信 BE log 的 caller 計數**。
2. **Cross-source consistency** — Bug B 第二段就是「Excel 已修，docx 漏修」。Bug D 也要看 Excel 走同一 `confirm_service.confirm()` 是否有同樣症狀 — 若 Excel OK，比對兩條 path 找 docx 偏差。
3. **不要假設 user flow** — 前次 session 一開始假設 user 走 update mode 修錯地方。Bug D 同樣場景：user 從「合規資源庫 → 新增 → 從文件建立」進 → URL `/module-frame/import-docx` 是 **create mode**，wizard 跳 Step 3，預覽用 `LeveragedSection.vue` 不是 diff stepper。
4. **直面修，不要 caveat 甩鍋** — 把「BE 只寫 1/4」標 caveat 叫 user 手動補另外 3 筆是 anti-pattern。修到 root cause 為止。

---

## 3. 兩條 root cause 候選（先 verify 哪邊）

### 3.1 候選 A — BE 端：v2-bundle confirm 寫 LA 時某條件擋掉 3 筆

**該追的 path**：

| 步驟 | 檔案 / 行號 | 該看什麼 |
|---|---|---|
| 1 | `ssp_docx_import_app_service.py:_run_v2_bundle_confirm` (~line 703) | source_type='module_frame' branch 走 `ssp_shell_service.ensure_shell` → `confirm_service.confirm(ssp_id=...)` |
| 2 | 找 `SspImportConfirmService.confirm` 實作 | 處理 LA / Component / Inventory write 的邏輯；leveraged 是否依賴 catalog_id？line 749-755 註解說「catalog_id 是 None — orchestrator's catalog-scoped reconcilers skip gracefully」— 可能 reconciler 過濾掉 3 筆 |
| 3 | `bundle.parsed_leveraged_authorizations` 從哪填 | docx adapter `app/oscal/service/ssp_docx_import_app_service.py:424` `_convert(asdict(la)) for la in bundle.parsed_leveraged_authorizations` — 找 bundle build 處 |
| 4 | `parsed_ssp.leveraged_services` vs `bundle.parsed_leveraged_authorizations` | 兩個概念可能不同 — FE 預覽 4 筆是哪個 source？BE 寫 1 筆是哪個 source？|
| 5 | BE log 完整 confirm trail | `grep "v2-bundle\|written_las\|confirm_service\|Docx v2-bundle confirm" log/app.log` |

**Verify A**：

```bash
# A1: 查最近 parse_job 內 parsed_result 的 leveraged 結構
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -c "
SET app.is_super_admin='t';
SELECT id, status,
       jsonb_array_length(COALESCE(parsed_result->'leveraged_services', '[]'::jsonb)) AS lev_svcs,
       jsonb_array_length(COALESCE(parsed_result->'leveraged_authorizations', '[]'::jsonb)) AS lev_auths
  FROM oscal.parse_jobs
 ORDER BY id DESC LIMIT 5;
"
# 解讀：
# - lev_svcs=4 + lev_auths=1 → A2 adapter 把 4 services 縮成 1 authorization（候選 A 成立，上游 adapter bug）
# - 兩者都 4 → confirm_service 寫入時擋掉 3（候選 A 成立，下游 write bug）
# - 兩者都 1 → docx parser 抽錯 / dedup（更上游 extractor bug）

# A2: 查 ssp 266 完整 confirm 寫入路徑 BE log
grep -E "ssp_id=266|leveraged|v2-bundle|written_las|written_components|reconciler" \
     ~/Projects/Billows/Audit-Manager/compliance-manager-be/log/app.log | tail -50
```

### 3.2 候選 B — FE 端：應該把 leveraged 加進 payload 但漏送

**該追的 path**：

| 步驟 | 檔案 / 行號 | 該看什麼 |
|---|---|---|
| 1 | `compliance-manager-fe/src/components/grc/ssp-docx-import-v2/SspDocxImportPage.vue` onConfirm (~line 500-610) | 看是否該組 `leveraged_decisions` / `leveraged_services_decisions` 進 payload |
| 2 | `compliance-manager-fe/src/components/grc/ssp-docx-import-v2/sections/LeveragedSection.vue` | 預覽顯示 4 筆的元件 — 有沒有 user decision UI？|
| 3 | `compliance-manager-fe/src/stores/sspDocxImportStore.js` | 看 store 有沒有 `decisions.leveraged_services` 結構 |
| 4 | BE confirm schema | 看 `_validate_decisions` 等是否預期收 leveraged 欄位 |

**Verify B**：

```bash
grep -rn "leveraged_services_decisions\|leveraged_decisions\|decisions.leveraged" \
     ~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/
# 完全沒 hit → 候選 B 成立（FE 從沒實作 leveraged 決策）

grep -n "props.leveraged\|leveraged_services\|emit.*update" \
     ~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/components/grc/ssp-docx-import-v2/sections/LeveragedSection.vue
```

### 3.3 推測（待 verify）

直覺：**候選 A 概率較高**。理由：
- `content_overrides={}` 是空的（FE 預設 — 邏輯上 leveraged services 應該透過 `content_overrides.leveraged_services` 傳，但 user 沒編輯所以空）
- BE 既有寫 1 筆（不是 0 筆），代表 confirm path 有跑 leveraged write，只是部分 — 那是寫入時 filter
- Excel/docx 共用 `confirm_service.confirm()` — Excel 端可能 OK 或同樣有 bug，cross-source check 必跑

但也不排除 **A + B 混合**：FE 該支援 leveraged decisions 但沒做，BE 寫入也有 bug。

---

## 4. 開工順位

1. **跑 §6 Pre-flight + §7 Verify Bug B**（5-10 分鐘）
2. **跑 §3.1 + §3.2 Verify command 確認候選 A vs B**（10 分鐘）— 結果決定後續走 BE 還 FE
3. **追 root cause 到底**：
   - 候選 A → trace `SspImportConfirmService.confirm` 內 LA write 邏輯，找出 3 筆被擋的條件
   - 候選 B → 看 FE Step 3 是否該補 leveraged_decisions payload；BE schema 是否預期收
   - 混合 → 兩邊都修
4. **Fix + DB 雙端 verify**：query `ssp_leveraged_authorizations WHERE ssp_id=<new_ssp>` 真有 4 筆才算 done。**不要相信 BE log 的 caller 計數**（§2.1 教訓）
5. **補 design.md §11.25**（mirror §11.22~§11.24 寫法，含 root cause 哪條候選成立 / why / 教訓）
6. **commit + Bug D FIXED SUMMARY handoff**（mirror `2026-05-25-bug-b-FIXED-SUMMARY.md`）
7. **問 user push 規模**（永不自動 push）

---

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

### 5.1 BE 端

| 檔案 | 為何 read |
|---|---|
| `app/oscal/service/ssp_docx_import_app_service.py:_run_v2_bundle_confirm` (~line 703) | confirm path 入口，看 `catalog_id=None` 是否影響 LA write |
| `app/oscal/service/ssp_docx_import_app_service.py:424` 附近 | `bundle.parsed_leveraged_authorizations` build 處 |
| `SspImportConfirmService.confirm` 實作（grep 找） | LA / Component / Inventory write 核心邏輯 |
| `app/oscal/service/ssp_excel_import_app_service.py:_run_v2_bundle_confirm` (~line 1602) | Excel 走同一 confirm_service — 看 Excel 有沒有踩 Bug D（cross-source check） |

### 5.2 FE 端

| 檔案 | 為何 read |
|---|---|
| `compliance-manager-fe/src/components/grc/ssp-docx-import-v2/SspDocxImportPage.vue:500-610` | onConfirm 組 payload 處，看有沒有 leveraged_decisions |
| `compliance-manager-fe/src/components/grc/ssp-docx-import-v2/sections/LeveragedSection.vue` | 預覽顯示 4 筆 leveraged 的元件 |
| `compliance-manager-fe/src/stores/sspDocxImportStore.js` | 看 decisions 結構是否有 leveraged |
| `compliance-manager-fe/src/composables/useSspLeveraged.js` | leveraged 相關 composable（如果有）|

### 5.3 改 FE 前必做

跨 repo 改 FE 前必 read `~/Projects/Billows/Audit-Manager/compliance-manager-fe/CLAUDE.md`（per memory `feedback_cross_repo_read_claude_md_first`）。

---

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

```bash
# 1. 兩 repo branch
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

# 2. 兩 repo working tree
git -C ~/Projects/Billows/Audit-Manager/compliance-manager-be status --short
# 預期：M pyproject.toml only (dev path-dep，永不 stage)
git -C ~/Projects/Billows/Audit-Manager/compliance-manager-fe status --short
# 預期：clean

# 3. local vs origin（user 已 push）
git -C ~/Projects/Billows/Audit-Manager/compliance-manager-be log origin/feature/ssp-oscal-alignment..HEAD --oneline | wc -l
# 預期：0
git -C ~/Projects/Billows/Audit-Manager/compliance-manager-fe log origin/feature/ssp-oscal-alignment..HEAD --oneline | wc -l
# 預期：0

# 4. BE 是否有重啟接 Bug B 第二段 fix (fef5eecf)
lsof -t -i:8000   # 若有 listener，pid → ps -o lstart -p <pid> 看啟動時間
# 若 BE 還沒接 fef5eecf 重啟一次:
# lsof -t -i:8000 | xargs -r kill -9 && python main_socketio.py

# 5. BE test smoke
poetry run pytest tests/test_ssp_docx_import_app_service.py -q
# 預期：全綠
```

---

## 7. Verify Bug B 確實 close（必跑，避免回頭追）

```bash
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -c "
SET app.is_super_admin='t';
SELECT id || ' role=' || role_id || ' party=' || party_uuid::text
  FROM oscal.oscal_responsible_parties
 WHERE context_type='module_frame' AND context_id=368;
"
# 預期 5 筆 (id 216-220)：
#   216 role=responsible-organization (org)
#   217-220 role=system-user (4 person)
```

如果 5 筆都在 → Bug B close，繼續 Bug D。
如果 0 筆 → Bug B 沒 close（不該發生但 verify 一下），先回頭挖 §11.22 / §11.24 是否被誰 revert。

---

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

- **永不切 branch**：發現 branch 不對停下問 user，不自己 fix（per CLAUDE.md / `feedback_no_branch_switch`）
- **可自行 commit，不自動 push**：user 明確指示才 push（per CLAUDE.md / `feedback_stage_commit_no_ask`）
- **跨 repo 改 FE 前必 read FE CLAUDE.md**（per memory `feedback_cross_repo_read_claude_md_first`）
- **改 BE service 層後必提醒 user 重啟**（per memory `feedback_be_restart_after_service_change`，BE 沒 hot reload）
- **改套件先走 poetry path dependency**（per memory `feedback_jedi_package_dev_path_first`，dev 階段不每改必 bump version）
- **DB 兩張表都 verify 才算 done**（§2.1 教訓 — caller 計數會騙人）
- **不要把 bug 標 caveat 甩給 user**（Bug B 前 session 的錯誤示範 — 直面修，不迴避）
- **changelog 收尾 batch 寫**（user 說「summary / 告一段落」才寫，per CLAUDE.md）
- **不要晶晶體**（per memory `feedback_no_chinglish` — 動詞 / 連接詞用中文，技術專有名詞保留英文）
- **plan 內 method/欄位假設先 verify 才開工**（per memory `feedback_plan_vs_reality_verify_first`）

---

## 9. 收尾流程（fix 完後）

1. **design.md §11.25** 補 Bug D fix entry（mirror §11.22~§11.24 寫法，含「為何 §11.22~§11.24 fix Bug B 沒順手解 Bug D」分析）
2. **`handoff/2026-05-25-bug-d-FIXED-SUMMARY.md`** 寫短收尾報告（mirror `2026-05-25-bug-b-FIXED-SUMMARY.md` 樣式：root cause / commits / DB verify / 教訓 / 殘留）
3. **commit**（BE fix + design.md + handoff SUMMARY；FE 若改也獨立 commit）
4. **問 user 確認 push 規模**（本期 Bug D commits + 任何累積的 unpushed）
5. **user 拍板才做** jedi 進版 / 版號對齊 / 改 silent-skip pattern 等 follow-ups

---

## 10. 不在 Bug D scope

- L4 5-tab 結構 spec drift（design.md §4.1 仍 spec 5-tab，本期保持 4-tab）
- L8 jedi-oscal 進版 + Nexus push（user 拍板）
- L9 BE/FE 版號 1:1 對齊 v1.1.0 → v1.2.0（同 L8）
- Phase 5 OSCAL Export（已 deferred）
- jedi-oscal `_upsert_responsible_party` silent-skip cleanup（列 jedi 進版時做）
- Cross-source ParsedParty adapter 共用 helper refactor（Bug B 教訓延伸）
- mf 367 (`fc51dd08`) 失敗 import 殘留清理（無害，user 可自行刪）

---

## 11. 前次 session（2026-05-25 凌晨）commits 清單

origin/feature/ssp-oscal-alignment 已包含（user 已 push）：

```
BE:
4441d3f4 docs(ssp-oscal-alignment): Bug B 收尾 SUMMARY + Bug D handoff
fef5eecf fix(ssp-oscal-alignment): Bug B 第二段 — docx _dict_to_parsed_parties 補 default role
ba7885d4 docs(ssp-oscal-alignment): design §11.22 + §11.23 — Bug B + Issue 1 reconciliation
8648940d docs(ssp-oscal-alignment): fix phase4 SUMMARY §5 L7 drift — Bug B 改現在優先 fix
375236a9 docs(ssp-oscal-alignment): Bug B + parties stepper handoff for next session

FE:
f31d0cf  fix(ssp-oscal-alignment): Bug B parties default decision + Issue 1 缺欄位 render
```

更早的 Phase 4 task arc commits (BE 34 + FE 5) 也已 push。

---

## 12. 給 fresh session 的超短 prompt（user 用這個貼）

```
接手 SSP OSCAL Alignment Bug D — docx import 4 leveraged services 只寫 1
筆。完整 context 全在
docs/features/FR-028-2605-ssp-oscal-alignment/handoff/2026-05-25-bug-d-leveraged-services-handoff.md
按本檔 §0 接手讀序執行 — 必跑 §6 Pre-flight + §7 Verify Bug B → §3 verify
候選 → §4 開工。注意 §2 前次教訓 + §8 行為規範。前次 (Bug B 兩段) 已 close。
```
