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

Task arc:2026-05-07(單日,10:19 → 12:18,~2h) Branchfeature/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.pyGRC_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

低優先

  1. i18n 「移除」/ "Remove" key 跟其他 hard-coded zh-tw 字串 — Phase 4.1 known TODO,沒這次處理
  2. adapter_status 目前只有 atomic(單一 v2 adapter call)的 binary fail-or-pass;未來 jedi-oscal adapter API 可能改 per-section 內部 try/except,schema 預留位置即可
  3. _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

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。