# SSP Update Diff — Code Review Fixes & Smoke Test — Final Summary

> **Task arc**：2026-05-07（單日，10:19 → 12:18，~2h）
> **Branch**：`feature/ssp-update-diff`（同 5/6 上一個 task arc，續修）
> **核心需求**：對 5/6 上線 ready 的 SSP Update Diff feature 做 post-implementation code review，依 review 報告把 6 條 critical / 12 條 important issues 收口；最後跑 Playwright MCP smoke 驗證所有改動 + 順手抓出 1 個 dead-component bug。

---

## 1. Commits（依 repo 分）

### BE (`compliance-manager-be`) — 4 commits
Branch base：`08872cb` → HEAD：`bd15b11`

| Phase | Commit | 對應 review issues |
|------|--------|------------------|
| Critical | `49c8b8c` | C4 (confirm 吞 DB 錯) / C5 (`_load_current_parties` silent return) / C6 (`_filter_parties_for_write` fallback) |
| Important | `09fc125` | I1 (error code 歸位) / I3 (`_dict_to_parsed_parties` docstring) / I5 (@transaction docstring) / I6 (logger refactor) / I7 (strategy bare except 收緊) / I8 (`_load_candidates` 12 個 bare except 拿掉) |
| Feature: gone parties unlink | `94a53fa` | I4 (BE) + I10 (FE coordinated) — strategy `unlink_parties` + `_collect_parties_to_unlink` helper + design.md §11.3 補述 + 3 unit tests |
| Feature: adapter status | `bd15b11` | I9 — per-section `adapter_status` / `adapter_errors` + `GRC_DOCX_ADAPTER_PARTIES_FAILED` (412) hard block + 2 unit tests |

### FE (`compliance-manager-fe`) — 8 commits
Branch base：`bd5ab06` → HEAD：`871a535`

| Phase | Commit | 對應 review issues |
|------|--------|------------------|
| Critical | `a06a943` | C1 (banner mode 比錯) / C2 (`reset()` filter regression) / C3 (parties_decisions fallback drift) |
| Important | `93c2045` | I12 (Step 3 → Step 1 confirm dialog) |
| Feature: gone parties UI | `aac8677` | I10 — `PartiesDiffSection` 留 gone + `PartyDiffCard` 加移除 pill + `_flattenDiffParties` 對 (gone, use_docx) 不渲染 |
| Feature: adapter status UI | `c1e2b14` | I9 — store getters + `ImportOutline.vue` banner + `onConfirm` guard |
| Stepper UX (Raymond 自手) | `dacbf6c` | stepsModel 改「依 mode 固定」 |
| goBackFromStep3 (Raymond 自手) | `0153ac7` | 改用 `isCreate.value` 判斷 confirm dialog |
| **Smoke 抓 bug** | `f771ba3` | **`ImportOutline.vue` 是 dead component** — 整個 banner 修進孤兒元件等於沒做。Banner refactor 進 `SspDocxImportPage.vue` + 刪 `ImportOutline.vue` |
| Backfill changelog | `871a535` | docs(changelog) 補 dacbf6c + 0153ac7 |

**總計：12 commits 在這個 task arc**（BE 4 + FE 8）

Working tree 兩 repo 都乾淨（BE 只剩 5 張 smoke screenshot 在 `.playwright-mcp/`，已 gitignored；CLAUDE.md `M` 是 Raymond 個人改動）。

---

## 2. 改動範圍 — 檔案級

### BE
- **修改**
  - `app/oscal/service/ssp_docx_import_app_service.py`
    - `confirm_import` 移除外層 `try/except Exception` 吞錯（C4）
    - `_load_current_parties` silent `return []` 改 raise（C5）
    - `_filter_parties_for_write` annotated=None+decisions 非空 改 raise（C6）
    - `_collect_parties_to_unlink` 新 helper（I4）
    - `confirm_import` adapter_status.parties=failed → raise PreconditionFailedError（I9）
    - upload_and_parse adapter try/except 改 per-section status（I9）
    - `_load_candidates` / `_load_module_frame_current` / `_load_candidates_from_catalog` 12 個 bare `try/except → []` 全拿掉（I8）
    - `_enrich_party_user_org_names` docstring 補 @transaction scope（I5）
    - `_dict_to_parsed_parties` docstring 補 target_party_uid 說明（I3）
    - `_load_current_parties` log 改 `_safe_log_info`（mock test 環境兼容）
  - `app/oscal/service/ssp_docx_diff_service.py` — module-level logger + `compute_party_diff` log 改 debug（I6）
  - `domain/oscal/strategy/{ssp,module_frame}_write_strategy.py`
    - `_upsert_party` / `_upsert_responsible_party` 移除 bare `except Exception:`（I7）
    - `_upsert_party` target_uid 找不到 raise `NotFound(GRC_PARTY_NOT_FOUND)`（I7）
    - 新增 `unlink_parties(party_uids, source_uid)` method（I4）
  - `api/oscal/serializers/ssp/ssp_docx_import.py` — response schema 加 `adapter_status` / `adapter_errors`（I9）
  - `common/code/grc_error_code.py` — `GRC_409021` 移到 SSP Doc Parser section（I1）+ 新增 `GRC_DOCX_ADAPTER_PARTIES_FAILED = GRC_412016`（I9）
  - `docs/features/FR-025-2605-ssp-update-diff/design.md` — §11.3 補完整 unlink semantic 段落
  - `tests/test_ssp_docx_import_app_service.py` — 加 5 unit tests（3 _collect_parties_to_unlink + 2 adapter_status block）
  - `tests/test_ssp_docx_import_create_no_diff.py` — `_make_service` 補 responsible_party / party mocks
- **新增**
  - `docs/changelog/2026-05-07-fix-ssp-update-diff-critical-review.md`
  - `docs/changelog/2026-05-07-tweak-ssp-update-diff-important-review.md`
  - `docs/changelog/2026-05-07-feat-ssp-update-diff-gone-parties-unlink.md`
  - `docs/changelog/2026-05-07-feat-ssp-update-diff-adapter-status.md`
  - `docs/review/2026-05-07-ssp-update-diff-review.md`（review 報告本身）
  - `docs/conversation-history/2026-05-07-ssp-review-fixes/` 7 part + SUMMARY

### FE
- **修改**
  - `src/stores/sspDocxImportStore.js`
    - 移除 `mode` state（C1）
    - `reset()` 內 filter 改回 `'all'`（C2）
    - 新增 4 個 getters（adapterStatus / adapterErrors / adapterPartiesFailed / adapterHasAnyFailure）（I9）
  - `src/components/grc/ssp-docx-import-v2/sections/ImportOutline.vue` — **刪除（整支 dead component）**
  - `src/components/grc/ssp-docx-import-v2/SspDocxImportPage.vue`
    - `onConfirm` 內 fallback `'keep_current'` → `'skip'`（C3）
    - `goBackFromStep3` 加 confirm dialog（I12）
    - 後續 Raymond 改 stepper 改「依 mode 固定」+ goBackFromStep3 用 `isCreate.value` 判斷
    - 銜接 banner 從 ImportOutline.vue 搬進來（adapter critical / soft warn / diff summary）
    - `onConfirm` 開頭加 adapterPartiesFailed toast guard + Step 3 confirm 鈕加 disabled 條件（I9）
  - `src/components/grc/ssp-docx-import-v2/diff/PartiesDiffSection.vue` — visibleParties 留 gone（I10）
  - `src/components/grc/ssp-docx-import-v2/diff/PartyDiffCard.vue` — actionOptions 加 gone branch + 「將清空」改「將移除」+ gone 右欄 placeholder（I10）
- **新增**
  - 6 個 changelog（每個 phase 一份 + stepper 補檔）

---

## 3. 行為差異（before vs after）

| 行為 | Before（5/6 shipped） | After（5/7 review fixes） |
|------|----------------------|---------------------------|
| confirm parties 寫入 DB error | silent log + 回 success/parties_written=0 | rollback 整 transaction，client 看到 error |
| GET parse_result 對已刪除 source | 200 + 全 added | 404 `GRC_DOCX_SOURCE_NOT_FOUND` |
| diff_service 沒 wire + parties_decisions 非空 | silent over-write 全部 parties | raise RuntimeError |
| `_upsert_party` target_uid 找不到 | silent fall through INSERT（append bug） | raise `NotFound(GRC_PARTY_NOT_FOUND)` |
| `_load_candidates` DB error | silent `[]` → 「all added」假象 | 例外傳播 |
| `gone` parties Step 2 | 隱藏 | 顯示 + decision pills [保留現值 / 移除] |
| `gone + use_docx` for parties | silent drop | unlink link record（保留 oscal_parties row） |
| Step 3 diff summary banner | 永遠不顯示（mode 比錯 + 元件 dead） | 黃 banner，顯示 changed/added/gone 計數 |
| Adapter parties=failed | silent，FE 看不出 | 紅 banner Step 2/3 + Step 3 confirm disabled + BE 412 hard block |
| Adapter non-parties=failed | silent | 黃 soft banner，confirm 仍允許 |
| Cancel + 重新上傳 | filter 回 'pending'，user 點完項目消失 | filter 維持 'all' |
| Added party 沒選 → confirm | `keep_current`（語意錯） | `'skip'` 與 store 一致 |
| Step 3 → Step 1（create mode） | silent 砸所有 decisions / inline edits | confirm dialog「會丟棄目前所有決策與編輯」 |
| Stepper 格數 | 依 diff 內容動態（造成 goBackFromStep3 fallback 邏輯不穩） | 依 mode 固定（create 2 格 / update 3 格） |

API contract：
- response 新增 4 個 optional 欄位：`parties_unlinked` / `adapter_status` / `adapter_errors`（舊 client backward compat，缺值視為 legacy）
- 新增 2 個 error code：`GRC_DOCX_ADAPTER_PARTIES_FAILED`（412016）+ `GRC_PARTY_NOT_FOUND` 重用（404024）
- design.md §11.3 補完 parties unlink semantic 段落

---

## 4. 規範文件清單

### Code Review 報告
- `compliance-manager-be/docs/review/2026-05-07-ssp-update-diff-review.md`（本 task arc 起點）

### Changelogs
- BE 4 份：critical-review / important-review / gone-parties-unlink / adapter-status
- FE 6 份：critical-review / important-review (I12) / gone-parties-unlink / adapter-status / banners-render-in-page (smoke 抓 bug) / stepper-fixed-by-mode (backfill)

### Spec 更新
- `compliance-manager-be/docs/features/FR-025-2605-ssp-update-diff/design.md` §11.3 補完 parties unlink hard-delete + multi-role + 與 controls/objectives 語意對照

### 對話紀錄
- `compliance-manager-be/docs/conversation-history/2026-05-07-ssp-review-fixes/`
  - `part-01-of-07-review-report-and-pickup.md`（24 訊息）
  - `part-02-of-07-critical-fixes-and-commit.md`（287 訊息）
  - `part-03-of-07-important-fixes-and-commit.md`（28 訊息）
  - `part-04-of-07-i4-i10-gone-parties-unlink.md`（108 訊息）
  - `part-05-of-07-i9-adapter-failure-surfacing.md`（88 訊息）
  - `part-06-of-07-playwright-smoke-test.md`（550 訊息 — smoke 階段 tool calls 量大）
  - `part-07-of-07-archive-and-summary.md`（29 訊息）

### Smoke screenshots（gitignored）
- `compliance-manager-be/.playwright-mcp/smoke-A1-diff-banner.png`
- `compliance-manager-be/.playwright-mcp/smoke-D1-gone-parties-ui.png`
- `compliance-manager-be/.playwright-mcp/smoke-E1-critical-banner.png`
- `compliance-manager-be/.playwright-mcp/smoke-E5-soft-warn.png`
- `compliance-manager-be/.playwright-mcp/smoke-F1-back-confirm-dialog.png`

---

## 5. Smoke Test 結果

Playwright MCP 在 `localhost:5180`（FE）+ `localhost:8000`（BE）跑：

| Case | 範圍 | 結果 |
|------|------|------|
| A1 | C1 — Step 3 diff summary banner | ✅ PASS |
| D1-D2 | I10 — gone parties Step 2/3 UI | ✅ PASS |
| D3-D6 | I4 — BE unlink path（link 5→3, party row 保留, BE log `removed 2 link rows`） | ✅ PASS |
| E1-E5 | I9 — critical/soft banner + button disabled + BE 412 hard block | ✅ PASS |
| F1-F3 | I12 — create mode confirm dialog 完整流程 | ✅ PASS |
| F4 | I12 — update mode silent 回 Step 2（per Raymond-revised goBackFromStep3） | ✅ PASS |

BE pytest：112 passed / 2 failed（2 fail 為 pre-existing — DB log handler vs `@transaction` 衝撞 + 過時 v2_parties test assertion）。

---

## 6. 已知 follow-up（未處理）

延續 5/6 task arc 的 follow-up 清單未變動，新加：

### 中優先
1. **`tests/test_ssp_docx_import_diff_response.py` 7+ pre-existing fixture failure** — 仍未修，跟 `test_create_mode_get_parse_result_all_controls_added` / `test_write_parties_refreshes_existing_party_in_place` 同根因（DB log handler vs `@transaction` 衝撞）
2. **「移除」hard delete 不可復原** — design.md §11.3 已標 v1 限制；如需 undo 需 `oscal_responsible_parties` schema 加 `is_active` + soft-delete 機制
3. **Phase 5 E2E 自動化** — 仍待在 `compliance-manager-test` repo 寫 BDD scenarios（design.md §7.4），這次 Playwright MCP 是手動 driver，未轉成 Cucumber feature

### 低優先
4. **i18n 「移除」/ "Remove" key 跟其他 hard-coded zh-tw 字串** — Phase 4.1 known TODO，沒這次處理
5. **adapter_status 目前只有 atomic（單一 v2 adapter call）的 binary fail-or-pass**；未來 jedi-oscal adapter API 可能改 per-section 內部 try/except，schema 預留位置即可
6. **`_load_candidates` 12 個 bare except 拿掉後，若 production 撞到真實 DB 錯**：`@transaction` rollback 整個 parse — 預期是「parse 完整失敗」而非「parse 部分成功」，但需觀察 production behaviour

### Plan-stage open issue（已 resolved 但要 follow-up monitor）
- `oscal_responsible_parties` 沒 audit columns — 移除 link 後追溯只能靠 `system_logs` 表（hard 解析），考慮 schema 加 `deleted_at` / `deleted_by` 列

---

## 7. 部署 handover

### Pre-deploy checklist
- [ ] BE deploy 前確認新 `GRC_412016`（GRC_DOCX_ADAPTER_PARTIES_FAILED）序號未跟 main branch 既有 codes 衝突
- [ ] BE 新增 `parties_unlinked` / `adapter_status` / `adapter_errors` 欄位是 backward compat — 舊 FE client 收到不會 break
- [ ] FE deploy 前 `npm run build` 確認 banner CSS 沒 build error（已 inline 在 SspDocxImportPage.vue）
- [ ] BE deploy 後重啟 — 新 `unlink_parties` strategy method + `_collect_parties_to_unlink` helper 都需新代碼載入
- [ ] FE deploy 後檢查 Step 3 banner 在 dark theme 下顯示是否可讀（紅 / 黃 banner 都用了 hex，dark theme polish 是 follow-up）

### Smoke test 路徑（production sanity）
1. **Critical fix C4 / C5 / C6 sanity**：psql 把一個 SSP `is_active=false` → GET parse_result 應 404，不能拿到「all added」假象
2. **D path**：對既有 module_frame 重新匯入「砍掉部分 parties」的 docx → Step 2 看到 gone parties → 選「移除」→ 確認 → DB `oscal_responsible_parties` 該 link 被刪、`oscal_parties` row 還在
3. **E path**：BE 模擬 v2 adapter raise（temporarily inject SQL `parsed_result.adapter_status.parties = failed`）→ FE 顯紅 banner + Step 3 確認鈕 disabled + 強送 confirm → BE 412
4. **F path**：create 新 module_frame 流程 → Step 3 → 上一步 → 應跳 confirm dialog
5. **A path**：update mode 有 diff → Step 3 顯示 diff summary banner

### Rollback path
- BE：`feature/ssp-update-diff` revert 4 review-fix commits（`49c8b8c..bd15b11`）保留 5/6 的原 30 commits
- FE：revert 8 review-fix commits（`a06a943..871a535`）— 但 Raymond 自手的 stepper 改動（dacbf6c / 0153ac7）revert 後可能造成 update mode no-diff 的 stepper 變回 2 格，建議分批

### 監控
- BE log prefix 不變（`[ssp-docx-import]` / `[ssp-confirm]` / `[party-diff]`），新增：
  - `[ssp-confirm] calling ModuleFrameWriteStrategy.unlink_parties with N uids`
  - `[ssp-confirm] strategy.unlink_parties removed N link rows`
  - `[ssp-docx-import] adapter_status: {parties: failed, ...}`（adapter raise 時）

---

## 8. Lessons learned

1. **Component graph 要驗 reachability** — 改 `ImportOutline.vue` 改了 4 commits 都沒人發現它根本沒被 import；smoke test 第一輪就抓到。Refactor / fix UI element 時先 `grep -r ComponentName /src/components/` 確認 caller 存在。
2. **Recon agent 在動工前 verify 套件 API 是 high-leverage** — I4+I10 開動前先派人 verify `delete_by_id` / `OscalResponsibleParty` schema，避免做到一半撞 §11.12 那種 wall。
3. **Bare `except Exception` 在 audit 系統是反 pattern** — 看似防禦但實際上是 silent failure 的入口。`get_by_uid` / `get_one` 已經正確 return None，外層包 try/except 等於把 DB error 偷偷吞掉。本 PR 拿掉 12+ 個 bare except 是這個 lesson 的 institutional fix。
4. **Per-section status 比 binary flag 好擴充** — I9 即使當前 adapter 是 atomic call（只能整體 ok/failed），仍把 `adapter_status` 設成 per-section dict — schema 預留空間，未來 adapter 內部分節 try/except 後可逐 section 標記，不需要再改 serializer / FE getters。
5. **User-side 介入要在 changelog 留痕** — Raymond 中途自己改 stepper 邏輯（dacbf6c / 0153ac7）沒寫 changelog，archive 時要 backfill。Long-term：考慮 git pre-commit hook 檢查 changelog 是否與 source 同 PR 提交。
6. **Hard delete 對 audit trail 不友善** — `oscal_responsible_parties` schema 沒 audit / soft-delete columns，本次選擇接受 v1 限制；長期應 schema migration 加 `deleted_at` / `deleted_by`。

---

**狀態**：6 critical + 6 actionable important + 1 smoke-found bug 全收口；2 design-by-intent 不修；3 deferred design issues 各自落地完成。可 deploy 上線。剩 follow-up 都是 polish / nice-to-have。
