依
docs/features/FR-051-2607-planning-advance-prechecks/design.md。 跨 repo:BE(compliance-manager-be)+ FE(compliance-manager-fe)。E2E 後補(Phase 4 test-plan)。 只規劃不實作——本檔為 plan,user approve 後才進 Phase 5。
BE:
workflow_templates.xml 或資源庫),確認 userTask main_role 值域 == ParticipantRole 值(manager/auditor/reviewer/viewer)。
advance_stage line 366-378 halt 分支,確認 handler_result(含新增 reasons)原樣回 FE,無 schema 白名單過濾。count_incomplete_prep_jobs 的「本輪 vs 專案」scope(design §8.2)——首輪按專案全部 in-scope prep-job,多輪列 follow-up。輸出:Step 0 結論寫一段到本檔末「Step 0 查證結果」,再開工。
infra/grc/repository/grc_project_repo_impl.py:新增 count_incomplete_prep_jobs(project_uid) -> int,reuse get_ap_dashboard 的 JOIN,COUNT(*) FILTER (WHERE je.status != COMPLETED AND je.type=USER)。domain/grc/service/grc_project_domain_service.py + repo interface:加對應 abstract + 委派。get_ap_dashboard 的 not_started+in_progress 一致。app/grc/service/planning_readiness_checker.py:
class PlanningReadinessChecker:
def __init__(self, grc_project_domain_service, project_participant_domain_service,
audit_round_domain_service, workflow_execution_domain_service): ...
def check(self, round_uid, project_uid) -> list[dict]:
"""回 [{code, context}];caller 須在 @transaction scope。fail-open:任一子檢查
取不到資料 → skip 該項 + log warning,不阻推進。"""n = count_incomplete_prep_jobs(project_uid);n>0 → append {"code":"planning_prep_jobs_incomplete","context":{"count":n}}。BpmnUtils(xml).jobs 收 main_role(split ",")去重 → required_roles;participant_roles = {p.role for p in participants};missing = required - participant;非空 → append {"code":"planning_roles_unassigned","context":{"roles":sorted(missing)}}。di_containers/ grc container 註冊 checker(注入上述 4 domain service),注入進 LaunchAuditOnCompleteHandler。app/grc/service/oscal_stage_handlers.py::LaunchAuditOnCompleteHandler:
__init__ 多注入 planning_readiness_checker。execute:force = bool((ctx or {}).get("force"));if not force: warnings = checker.check(round_uid, project_uid); if warnings: return {"warning":"planning_readiness","reasons":warnings};否則照舊 launch_audit。advance_stage 在 not force 時 precondition 先於 handler 跑(stage_advance_service.py:313-331)。planning 的 precondition RoundPrepTasksDoneCheck 必須維持 lenient(不能改嚴)——否則會在 handler warning 之前先硬擋,破壞「confirm 放行」語意。檢查全放 handler warning 層,precondition 一行都不動。advance_stage 整合 test — force=false 有 warning → 回 {"advanced":false, handler_result.reasons=[...]}、BPMN 未推進;force=true → 正常推進。注意 test 必 patch logger(memory feedback_test_logger_patch_db_handler)。common/enum 或 checker 內常數),確保與 FE i18n key 對得上。src/composables/useStageInfo.js::advance:BE handler_result.reasons 透到回傳 result.reasons(現只透 needsConfirmation/warningMessage)。src/components/grc/FlowPhaseBanner.vue::doAdvance 的 needsConfirmation 分支(review 確認 :442-450 存在、accept 帶 force:true 重打):
{"warning":"planning_readiness","reasons":[...]},沒有 message key。F2 邏輯 =「result.reasons 有值 → 聚合渲染;否則走既有 warningMessage / default_message fallback」。不可只寫 reasons 分支——要保住其他 handler 未來回單句 warning(如既有 launch_audit incomplete_tasks 提示)的契約不被破壞。planning_prep_jobs_incomplete → t('lang.flow_engine.readiness.prep_jobs_incomplete', {count});planning_roles_unassigned → t('...roles_unassigned', {roles: roles.map(r=>t('lang.grc_participants.role_'+r)).join('、')}))→ 換行/列表聚合成 message。lang.flow_engine.readiness.confirm_*。doAdvance({...payload, force:true})(既有)。src/config/locales/i18n/{en,tw,cn}/(對應 flow_engine 檔)新增:
flow_engine.readiness.prep_jobs_incomplete(還有 {count} 個規劃階段任務尚未完成)flow_engine.readiness.roles_unassigned(流程需要「{roles}」角色,但專案參與人員尚未指派,流程可能無法進行)flow_engine.readiness.confirm_header / confirm_accept(仍要推進)/ confirm_reject(取消)grc_participants.role_auditor / role_reviewer 等既有角色譯名存在(無則補)。Step 0 查證
↓
B1(count) → B2(checker,依 B1) → B3(掛 handler,依 B2) → B4(BE i18n 常數)
↓(BE pytest 綠)
F1(useStageInfo) → F2(Banner 彌窗,依 F1) → F3(FE i18n)
↓
manual smoke(規劃頁 + 總覽頁,both)→ Phase 4 test-plan → E2E
2026-07-21 首腦 review 已預驗銷掉三項中的兩項,Step 0 剩一項必做。
✅ 已預驗銷掉(不用重做):
advance_stage halt 分支(stage_advance_service.py:366-378)原樣回整個 handler_result;route 序列化 StageAdvanceResponseSchema.handler_result = fields.Dict(allow_none=True)(api/flow_engine/serializers/stage_advance.py:73),Dict 不濾 key → 新增 reasons 可原樣到 FE。advance_stage 開頭已有 ctx.setdefault("force", force),handler execute(ctx=ctx) 拿得到 → B3 寫法(force = bool(ctx.get("force")))成立。useStageInfo.js:67-73 / FlowPhaseBanner.vue:442-450 needsConfirmation 分支存在、accept 帶 force:true 重打 → F1/F2 落點正確。✅ Step 0-1 main_role 值域(2026-07-21 已驗,live DEV DB):
scripts/sql/seeds/bpmn/builtin-*.bpmn):main_role ∈ {manager, auditor, reviewer}。compliance.flow_templates.bpmn_xml(含專案 cloned 模板,11/12 模板有 main_role)distinct 值域 + 筆數:manager×21 / reviewer×11 / auditor×6。ParticipantRole(manager/auditor/reviewer),全部合法。viewer 從不作 main_role(合理,觀察者不驅動階段)→ checker 不需 normalize map,B2 檢核 B 直接 set 相減即可。⏳ Step 0-3(設計已定,無需驗):首輪按專案全量 prep-job,多輪情境列 follow-up。
→ Step 0 全數清空,可進 Phase 5 實作。