# FR-038 Wave 2 — 稽核輪次流程引擎再整合（含 AP 填寫階段）

> **狀態**：設計中（2026-06-16）
> **歸屬**：FR-038 OSCAL v2 重設計 Wave 2 增量（跨 B3 輪次狀態機 / B4 AP）
> **類型**：v2 切換的「原有功能再生」—— 把 [FR-026](../FR-026-2605-project-flow-engine-integrate/) BPMN 流程引擎從舊 AP retarget 到 v2 first-class `project_audit_rounds`，並加入新的 `ap_authoring` 階段
> **決策來源**：2026-06-16 設計對話（user 拍板）
> **連帶**：建專案流程照新規格實作（資源庫 + 第一輪流程範本；舊資料已清空、不做向後相容）

---

## 0. 為什麼歸在 FR-038（不另開 FR）

FR-038 是 OSCAL V1→V2 抽換底層的 arc，本職有二：**(1) 原有功能在 v2 基座上要能運作；(2) 新模型的新流程加入**。FR-026 的流程引擎在 v2 切換時被擱在已 dark 的舊 AP 上（孤兒），把它接回 v2 輪次 = 第 (1) 項；加 `ap_authoring` 階段 = 第 (2) 項。屬 FR-038 Wave 2（B3 輪次狀態機 + B4 AP）的自然延伸，故為本資料夾 sub-design，不佔新整數 FR。

---

## 1. 問題陳述

### 1.1 觸發脈絡
起點是建專案流程在 v2 切換後對不上（FE/BE payload 不一致）。**舊專案資料已全部清空 → 不做向後相容**，建專案流程**直接以本增量新規格實作**（資源庫 + 第一輪流程範本，見 D6）。`OscalProjectStartRequest` 可一併清掉舊 `Meta.unknown=EXCLUDE` / 舊欄位相容註解。

### 1.2 根本問題（深一層）
症狀只是表面。真正問題是**同一套稽核生命週期狀態機在 v2 並存兩份**：

| | ① FR-026 流程引擎 | ② v2 audit_round 狀態機 |
|---|---|---|
| 載體 | `stage_objects` + `flow_templates` + `stage_advance_service` + handler registry + Banner | `audit_round_app_service` 硬編碼 7 態 |
| 驅動 | **BPMN 可設定** | **寫死轉換** |
| key | 舊 AP（`ap_uid` + `assessment_plan_extensions`）| 新 `project_audit_rounds`（first-class）|
| 現況 | 程式還在、沒 dark，但綁在 v2 已 dark 的舊 AP 上 → 孤兒 | v2 實際在跑的 |

v2 切換時，把 FR-026 拆掉的「寫死 stage」在輪次層**又寫死回去**，FR-026 引擎被擱在死掉的 AP 上。`稽核流程範本` 這條線在 v2 整個掉了。

### 1.3 為什麼要修（需求面）
「依稽核情境組不同流程」是真需求（FR-026 README 三情境：正式稽核 / 內部自查 / 自我評估，stage 組合不同）。放棄它 = FR-026 整個白做。**user 拍板：走完整再整合（big-bang），不逃避。**

---

## 2. 兩個被混淆的「流程」（先釐清）

| | **稽核流程範本（本增量主角）** | per-AO 收證據 job |
|---|---|---|
| 來源 | FR-026 專案流程引擎 | FR-038 Q1 `_generate_prep_jobs` |
| 是什麼 | **輪次的執行流程**：規劃→填寫→稽核→改善→結案 | 單一控制項目標一個收證據 job |
| 粒度 | 整個輪次 lifecycle（BPMN 驅動 stage 推進）| per-assessment-objective |
| 綁定 | 應綁 `project_audit_rounds`（本增量）| 掛 `module_frames.template_uid`（不動）|

> `module_frames.template_uid`（per-AO 收證據）**不在本增量範圍**，維持現狀。

---

## 3. 決策（已拍板）

| # | 決策 | 選擇 | 理由 |
|---|------|------|------|
| D1 | 流程範本綁哪一層 | **`project_audit_round`** | 一專案多輪（initial→surveillance→close-out），每輪各綁/凍結自己的流程；V1 `_bind_main_workflow_to_ap` 本來就綁輪次（per-AP snapshot），這是回到正確分層 |
| D2 | 交付策略 | **big-bang 一次到位** | 直接把 round 轉換改 BPMN 驅動 + flow_template 綁 round + stage_advance 整合 |
| D3 | AP 填寫主責角色 | **auditor** | reviewed-controls / subjects / tasks 是「稽核規劃」決策，屬稽核員職責；與現有 `start_auditing`（auditor 守門）一致 |
| D4 | 何時選範本 | **第一輪在「建專案」選；後續輪在「建輪次」選** | 範本綁 round（D1），於 round 建立當下選；第一輪的建立**折進建專案**（見 D6），故第一輪範本在建專案頁選 |
| D6 | 建專案 ↔ 第一輪關係 | **建專案順帶建 round 1（initial）並綁所選流程範本** | 維持現有建專案 UI（資源庫 + 稽核流程範本雙下拉）；一頁完成「專案 + 第一輪」。round 1 進 `planning`，SSP snapshot 仍等 launch_audit。每 audit_round ↔ 一支範本（1:1，與 D1 一致）|
| D5 | stage 模型 / task_execution 去留 | **`planning → ap_authoring → audit → poam`；task_execution 收進 planning、review 暫不納** | OSCAL/NIST 800-53A：SSP 補完 + 收證據屬 **Prepare 期**（非 audit 執行），是 planning 的內涵；FR-026 `task_execution` 命名誤導。PM「確認任務完成才啟動稽核」= **planning 的推進 precondition（本輪 per-AO 收證據 job 全完成）**，不需獨立階段。且此 4 階段 **1:1 對齊已 shipped 的 v2 輪次 status（planning/audit_planning/auditing/remediation），零 CHECK migration**。review 留作未來 `-with-review` 變體（FR-026 機制保留可隨時加回）|
| O1 | 範本預設來源 | **(a)+(b)** | 資源庫加 `audit_flow_template_uid` 欄（框架維護者設公版預設）→ **首輪用資源庫預設、後續輪沿用前一輪、皆可覆寫**。動 `module_frames` schema + 框架維護頁多一欄 |

---

## 4. 目標架構（FR-026 引擎 retarget 到 round）

```
flow_template (master, 系統層 published)
   │ clone snapshot（建輪次時）
   ▼
project_audit_rounds  ──┬── flow_template_snapshot_uid   (新欄位)
                        └── workflow_execution_uid       (新欄位，running instance)
   │ stage_advance 改用 round_uid 解 context（不再 ap_uid）
   ▼
BPMN UserTask 串（範本定義，可不同情境不同串）：
   StartEvent → planning → ap_authoring(新) → audit → (Gateway) → poam → audit / End
```

### 4.1 canonical stage 鏈 ↔ round status ↔ handler 對應（D5 收斂）

**4 階段鏈**（builtin 全部走這串；review 暫不納，未來變體再加）：

```
planning ──→ ap_authoring ──→ audit ──→ poam ──→ End
```

| BPMN stage_object | `complete_handler_key` | round.status（推導）| on_complete 包 `audit_round_app_service` | `precondition_key` | main_role |
|---|---|---|---|---|---|
| `planning`（含 FR-026 task_execution）| `launch_audit` | `planning` | `launch_audit`（snapshot SSP 邊界③ + `create_draft_for_snapshot` 建 AP 草稿）| **`round_prep_tasks_done`**（本輪 per-AO 收證據 job 全完成；PM gate）| manager |
| **`ap_authoring`（新）** | `submit_ap` | `audit_planning` | `start_auditing`（建 AR + AO 全量矩陣）| `ap_reviewed_controls_set`（reviewed-controls 非空）| **auditor** |
| `audit` | `confirm_audit` | `auditing` | `finalize_audit`（AR 定版 + 有 not_met 生 POA&M）| `ar_all_verdicts_filled`（全 AO 判定完）| auditor |
| `poam` | `close_round` | `remediation` | `close_round`（→ pending_reverify / closed）| `poam_all_closed` | manager |
| EndEvent 前 terminal UserTask | `terminal_close` | `closed` | round 標 closed | — | — |

> **handler_key 沿用既有命名**（`launch_audit`/`confirm_audit`/`close_round`/`terminal_close`）但**改 wrap `audit_round_app_service`**（原 wrap 已 dark 的 `OscalAuditService`）。`activate_project` handler 退役（planning 不再對 activate，改對 launch_audit）。`review_decision` handler 保留但 builtin 不用（未來變體）。

### 4.1b round.status 推導（取代寫死 7 態）

round.status 不再由 `audit_round_app_service` 寫死常數，改由 **stage_advance 推進成功後依「當前 BPMN UserTask 的 stage_object_code」查下表回填**：

| stage_object_code | round.status |
|---|---|
| `planning` | `planning` |
| `ap_authoring` | `audit_planning` |
| `audit` | `auditing` |
| `poam` | `remediation` |
| （terminal / EndEvent）| `closed` |

- 對齊既有 DB CHECK 值域（`ck_audit_rounds_status`），**零 migration**。
- `audit_round_app_service` 各轉換方法**移除自身的 `e.status = STATUS_*` 寫死**，status 改由 stage_advance 在 handler 跑完後依當前 stage 統一回填（單一真相源）。
- 覆核（close-out）走子輪 + `parent_round_id`（FR-038 既有）；`pending_reverify` / close-out 連動維持 `close_round` handler 內邏輯。

**推進 ↔ status 寫入 ordering（單一真相源，避免 race）:**
1. stage_advance 解析當前 UserTask（= 當前 stage）→ 跑 precondition → dispatch handler。
2. handler 呼叫 `audit_round_app_service` 轉換方法：**保留** status **guard**（`if e.status != STATUS_X: raise`，讀的是「推進前」status，仍正確）+ **保留業務寫入**（`ssp_id` / `assessment_plan_id` / `ar_result_id` / `poam_id`）；**只移除 `e.status = STATUS_X`**。
3. BPMN engine 推進到下一 UserTask；stage_advance 依「**新的當前 stage**」查 §4.1b map 回填 `round.status`（status 反映「你現在所在的 stage」）。
4. **audit → closed/remediation 的分叉是 BPMN Gateway，不是純 map**：`finalize_audit` handler 回傳 gateway 變數 `has_findings`（= not_met_count>0），BPMN 路由 `has_findings→poam`(status=remediation) / 否則 `→End`(status=closed)。`poam_id` 仍由 `finalize_audit` 業務寫入。機制同 FR-026 §8 builtin gateway + `_next_stage_code` peek。

### 4.2 要動的塊（big-bang 全包）

**BE — schema**
- `project_audit_rounds` 加 `flow_template_snapshot_uid` + `workflow_execution_uid`（沿用既有 soft-ref 慣例，不建 ORM relationship；不開新表）。
- 新增 `ap_authoring` stage_object seed（i18n / route_pattern / handler_key / precondition_key / main_roles=["auditor"] / allowed_predecessors=["planning"] / allowed_successors=["audit"]）。
- builtin flow_templates 的 BPMN 重 seed：planning 與 audit 之間插入 `ap_authoring` UserTask。

**BE — flow 引擎 retarget**（registry interface 簽章 big-bang 一次改全）
- `domain/flow_engine/service/stage_completion_registry.py`：`IStageCompletionHandler.execute` / `IStagePreconditionCheck.check` 第一參數 `ap_uid` → `round_uid`。
- `app/flow_engine/service/stage_advance_service.py`：`_resolve_workflow_context` 改查 `project_audit_rounds.workflow_execution_uid`（不再走 `assessment_plan_extensions`）；`advance_stage` / `get_current_stage_info` 簽章 `ap_uid`→`round_uid`；內部 ~30 處 `ap_uid` 引用（log + `handler.execute(ap_uid=)` 呼叫點 + `_run_precondition`）全改；DI 加 `audit_round_domain_service`、移除 `assessment_plan_extension_domain_service`。推進成功後依 §4.1b 表回填 round.status。
- **`app/project/service/oscal_audit_service.py` 退場**：既有 6 handler 原 wrap 此 service（2A 已 dark no-op，DI 用 `_safe_register` try/except 靜默跳過）。本增量**全部 handler 建構子改注入 `audit_round_app_service`**、`execute(round_uid, …)`、拆掉 `_safe_register` dark-skip 鷹架（grc_containers.py:468-489）。
  - 改 wrap 的 6 handler：`Planning(→launch_audit)` / **新 `SubmitAp(→start_auditing)`** / `Audit(→finalize_audit)` / `Poam(→close_round)` / `TerminalClose(→round closed)` / `ReviewDecision`（保留不 wire）。`activate_project` handler 退役。
- **precondition 全 retarget**：既有 4 個（`all_tasks_assigned` / `all_tasks_completed` / `all_controls_verdict_filled` / `all_poam_closed`）`check(ap_uid)`→`check(round_uid)`；新增 `round_prep_tasks_done`（planning gate）+ `ap_reviewed_controls_set`。後者用 `AssessmentPlanAppService.get_ap_for_round(round_uid)` 取 AP，再查 reviewed-controls 數（需新增薄 method `count_reviewed_controls`，內部 `ApReviewedControlsRepoImpl.get_by_ap`）。
- **舊 ap-scoped route 退役**：`api/flow_engine/routes/stage_advance_route.py`（`/project/<uid>/ap/<ap_uid>/stage/*`）+ `api/flow_engine/__init__.py` 註冊一併移除/disable（否則 boot 出壞端點）。
- `create_round`：clone flow_template master → snapshot（`workflow_template_snapshot_service.clone_master_as_snapshot`，mirror `app/project/service/oscal_project_service.py:196`）→ 起 main `workflow_execution` → 回填 round 兩新欄位 + **`_to_dto` 加回傳這兩欄**。範本來源 O1：入參 > 前輪 snapshot 之 master > 資源庫 `audit_flow_template_uid`。
- **i18n**：新 precondition reason key（`round_prep_tasks_done` / `ap_reviewed_controls_set`）+ stage 按鈕 label 補 `config/translations/` zh_Hant_TW + en。

**BE — route**
- 新 `GET/POST /project/<project_uid>/audit-round/<round_uid>/stage/{info,advance}`（取代 ap-scoped）。
- 現有 `audit_round_route` 的 `launch-audit`/`start-auditing`/`close` 獨立轉換 route：收斂為 stage/advance 統一入口（O2，傾向統一、獨立 route deprecated）。

**BE — 建專案順帶建第一輪（D6，新規格）**
- `OscalProjectStartRequest` serializer（清掉舊相容）：`resource_library_uid` required + `flow_template_uid`（= 第一輪範本）+ `name/description/start_date/end_date/owner_uid/participants`。
- `ProjectStartAppService.start_project`：clone 資源庫 + 建 project/extension/participants/prep jobs 後，**呼叫 `audit_round_app_service.create_round(round_type='initial', flow_template_uid=…)` 建 round 1**（clone flow snapshot + 起 workflow + 綁 round）。未給 `flow_template_uid` → O1 fallback（資源庫 `audit_flow_template_uid`）。
- FE 建專案（新規格）：資源庫下拉送 `module_frame.uid`、保留稽核流程範本下拉送 `flow_template_uid`。

**FE**
- `FlowPhaseBanner.vue` + `useStageInfo.js` + `StageService.js`：props/endpoint `ap_uid` → `round_uid`。
- 新 **AP 填寫 stage 畫面**（routed stage `ap_authoring`）：reviewed-controls 多選 + subjects + tasks，接 `PUT …/reviewed-controls|subjects|tasks`。
- `ProjectCreateView.vue`：照新規格送 `resource_library_uid`（module_frame.uid）+ 保留流程範本下拉送 `flow_template_uid`（第一輪範本）。
- `RoundSwitcherBar` / `ProjectAuditorOverview`：Banner 改吃 round context。

---

## 5. 受影響檔案地圖（plan 用）

**BE**
- `infra/grc/model/project_audit_round.py` — 加兩欄
- `app/grc/service/audit_round_app_service.py` — create_round 綁範本；轉換方法被 handler 包
- `app/flow_engine/service/stage_advance_service.py` — `_resolve_workflow_context` / 簽章 retarget
- `domain/flow_engine/service/stage_completion_registry.py` — 註冊 `submit_ap` handler + `ap_reviewed_controls_set` precondition
- `api/project/routes/audit_round_route.py` — 加 round-scoped stage/info + stage/advance
- `api/project/serializers/project.py` — v2 shape（已是；FE 對齊即可）
- `scripts/sql/2026-06-1x-fr038-round-flow-binding.sql` — round 兩欄 + ap_authoring stage seed + builtin BPMN 重 seed
- `common/code/grc_error_code.py` — 新 error code（round 無 workflow binding / AP reviewed-controls 未設）

**FE**
- `src/components/grc/FlowPhaseBanner.vue`、`src/composables/useStageInfo.js`、`src/service/StageService.js` — retarget round
- `src/views/project/ProjectCreateView.vue` — 新規格：資源庫送 module_frame.uid + 保留流程範本下拉（第一輪）
- 新 `src/views/project/RoundApAuthoringView.vue`（或併入規劃頁）— AP 填寫
- `src/config/api/api.js` — round-scoped stage endpoint 常數

---

## 6. 不變式 / 邊界

- round.status 不再寫死 → 必須與 BPMN stage_object 對應表 100% 一致（DB CHECK 值域 + stage_object code 對齊）。
- snapshot 三層獨立性沿用 FR-026 §3.4：master 改不影響已啟動輪次。
- flow_template snapshot 綁 **create_round**（planning 起算）；SSP snapshot 仍在 **launch_audit**（邊界③）—— 兩個 snapshot 不同時機、不同物件。
- 權限 / @transaction / DDD 分層照 CLAUDE.md：route 不碰 DB、轉換在 app service、precondition 在 domain。

---

## 7. 開放項（plan 前要定）

- ~~**O1 範本預設來源**~~：**已定 (a)+(b)**（2026-06-16）—— `module_frames` 加 `audit_flow_template_uid` 欄；首輪用資源庫預設、後續輪沿用前一輪、皆可覆寫。schema 異動納入本增量。
- ~~**O2 獨立轉換 route 去留**~~：**已定 — 退役**（2026-06-16）。big-bang 後輪次轉換**一律走 stage/advance 統一入口**；`audit_round_route` 的 `launch-audit`/`start-auditing`/`finalize`/`close` 4 個直接轉換 route disable/移除（否則直呼這些方法會繞過 status 推導 + 撞 status guard，造成 status 不一致）。FE 改走 Banner stage/advance（FE plan）。
- **O3 close-out 覆核輪**：覆核子輪（`parent_round_id`）流程範本沿用母輪還是另選；與 FR-038 B5 launch-reverify 交界。
- **O4 舊 `assessment_plan_extensions` / ap-scoped stage route**：big-bang 後 DROP / disable（比照 DROP 退役表四查）。
- **O5 資料遷移**：dev 無正式輪次資料則零遷移；正式環境在跑輪次的 workflow binding 回填（可能 follow-up）。

---

## 8. 交付順序

1. O1 定案 → BE 大重構（schema → 引擎 retarget → handler → route → seed），TDD bite-sized。
2. FE retarget（Banner → round + AP 填寫畫面 + 建專案照新規格 + 建輪次選範本）。
3. e2e（compliance-manager-test）。

> 詳細 bite-sized 步驟見同資料夾 `round-flow-engine-implementation-plan.md`（BE）+ FE plan。
