狀態:設計定稿待 user review(Phase 2 產出) 提出:2026-07-21 來源 Notion case:
- 母題
3a4346da-4cd0-80ed「專案規劃階段, 再點擊要推進下一階段時, 需做一些基本檢核」(功能=專案規劃)- 子項
3a3346da-4cd0-8020「從 規劃 → 下一步, 如果有任務沒執行, 要跳出提醒, 給 User Confirm, 確認要往下才進行」(功能=專案總覽,母題內文@mention) arc 關聯:relates to FR-038(round 狀態機 / stage_advance)、FR-050(stage 歷程 / Banner)
專案在規劃階段(planning)按 FlowPhaseBanner 的「推進下一階段」按鈕(實際觸發 launch_audit)時,推進之前做兩項基本檢核,各自若不滿足就跳提醒、讓 user confirm 放行才往下(都不是硬擋):
not_started / in_progress > 0),跳提醒帶「還有 N 個任務未完成」→ confirm 放行。兩項檢核在「按推進」同一瞬間觸發,聚合列在同一個 confirm 彌窗(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) |
app/flow_engine/service/stage_advance_service.py::advance_stage:
| 路徑 | 觸發 | UI 呈現 | 可否放行 |
|---|---|---|---|
| precondition fail | _run_precondition 回 passed=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。
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_execution,status=TODO),指派由 PM 之後手動。
main_role(StageAdvanceService._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 解析可參考。workflow_execution(_resolve_workflow_context(round_uid) 回 workflow_template_xml)。planning 是主流程第一個 UserTask,此時整份主流程 XML 已可讀。ParticipantRole StrEnum ∈ {manager, reviewer, auditor, viewer}(common/enum/participant_enum.py);查 ProjectParticipantDomainService 拿專案所有 participant 的 role 集合。⚠️ main_role 值域對齊:BPMN
main_role存的是否等於ParticipantRole值(manager/auditor/…)需在 plan Step 0 抽樣真實模板 XML 驗證。若模板存的是 stage_object 角色碼(另一套詞),比對前要 normalize;此為 plan 的前置查證第一項(見 implementation-plan §Step 0)。
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 的既有二次確認仍在,維持不變)。
邊界 / 錯誤路徑:
user_can_advance=false → 根本看不到推進按鈕(既有行為,不變)。force=true 但非 manager:既有 advance_stage line 303 擋 GRC_NOT_MANAGER(不變)。| 層 | 檔案 | 動作 |
|---|---|---|
| Handler | app/grc/service/oscal_stage_handlers.py::LaunchAuditOnCompleteHandler |
execute() 內、呼叫 launch_audit 前先跑 checker;force 從 ctx 讀 |
| 新 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(callerLaunchAuditOnCompleteHandler.execute已在advance_stage的@transactionscope 內);docstring 標「caller 必須在 @transaction scope 內」。
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}))
# 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)}))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。main_roles + manager override(advance_stage line 292)——不變。force=true 放行:既有 line 303 限 manager(GRC_NOT_MANAGER)——不變。本案不新增權限碼(檢核走 warning 非 error)。launch_audit line 371:status=planning、living_ssp 存在)——不變,檢核 A/B 疊加在其前。flow_engine.precondition.* 同族的 i18n warning key(FlowPhaseBanner 既有 getPreconditionMessage / force_confirm 機制)。{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_rejectroles 陣列(auditor/reviewer…)FE 轉 lang.grc_participants.role_* 既有翻譯。FlowPhaseBanner.vue:
doAdvance 的 needsConfirmation 分支(line 442)已存在 → 只需把 warningMessage 從單句擴成「讀 handler_result.reasons 陣列 → 逐條 t() → 換行聚合」。useStageInfo.js::advance 需把 BE 回的 handler_result.reasons 透出到 result.reasons(目前只透 needsConfirmation / warningMessage)。ProjectPlanningView / ProjectAuditorOverview)都嵌 Banner → 零額外改動即雙頁涵蓋。useConfirm(Banner 已 import),內容改多行 HTML/列表。RoundPrepTasksDoneCheck(維持 lenient;檢查全在 handler warning 層)。advance_stage 核心流程(只擴 handler_result 欄位)。ParticipantRole 值 —— 抽真實模板 XML 驗(§2.3 ⚠️)。count_incomplete_prep_jobs 的「本輪」scope:prep-job 是專案層級(非 round 綁定,見 RoundPrepTasksDoneCheck 註解)——多輪專案時是否要只算當前輪?規劃階段通常是首輪,先按「專案全部 in-scope prep-job」,多輪情境列 follow-up。handler_result.reasons 透出鏈:確認 advance_stage line 373-378 halt 分支把整個 handler_result 原樣回 FE(含新增的 reasons)——讀碼確認無 schema 白名單濾掉。