# FR-051 實作計畫 — 規劃階段推進前置檢核

> 依 `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。

---

## CLAUDE.md 規範檢查（全 plan 適用）

- [x] **DDD 層級**：Route 不查 DB（本案不新增 route）；App/handler 層透過 domain service；新 `count_incomplete_prep_jobs` 只在 infra repo 寫 SQL。
- [x] **@transaction**：checker 為 helper，不加 `@transaction`（caller `advance_stage` 已開 scope）；docstring 標「caller 須在 @transaction scope」。
- [x] **Repo session lazy**：新 count 方法掛既有 `grc_project_repo_impl`（已 `@property session` / `get_session()`）。
- [x] **權限**：不新增權限碼；沿用 `advance_stage` 既有 manager/main_role + force=manager 守門。
- [x] **前置條件**：疊加在 `launch_audit` 既有前置（status=planning / living_ssp）之前，不改既有。
- [x] **Error code**：不新增（走 warning，非 error）。i18n key 走 `flow_engine.readiness.*` 同族。
- [x] **不重複造輪子**：檢核 A reuse `get_ap_dashboard` 同 JOIN；檢核 B 用既有 `BpmnUtils` + `ProjectParticipantDomainService`。
- [x] **BE→FE i18n 同步**：新增 3 語（en/tw/cn）warning key。
- [x] **顯式 git add，禁 -am**；不切 branch；push 等 user 明示。

---

## Step 0 — 前置查證（動手前必做，design §8）

**BE**：
1. 抽 1~2 份真實主流程模板 XML（DEV DB `workflow_templates.xml` 或資源庫），確認 userTask `main_role` 值域 == `ParticipantRole` 值（manager/auditor/reviewer/viewer）。
   - 若不符（存 stage 角色碼）→ 在 checker 加 normalize map，並在此 plan 補一列。
2. 讀 `advance_stage` line 366-378 halt 分支，確認 `handler_result`（含新增 `reasons`）原樣回 FE，無 schema 白名單過濾。
3. 確認 `count_incomplete_prep_jobs` 的「本輪 vs 專案」scope（design §8.2）——首輪按專案全部 in-scope prep-job，多輪列 follow-up。

**輸出**：Step 0 結論寫一段到本檔末「Step 0 查證結果」，再開工。

---

## BE 任務（compliance-manager-be）

### Task B1 — domain count 方法（檢核 A 資料源）〔≤0.5d〕
- `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 + 委派。
- **驗**：pytest 直接對 DEV 種子專案跑，比對 `get_ap_dashboard` 的 `not_started+in_progress` 一致。

### Task B2 — PlanningReadinessChecker（新檔）〔0.5d〕
- 新 `app/grc/service/planning_readiness_checker.py`：
  ```python
  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，不阻推進。"""
  ```
  - 檢核 A：`n = count_incomplete_prep_jobs(project_uid)`；`n>0` → append `{"code":"planning_prep_jobs_incomplete","context":{"count":n}}`。
  - 檢核 B：取本輪主流程 XML（audit_round → workflow_execution.workflow_template_xml）→ `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**：`di_containers/` grc container 註冊 checker（注入上述 4 domain service），注入進 `LaunchAuditOnCompleteHandler`。
- **驗**：unit test 三情境（都通過 / 只缺 job / 只缺角色 / 兩者皆缺）。

### Task B3 — 掛進 LaunchAuditOnCompleteHandler〔0.25d〕
- `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`。
- **⚠️ 守住 design §7 前提（review 調整 2）**：`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`）。

### Task B4 — BE 端 i18n / 常數〔0.1d〕
- warning code 常數集中（如 `common/enum` 或 checker 內常數），確保與 FE i18n key 對得上。

---

## FE 任務（compliance-manager-fe）

### Task F1 — useStageInfo 透出 reasons〔0.25d〕
- `src/composables/useStageInfo.js::advance`：BE `handler_result.reasons` 透到回傳 `result.reasons`（現只透 `needsConfirmation`/`warningMessage`）。

### Task F2 — FlowPhaseBanner 聚合彌窗〔0.5d〕
- `src/components/grc/FlowPhaseBanner.vue::doAdvance` 的 `needsConfirmation` 分支（review 確認 `:442-450` 存在、accept 帶 `force:true` 重打）：
  - **⚠️ 必保留無-reasons fallback（review 調整 1）**：新 warning shape 是 `{"warning":"planning_readiness","reasons":[...]}`，**沒有 message key**。F2 邏輯 =「`result.reasons` 有值 → 聚合渲染；**否則走既有 `warningMessage` / `default_message` fallback**」。**不可只寫 reasons 分支**——要保住其他 handler 未來回單句 warning（如既有 `launch_audit` incomplete_tasks 提示）的契約不被破壞。
  - reasons 有值時：逐條 map 成訊息（`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`。
  - ConfirmDialog header/accept/reject 用 `lang.flow_engine.readiness.confirm_*`。
  - accept → `doAdvance({...payload, force:true})`（既有）。
- **兩頁自動涵蓋**：ProjectPlanningView / ProjectAuditorOverview 都嵌 Banner，零額外改。

### Task F3 — i18n key（3 語）〔0.15d〕
- `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
```

- B1~B4 可先於 FE；FE F1→F2 有序，F3 可並行。
- 回滾：每 Task 一 commit，可獨立 revert（B3 是唯一改既有行為點，revert 即恢復無檢核）。

---

## 完成標準
- [ ] BE pytest：checker 四情境 + advance 整合（force true/false）全綠。
- [ ] 規劃頁 & 總覽頁 manual smoke：缺 job / 缺角色 / 兩者 → 聚合彌窗正確列出；confirm 放行後正常推進；取消不變。
- [ ] 都通過時不跳彌窗（既有二次確認仍在）。
- [ ] i18n 三語無 raw key。

---

## Step 0 查證結果

> 2026-07-21 首腦 review 已預驗銷掉三項中的兩項，Step 0 剩一項必做。

**✅ 已預驗銷掉（不用重做）**：
- **Step 0-2 handler_result 透出鏈** ✅：`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。
- **force 從 ctx 讀** ✅：`advance_stage` 開頭已有 `ctx.setdefault("force", force)`，handler `execute(ctx=ctx)` 拿得到 → B3 寫法（`force = bool(ctx.get("force"))`）成立。
- **FE 掛點** ✅：`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）**：
- seed 檔（`scripts/sql/seeds/bpmn/builtin-*.bpmn`）：`main_role` ∈ `{manager, auditor, reviewer}`。
- **live DEV DB `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 實作。**
