2026-08-02 覆核輪受評範圍收斂(narrowed scope)— SUMMARY

項目 內容
Arc 覆核輪(close-out)受評範圍未收斂 — 控制項導覽顯示母輪全部控制項
定位 FR-053.1(覆核輪承襲與脈絡)續作 bug fix;FR-053 母案「第 3 刀讀取端按輪過濾」的漏網
Branch fix/1.13.0-bug(BE 單 repo,FE 零改動)
Commits 6 支(b283f1ab / 167f578f / 73a6abc0 / 1182034d / 9b2eae04 / ea25fcd1)
驗證 STG user 手動端對端測試通過;POC migration 已套用並驗證
Notion CM-1032(b283f1ab 結案狀態同步)+ 本 arc 主案(見 §7)
核心教訓 同一 bug 修三次才修對 — 寫入端 / 讀取端 / 第二寫入者三層各自獨立

1. 問題與三層 root cause

現象:覆核輪的控制項導覽顯示母輪全部控制項,而不是只顯示本輪需複驗的那幾條 (母輪 not_met)。POC 實證(專案 659deb51 / round 8):AP=15 條、AR=4 條、job=4 條 —— 三端只有 AP 錯。

同一個「範圍沒收斂」的表象,實際上由三個彼此獨立的成因造成,逐層剝開才修對:

層 成因 修正 commit
① 寫入端 launch_reverify → clone_ap_for_round 原樣複製母輪 control_selections(全集)。narrowed(母輪 not_met)稍後才在 _generate_reverify_prep_jobs 算出,只餵給 job 生成、沒回寫 AP 167f578f
② 讀取端 build_control_tree_by_ssp_id 只在凍結輪(round.ssp_id == ssp_id)才讀本輪 AP。覆核輪 planning 階段 ssp_id IS NULL(SSP 快照延到 launch_audit),FE 退用 living SSP → 前置條件不成立 → 退回 ProfileResolutionService.resolve 的 living profile 全集 1182034d
③ 第二寫入者 clone_ap_for_round 也複製了 ap_assessment_activities.related_controls(同為全集);set_tasks 結尾的 _backfill_scope_from_tasks(FR-040 Phase9 行程驅動)用行程控制項聯集回寫 AP → 蓋掉第 ① 層的修正 9b2eae04

③ 的 STG 實證(專案 b4491646):04:08:04 建輪時 AP 455 為 1 條(正確),04:25:26 按 「啟動稽核」,04:25:37 被改寫成 2 條(母輪全集)—— 建輪後 11 秒被打回。job 端 wecm(round_id=2447) 始終正確。

OSCAL 語意依據(非僅顯示問題)

OSCAL v1.2.2 reviewed-controls.control-selections:AP 的是「本次計畫查哪些」、 AR result 的是「本次實際查哪些」。覆核輪=partial reassessment,本就該有自己收斂的 reviewed-controls。

修正前 AP=15 / AR=4 的 diff 會被讀成「計畫查 15 條卻只查了 4 條」(像評估未完成), 匯出 OSCAL AP 會把這個矛盾一併輸出。佐證:ar_results.reviewed_controls 在 schema 是 per-result required[1],正因每輪範圍本可不同。


2. Commits

Hash 標題 主要檔案
b283f1ab finalize_audit 漏同步 project.status——順利結案專案永遠停在 in_progress app/grc/service/audit_round_app_service.py
167f578f 覆核輪 AP reviewed-controls 未收斂——控制項導覽顯示母輪全集 assessment_plan_app_service.py(新增 narrow_reviewed_controls_for_reverify)+ audit_round_app_service.py
73a6abc0 (舊)覆核輪 AP reviewed-controls 既有資料收斂 migration scripts/sql/(已被 ea25fcd1 重寫取代,只改一張表不完整)
1182034d planning 階段覆核輪控制樹仍讀 living profile 全集——補漏 167f578f app/oscal/service/ssp_control_implementation_service.py(新增 _is_planning_stage_reverify)
9b2eae04 覆核輪稽核行程仍帶母輪全集——set_tasks 回填把收斂好的 AP 蓋回去 assessment_result_app_service.py(抽出 narrowed_control_ids_from_parent)+ assessment_plan_app_service.py(clone_ap_for_round 加 narrowed_control_ids 參數)+ audit_round_app_service.py
ea25fcd1 migration 重寫成兩張表——只改 reviewed-controls 會被行程聯集蓋回 scripts/sql/2026-08-02-reverify-round-narrow-ap-reviewed-controls.sql

b283f1ab 是同 session 順手修的另一支獨立 bug(結案狀態同步),不屬於範圍收斂三層, 已由 Notion CM-1032 涵蓋,此處併列以完整交代 branch 上的六支 commit。


3. 改動範圍(檔案 × 改動)

BE

檔案 改動
app/grc/service/audit_round_app_service.py ① finalize_audit 補 _sync_project_status(+4 行)② _generate_reverify_prep_jobs 於 narrowed 非空後呼叫 narrow_reviewed_controls_for_reverify ③ launch_reverify clone 前先算 narrowed 並傳入 clone_ap_for_round
app/grc/service/assessment_plan_app_service.py ① 新增 narrow_reviewed_controls_for_reverify(ap_id, control_ids, curr_user)——不走對外入口 set_reviewed_controls(吃 ap_uid + 內含 auditor 權限檢查,建輪內部流程無 curr_user_id 語境),直接委派 v2 domain service;description / control_objective_selections 傳 None → v2 upsert 只覆寫 control_selections。② clone_ap_for_round 加 narrowed_control_ids 參數,同步收斂 reviewed-controls + activity.related-controls;過濾後控制為空的 activity 整條不 clone,其 task 一併不 clone(不留孤兒行程)
app/grc/service/assessment_result_app_service.py 抽出 narrowed_control_ids_from_parent(parent)(直接吃母輪 entity),原 narrowed_control_ids_for(round_entity) 委派至此——同一份邏輯不分岔
app/oscal/service/ssp_control_implementation_service.py 新增 _is_planning_stage_reverify(close-out + ssp_id IS NULL + 有 assessment_plan_id);凍結條件之外新增此分支,一樣讀本輪 AP 當控制母體
scripts/sql/2026-08-02-reverify-round-narrow-ap-reviewed-controls.sql 既有資料修正:備份兩張表 → ap_reviewed_controls.control_selections 15→4 → ap_activities.related_controls 15→4 → schema_migrations 登記

測試碼(已 commit,未實跑,見 §6)

檔案 內容
test/test_audit_round_app_service.py TestFinalizeAuditSyncsProjectStatus ×3、TestLaunchReverifyNarrowsApScope ×3(收斂到 narrowed / 母輪 AP 零觸碰 / narrowed 空時不動 AP)
test/test_ssp_control_tree_round_scope.py(新檔) 判定函式 ×4(planning 覆核輪認得 / 無 AP 退回 / frozen 不走本分支 / 初始輪與 surveillance 不縮範圍)+ scope 分支 ×3
test/test_fr040_ap_task_linkage.py clone 收斂行程相關 ×N

FE

零改動——導覽、規劃頁、AP 編輯頁、AR 矩陣、匯出全部自動一致(因為都讀同一顆 AP)。


4. 行為差異(對 user 可見)

情境 修正前 修正後
覆核輪控制項導覽(planning 階段) 顯示母輪全部控制項 只顯示母輪未通過(not_met)的控制項
覆核輪控制項導覽(launch_audit 後 / 凍結) 同上(被 set_tasks 回填打回全集) 維持收斂
覆核輪稽核行程(AP activities) 帶母輪全部控制的行程 只留與 narrowed 有交集的行程;無交集者整條不建(含其 task)
按「啟動稽核」後 AP 被 _backfill_scope_from_tasks 打回全集 行程本身已收斂,聯集回填結果仍是 narrowed
匯出 OSCAL AP AP=全集 / AR=narrowed,語意矛盾 兩者一致
專案列表「已完成」篩選 稽核順利通過(無 not_met)的專案永遠 in_progress,篩選撈不到 結案即同步 completed

非覆核情境零影響:clone_ap_for_round 未傳 narrowed_control_ids 時行為與修正前完全一致; _is_planning_stage_reverify 綁 round_type == "close-out",初始輪 / surveillance 的 母體真相仍是 living profile。


5. 規範文件

文件 狀態
docs/issues/resolved/2026-08-02-reverify-round-ap-scope-not-narrowed.md 已完成(2026-08-02):⓪ Resolution 段、三層 root cause(§3.5/§3.6/§3.7)、§4.2 JSONB 形狀更正,已 git mv 到 resolved/
docs/specs/current/(專案管理群 + 稽核執行群) 已完成(2026-08-02):project-management/_overview.md、project-planning.md、project-overview.md、audit-execution/_overview.md、ap-authoring.md
scripts/sql/2026-08-02-reverify-round-narrow-ap-reviewed-controls.sql 已 commit,檔頭含完整背景 + 檔末還原語句
Notion CM-1032 b283f1ab 已建卡(狀態:修正待驗證,待更新為 Done)
本 SUMMARY 放 FR-053.1 handoff/ —— 本 arc 修的是 FR-053 第 3 刀「讀取端按輪過濾 + 凍結輪讀本輪 AP reviewed_controls」與 FR-053.1「覆核輪 clone」兩者交會處的漏網,屬 FR-053.1 續作 bug fix,不另立 FR

6. 已知 follow-up

6.1 測試未實跑 ⚠️

三個 code fix(167f578f / 1182034d / 9b2eae04)測試碼皆已補齊並 commit,但依 user 指示本次跳過實跑 pytest,未經執行驗證。實際驗證來自 user 在 STG 的手動端對端測試。 下次進版或 CI 接入前應補跑:

pytest test/test_audit_round_app_service.py test/test_ssp_control_tree_round_scope.py test/test_fr040_ap_task_linkage.py -v

6.2 文案統一未做(user 已拍板,延後執行)

「待複驗」→「待覆核」全站統一,以及覆核輪預設名稱調整。FE + BE 全改,兩個 commit 分開:

Repo 檔案 現況
FE src/config/locales/i18n/zh-tw/grc-shared.json:58 "pending_reverify": "待複驗"
FE src/config/locales/i18n/zh-tw/error-code.json:455 GRC_412037「母輪不在待複驗狀態,無法發起覆核」
FE src/views/project/DashboardView.vue:151 註解 // amber — 待複驗
BE common/code/grc_error_code.py:248 GRC_REVERIFY_PARENT_NOT_PENDING = ("母輪不在待複驗狀態,無法發起覆核", "GRC_412037")
BE app/grc/service/audit_round_app_service.py:610 name=name or f"複驗-第{round_no}輪" → 應為「覆核-第N輪」

BE error code 文案改動須同步 FE error-code.json(含 en / zh-cn,見 memory feedback_be_error_code_fe_i18n_sync)。

6.3 STG 既有髒資料未清

本次 migration 只清了 POC。STG 上既有的 close-out 輪(activity 96 等)仍帶母輪全集, 需另寫一支 migration(兩張表,同 ea25fcd1 模式,值取自同輪 ar_results.reviewed_controls)。 依環境異動鐵律,套 STG 需決策者當次明確指示。

6.4 ea25fcd1 未 push

branch fix/1.13.0-bug 上其餘 commit 已在 origin,ea25fcd1 尚未 push(等 user 明示)。

6.5 POC 備份表待清

確認無需回退後可 DROP:

DROP TABLE IF EXISTS oscal.bak_20260802_ap_reviewed_controls;
DROP TABLE IF EXISTS oscal.bak_20260802_ap_activities;

7. 部署 handover

項目 狀態
DEV code 已在 branch,重啟 BE 生效;migration 未套(DEV 無對應髒資料需確認)
STG code 已部署並經 user 手測(b4491646 / 12767c34 通過);既有髒資料未清(見 6.3)
POC migration ea25fcd1 已套用並驗證——專案 659deb51 / round 8 / ap 6,兩張表皆 15 → 4,母輪(ap 5 / activity 17)維持 15 條未動。備份表已建
BE 重啟 改動全在 app service 層,pull 後必須重啟 BE
FE 零改動,不需重新 build
Breaking change 無

migration 套用指令(供其他環境參考)

psql -h <host> -p 25432 -U cmmgr -d <db> --single-transaction -v ON_ERROR_STOP=1 \
  -f scripts/sql/2026-08-02-reverify-round-narrow-ap-reviewed-controls.sql

⚠️ 該檔 WHERE 明確鎖 POC 的 r.id = 8,不是通用 UPDATE。要套其他環境必須先盤點 該環境的 round / ap id 並改寫條件(見 6.3)。

migration 已知陷阱(重寫時必看)

兩張目標表的 JSONB 形狀不同,不可共用同一個賦值(2026-08-02 POC 唯讀實測):

ar_results.reviewed_controls                = [{"include-controls":[...]}]      陣列
ap_reviewed_controls.control_selections     = [{"include-controls":[...]}]      陣列(同形)
ap_assessment_activities.related_controls   = {"control-selections":[{...}]}    物件,多一層

故 ap_reviewed_controls 整欄賦值、ap_assessment_activities 需 jsonb_build_object 包一層。