| 項目 | 內容 |
|---|---|
| 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 修三次才修對 — 寫入端 / 讀取端 / 第二寫入者三層各自獨立 |
現象:覆核輪的控制項導覽顯示母輪全部控制項,而不是只顯示本輪需複驗的那幾條 (母輪 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 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],正因每輪範圍本可不同。
| 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。
| 檔案 | 改動 |
|---|---|
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 登記 |
| 檔案 | 內容 |
|---|---|
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 |
零改動——導覽、規劃頁、AP 編輯頁、AR 矩陣、匯出全部自動一致(因為都讀同一顆 AP)。
| 情境 | 修正前 | 修正後 |
|---|---|---|
| 覆核輪控制項導覽(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。
| 文件 | 狀態 |
|---|---|
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 |
三個 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
「待複驗」→「待覆核」全站統一,以及覆核輪預設名稱調整。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,見 memoryfeedback_be_error_code_fe_i18n_sync)。
本次 migration 只清了 POC。STG 上既有的 close-out 輪(activity 96 等)仍帶母輪全集, 需另寫一支 migration(兩張表,同 ea25fcd1 模式,值取自同輪 ar_results.reviewed_controls)。 依環境異動鐵律,套 STG 需決策者當次明確指示。
ea25fcd1 未 pushbranch fix/1.13.0-bug 上其餘 commit 已在 origin,ea25fcd1 尚未 push(等 user 明示)。
確認無需回退後可 DROP:
DROP TABLE IF EXISTS oscal.bak_20260802_ap_reviewed_controls;
DROP TABLE IF EXISTS oscal.bak_20260802_ap_activities;| 項目 | 狀態 |
|---|---|
| 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 | 無 |
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)。
兩張目標表的 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 包一層。