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。
compliance-manager-be) — 4 commitsBranch 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 |
compliance-manager-fe) — 8 commitsBranch 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 個人改動)。
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)_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 mocksdocs/changelog/2026-05-07-fix-ssp-update-diff-critical-review.mddocs/changelog/2026-05-07-tweak-ssp-update-diff-important-review.mddocs/changelog/2026-05-07-feat-ssp-update-diff-gone-parties-unlink.mddocs/changelog/2026-05-07-feat-ssp-update-diff-adapter-status.mddocs/review/2026-05-07-ssp-update-diff-review.md(review 報告本身)docs/conversation-history/2026-05-07-ssp-review-fixes/ 7 part + SUMMARYsrc/stores/sspDocxImportStore.js
mode state(C1)reset() 內 filter 改回 'all'(C2)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)isCreate.value 判斷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)| 行為 | 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:
parties_unlinked / adapter_status / adapter_errors(舊 client backward compat,缺值視為 legacy)GRC_DOCX_ADAPTER_PARTIES_FAILED(412016)+ GRC_PARTY_NOT_FOUND 重用(404024)compliance-manager-be/docs/review/2026-05-07-ssp-update-diff-review.md(本 task arc 起點)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 訊息)compliance-manager-be/.playwright-mcp/smoke-A1-diff-banner.pngcompliance-manager-be/.playwright-mcp/smoke-D1-gone-parties-ui.pngcompliance-manager-be/.playwright-mcp/smoke-E1-critical-banner.pngcompliance-manager-be/.playwright-mcp/smoke-E5-soft-warn.pngcompliance-manager-be/.playwright-mcp/smoke-F1-back-confirm-dialog.pngPlaywright 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)。
延續 5/6 task arc 的 follow-up 清單未變動,新加:
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 衝撞)oscal_responsible_parties schema 加 is_active + soft-delete 機制compliance-manager-test repo 寫 BDD scenarios(design.md §7.4),這次 Playwright MCP 是手動 driver,未轉成 Cucumber feature_load_candidates 12 個 bare except 拿掉後,若 production 撞到真實 DB 錯:@transaction rollback 整個 parse — 預期是「parse 完整失敗」而非「parse 部分成功」,但需觀察 production behaviouroscal_responsible_parties 沒 audit columns — 移除 link 後追溯只能靠 system_logs 表(hard 解析),考慮 schema 加 deleted_at / deleted_by 列is_active=false → GET parse_result 應 404,不能拿到「all added」假象oscal_responsible_parties 該 link 被刪、oscal_parties row 還在parsed_result.adapter_status.parties = failed)→ FE 顯紅 banner + Step 3 確認鈕 disabled + 強送 confirm → BE 412feature/ssp-update-diff revert 4 review-fix commits(49c8b8c..bd15b11)保留 5/6 的原 30 commitsa06a943..871a535)— 但 Raymond 自手的 stepper 改動(dacbf6c / 0153ac7)revert 後可能造成 update mode no-diff 的 stepper 變回 2 格,建議分批[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 時)ImportOutline.vue 改了 4 commits 都沒人發現它根本沒被 import;smoke test 第一輪就抓到。Refactor / fix UI element 時先 grep -r ComponentName /src/components/ 確認 caller 存在。delete_by_id / OscalResponsibleParty schema,避免做到一半撞 §11.12 那種 wall。except Exception 在 audit 系統是反 pattern — 看似防禦但實際上是 silent failure 的入口。get_by_uid / get_one 已經正確 return None,外層包 try/except 等於把 DB error 偷偷吞掉。本 PR 拿掉 12+ 個 bare except 是這個 lesson 的 institutional fix。adapter_status 設成 per-section dict — schema 預留空間,未來 adapter 內部分節 try/except 後可逐 section 標記,不需要再改 serializer / FE getters。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。