FR-051 — 規劃階段推進前置檢核(Planning Advance Pre-checks)

狀態:設計定稿待 user review(Phase 2 產出) 提出:2026-07-21 來源 Notion case

  • 母題 3a4346da-4cd0-80ed「專案規劃階段, 再點擊要推進下一階段時, 需做一些基本檢核」(功能=專案規劃)
  • 子項 3a3346da-4cd0-8020「從 規劃 → 下一步, 如果有任務沒執行, 要跳出提醒, 給 User Confirm, 確認要往下才進行」(功能=專案總覽,母題內文 @mentionarc 關聯:relates to FR-038(round 狀態機 / stage_advance)、FR-050(stage 歷程 / Banner)

1. 需求白話(Phase 0 已與 user 對齊)

專案在規劃階段(planning)按 FlowPhaseBanner 的「推進下一階段」按鈕(實際觸發 launch_audit)時,推進之前做兩項基本檢核,各自若不滿足就跳提醒、讓 user confirm 放行才往下(都不是硬擋):

  • 檢核 A(子項 8020):本輪規劃階段的證據蒐集 job(per-AO prep-job)若還有沒執行完的(not_started / in_progress > 0),跳提醒帶「還有 N 個任務未完成」→ confirm 放行。
  • 檢核 B(母題 80ed 第二點):專案綁定的流程模板(BPMN)裡所有 userTask 用到的主要角色(main_role),逐一比對專案參與人員已指派的 role 集合;有缺(某 role 沒有任何參與人員)→ 跳提醒帶「缺少 X / Y 角色,流程可能無法進行」→ confirm 放行。

兩項檢核在「按推進」同一瞬間觸發,聚合列在同一個 confirm 彌窗(user 一次看完一次決定)。

1.1 五項設計決策(user 拍板)

# 決策 選定
1 檢核 A「任務」範圍 規劃階段的證據蒐集 job(per-AO prep-job,job_executions.status
2 檢核 B 缺角色 → 硬擋 or 軟提醒 軟提醒 + confirm 放行(對齊 case「提醒」語氣、與 A 同機制)
3 A / B 彌窗 共用單一彌窗、聚合列出所有 reasons
4 FR 編號 拆兩個 → 本案 FR-051(檢核);tab gate 另立 FR-052
5 confirm 放行需要誰 沿用既有 force 語意:manager(見 §5.3)

2. 現況盤點(實地 grep,非臆測)

2.1 推進機制既有兩條 confirm 路徑

app/flow_engine/service/stage_advance_service.py::advance_stage

路徑 觸發 UI 呈現 可否放行
precondition fail _run_preconditionpassed=False 按鈕 disabled + tooltip(FlowPhaseBanner.vue:51 ❌ UI 不給 force(硬擋)
handler warning handler execute(){"warning": ...}(line 366-378) ConfirmDialog → 按確認帶 force:true 重打(FlowPhaseBanner.vue:442 ✅ manager confirm 放行

關鍵:本案兩項檢核都要「軟提醒 + confirm 放行」→ 完全對應 handler-warning 路徑,不是 precondition。故 planning 階段的 precondition RoundPrepTasksDoneCheck(目前 lenient)維持不動,檢查全放進 planning 的 completion handler。

2.2 檢核 A 的完成度信號(canonical,可直接 reuse)

app/grc/service/project_service.py:560 get_ap_dashboard → domain → infra/grc/repository/grc_project_repo_impl.py(約 528-577):以 project_uid 統計 per-AO prep-job:

total_tasks, completed(je.status=COMPLETED), in_progress(PROCESSING), not_started(TODO)

→ 檢核 A 判定 = not_started + in_progress > 0不新造 SQL —— 抽一個 domain 層 count_incomplete_prep_jobs(project_uid) 共用同一條 JOIN(見 §4.2),避免第二套 job 完成度真相(CLAUDE.md 禁重複造輪子)。

prep-job 由 app/project/service/project_start_app_service.py::_generate_prep_jobs 在「專案成立」建(per-AO,workflow_execution + job_executionstatus=TODO),指派由 PM 之後手動。

2.3 檢核 B 的角色來源(實地確認)

  • 模板 userTask main_role:BPMN userTask extension property main_roleStageAdvanceService._resolve_main_roles 已在讀,可 comma 分隔多值)。列舉整份模板 userTask → jedi_flow_engine.common.utils.bpmn_uilts.BpmnUtils.jobs 逐一 .properties.get("main_role"))。app/flow_engine/util/bpmn_topology_validator.py 已有 _collect_user_tasks + extension 解析可參考。
  • 模板 XML 來源:規劃階段本輪已有 workflow_execution_resolve_workflow_context(round_uid)workflow_template_xml)。planning 是主流程第一個 UserTask,此時整份主流程 XML 已可讀。
  • 參與人員 roleParticipantRole StrEnum ∈ {manager, reviewer, auditor, viewer}(common/enum/participant_enum.py);查 ProjectParticipantDomainService 拿專案所有 participant 的 role 集合。
  • 既有 role-coverage 檢查:grep 全專案 (全新邏輯)。

⚠️ main_role 值域對齊:BPMN main_role 存的是否等於 ParticipantRole 值(manager/auditor/…)需在 plan Step 0 抽樣真實模板 XML 驗證。若模板存的是 stage_object 角色碼(另一套詞),比對前要 normalize;此為 plan 的前置查證第一項(見 implementation-plan §Step 0)。


3. User Mental Walkthrough(SOP 規則 2 強制)

1. Manager 進「專案規劃頁 / 專案總覽頁」→ 看到 FlowPhaseBanner,當前階段=規劃,
   主按鈕「啟動稽核」(label 由 BE 動態給)。
2. Manager 點「啟動稽核」→ 既有既有二次確認 ConfirmDialog("確定啟動稽核?")→ 按確定
   → 前端 advance(force=false)。
3. BE planning handler 先跑 PlanningReadinessChecker:
   3a. 檢核 A:本輪還有 3 個證據蒐集 job 未完成 → warning A。
   3b. 檢核 B:模板 userTask 需要 auditor / reviewer,但參與人員只有 manager
       → warning B(缺 auditor, reviewer)。
   → handler 回 {"warning": {...}, "reasons": [A, B]},advance_stage halt、不推進 BPMN。
4. 前端收到 needsConfirmation → 開「聚合 ConfirmDialog」:
      標題:仍要繼續推進?
      內容(條列):
        • 還有 3 個規劃階段任務尚未完成
        • 流程需要「稽核員 / 審核者」角色,但專案參與人員尚未指派,流程可能無法進行
      [取消] [仍要推進]
5a. Manager 按「取消」→ 停在規劃階段,什麼都沒變(可回頭補任務 / 補人員角色)。
5b. Manager 按「仍要推進」→ 前端 advance(force=true) →
    BE 跳過檢查 → launch_audit 正常執行 → 階段推進、toast「已成功推進階段」。
6. 若兩項檢核都通過(無 warning)→ 步驟 3 之後直接推進,不跳聚合彌窗
   (步驟 2 的既有二次確認仍在,維持不變)。

邊界 / 錯誤路徑

  • 非 manager 角色:規劃階段 main_role 通常僅 manager,非 manager user_can_advance=false → 根本看不到推進按鈕(既有行為,不變)。
  • force=true 但非 manager:既有 advance_stage line 303 擋 GRC_NOT_MANAGER(不變)。
  • 模板無 main_role 宣告 / 拿不到 XML:檢核 B skip(fail-open,只 log warning,不阻推進)—— 檢核是「提醒」不是「守門」,拿不到資料時不該擋 manager。
  • prep-job dashboard 查詢失敗:檢核 A skip(同上,fail-open + log)。

4. DDD 設計

4.1 層級落點

檔案 動作
Handler app/grc/service/oscal_stage_handlers.py::LaunchAuditOnCompleteHandler execute() 內、呼叫 launch_audit 前先跑 checker;forcectx
App helper app/grc/service/planning_readiness_checker.py(新檔) PlanningReadinessChecker.check(round_uid, project_uid) -> list[Warning]
Domain(檢核A) domain/grc/service/grc_project_domain_service.py + repo count_incomplete_prep_jobs(project_uid) -> int(reuse get_ap_dashboard 同 JOIN)
Domain(檢核B) 既有 ProjectParticipantDomainService 讀專案 participant role 集合(已有 list 方法)
BPMN 解析 jedi_flow_engine...BpmnUtils 列 userTask main_role(唯讀,無套件改動)

PlanningReadinessChecker 不加 @transaction(caller LaunchAuditOnCompleteHandler.execute 已在 advance_stage@transaction scope 內);docstring 標「caller 必須在 @transaction scope 內」。

4.2 檢核 A — domain count 方法(reuse SQL)

grc_project_repo_impl.py 抽出 per-AO prep-job 的 status 統計為可共用查詢,新增:

def count_incomplete_prep_jobs(self, project_uid: str) -> int:
    """本輪規劃階段 per-AO 收證據 job 中 status != COMPLETED 的數量。
    reuse get_ap_dashboard 同一 JOIN(je.status FILTER),只 COUNT(*) FILTER (WHERE je.status != COMPLETED)。"""

Checker:n = count_incomplete_prep_jobs(project_uid); if n > 0: warnings.append(Warning("planning_prep_jobs_incomplete", {"count": n}))

4.3 檢核 B — role-coverage 演算法

# 1. 拿本輪主流程 XML(_resolve_workflow_context 已有;或 checker 自行查 audit_round → workflow_execution)
# 2. BpmnUtils(xml).jobs → 收集所有 userTask 的 main_role(split ","),去重成 required_roles
#    - 過濾掉非參與角色語意的值(若有;plan Step 0 驗)
# 3. participant_roles = {p.role for p in participants}(該專案全部參與人員)
# 4. missing = required_roles - participant_roles
# 5. if missing: warnings.append(Warning("planning_roles_unassigned", {"roles": sorted(missing)}))

4.4 Warning 聚合形狀(BE → FE 契約)

LaunchAuditOnCompleteHandler.execute 在有 warning 且 not force 時回:

{
  "warning": "planning_readiness",          // 既有 advance_stage 只看 truthy 就 halt
  "reasons": [                              // 新增聚合欄位
    { "code": "planning_prep_jobs_incomplete", "context": { "count": 3 } },
    { "code": "planning_roles_unassigned",    "context": { "roles": ["auditor", "reviewer"] } }
  ]
}
  • advance_stage line 366 既有判斷 handler_result.get("warning") 為真即 halt、回 {"advanced": false, "handler_result": ...}不需改 advance_stage 核心(只是 handler_result 多帶 reasons)。
  • force=true:checker 不跑(或跑但忽略),直接 launch_audit

5. 權限 / 前置條件 / Error handling

5.1 權限

  • 推進按鈕本身:既有 main_roles + manager override(advance_stage line 292)——不變。
  • force=true 放行:既有 line 303 限 manager(GRC_NOT_MANAGER)——不變。本案不新增權限碼(檢核走 warning 非 error)。

5.2 前置條件

  • planning 階段既有轉換前置(launch_audit line 371:status=planning、living_ssp 存在)——不變,檢核 A/B 疊加在其前。

5.3 Error / i18n

  • 檢核走 warning(非 error code),沿用 flow_engine.precondition.* 同族的 i18n warning key(FlowPhaseBanner 既有 getPreconditionMessage / force_confirm 機制)。
  • 新增 FE i18n key({en, tw, cn} 三語,per CLAUDE.md BE→FE i18n 同步):
    • lang.flow_engine.readiness.prep_jobs_incomplete(含 {count} 插值)
    • lang.flow_engine.readiness.roles_unassigned(含 {roles} 插值)
    • lang.flow_engine.readiness.confirm_header / confirm_accept / confirm_reject
  • 角色顯示名:roles 陣列(auditor/reviewer…)FE 轉 lang.grc_participants.role_* 既有翻譯。

6. FE 設計(frontend-spec 摘要,細節見 FE 實作)

FlowPhaseBanner.vue

  • doAdvanceneedsConfirmation 分支(line 442)已存在 → 只需把 warningMessage 從單句擴成「讀 handler_result.reasons 陣列 → 逐條 t() → 換行聚合」。
  • useStageInfo.js::advance 需把 BE 回的 handler_result.reasons 透出到 result.reasons(目前只透 needsConfirmation / warningMessage)。
  • 兩頁(ProjectPlanningView / ProjectAuditorOverview)都嵌 Banner → 零額外改動即雙頁涵蓋
  • ConfirmDialog 用既有 PrimeVue useConfirm(Banner 已 import),內容改多行 HTML/列表。

7. 不在範圍(YAGNI)

  • 不動 precondition RoundPrepTasksDoneCheck(維持 lenient;檢查全在 handler warning 層)。
  • 不改 advance_stage 核心流程(只擴 handler_result 欄位)。
  • 不做其他階段(ap_authoring / audit / poam)的推進檢核 —— case 只講規劃階段。
  • 不做「自動指派缺角色」/「自動完成任務」—— 只提醒。
  • 不擋非 manager(既有 user_can_advance 已處理)。

8. 風險 / 待查證(plan Step 0 前置)

  1. BPMN main_role 值域 是否等於 ParticipantRole 值 —— 抽真實模板 XML 驗(§2.3 ⚠️)。
  2. count_incomplete_prep_jobs 的「本輪」scope:prep-job 是專案層級(非 round 綁定,見 RoundPrepTasksDoneCheck 註解)——多輪專案時是否要只算當前輪?規劃階段通常是首輪,先按「專案全部 in-scope prep-job」,多輪情境列 follow-up。
  3. handler_result.reasons 透出鏈:確認 advance_stage line 373-378 halt 分支把整個 handler_result 原樣回 FE(含新增的 reasons)——讀碼確認無 schema 白名單濾掉。