# 稽核輪次流程引擎再整合 — BE Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 把 FR-026 BPMN 流程引擎從舊 AP（`ap_uid` + `assessment_plan_extensions`）retarget 到 v2 first-class `project_audit_rounds`，新增 `ap_authoring` 階段，round.status 改由 BPMN 推導。

**Architecture:** flow_template master → 建輪次時 clone snapshot → 綁 `project_audit_rounds.workflow_execution_uid`；stage_advance 以 `round_uid` 解 workflow context；既有 stage handler 改 wrap `audit_round_app_service`；round.status 由當前 BPMN UserTask 的 stage_object_code 經對照表推導（§4.1b）。

**Tech Stack:** Flask + SQLAlchemy + PostgreSQL（DDD），jedi-flow-engine（BPMN），jedi-oscal-v2（AP/AR），pytest。

**設計依據:** [round-flow-engine-reintegration-design.md](./round-flow-engine-reintegration-design.md)（§3 決策含 D5 stage 模型 / §4.1 canonical 4 階段鏈 + handler 對應 / §4.1b status 推導）

**canonical stage 鏈（D5）:** `planning → ap_authoring → audit → poam → End`（task_execution 收進 planning；review 暫不納）。

---

## 前置驗證（開工第一步）
- [ ] `git log --oneline -3` 確認在 `feature/oscal-refactor`（不對停下問 user，**不自己切 branch**）
- [ ] grep 確認 §核心 method 簽章未 drift：`audit_round_app_service.{create_round,launch_audit,start_auditing,finalize_audit,close_round}`、`stage_advance_service._resolve_workflow_context`、`workflow_template_snapshot_service.clone_master_as_snapshot`、`assessment_plan_app_service.get_ap_for_round`
- [ ] `grep -n 'GRC_4' common/code/grc_error_code.py | tail -15` 取最大序號續編
- [ ] 列出所有 interface 實作者（避免 Task 4 漏改）：
      `grep -rln 'IStageCompletionHandler\|IStagePreconditionCheck' app/` → 預期 handlers=6（Planning/Audit/Poam/TerminalClose/ReviewDecision + 待建 SubmitAp）、preconditions=4（all_tasks_assigned/all_tasks_completed/all_controls_verdict_filled/all_poam_closed）

---

## 任務總覽（依賴順序）

| # | 任務 | 依賴 | 風險 |
|---|------|------|------|
| 1 | schema：3 新欄位 + entity/mapper + `_to_dto` | — | 低 |
| 2 | error code + i18n key | — | 低 |
| 3 | stage_object seed（加 ap_authoring + 改 planning handler/precondition）| 1,2 | 中 |
| 4 | builtin flow_templates BPMN 重 seed（canonical 4 階段）| 3 | 中 |
| 5 | registry interface + stage_advance retarget（ap_uid→round_uid + status 推導）| 1 | **高** |
| 6 | handler 全改 wrap audit_round + 新 submit_ap + 拆 _safe_register | 5 | **高** |
| 7 | precondition retarget + 新 2 個 + count_reviewed_controls | 5 | 中 |
| 8 | create_round 綁 flow snapshot | 1,4 | 中 |
| 8b | 建專案順帶建第一輪（D6）+ serializer | 8 | 中 |
| 9 | round-scoped stage route + 退役舊 ap-scoped route | 6,7 | 中 |
| 10 | DI wiring | 6,7,8,9 | 中 |
| 11 | 整合測試 + boot 驗證 | all | — |

> **建專案順帶建第一輪（D6，新規格）**：舊專案資料已清空、**不做向後相容**；serializer 清掉舊 `Meta.unknown=EXCLUDE` / 舊欄位 + 帶 `resource_library_uid`/`flow_template_uid`，`start_project` 建 round 1 = Task 8b。FE 照新規格建畫面在 FE plan。

---

### Task 1: Schema — 3 新欄位 + entity/mapper + `_to_dto`

**Files:** Create `scripts/sql/2026-06-16-fr038-round-flow-binding.sql`；Modify `infra/module_frame/models/module_frame.py`、`infra/grc/model/project_audit_round.py`、`infra/grc/mapper/project_audit_round_mapper.py`、`domain/grc/entities/project_audit_round_entity.py`、`app/grc/service/audit_round_app_service.py`（`_to_dto`）

- [ ] **Step 1: migration SQL**（檔頭 `-- Date:`，收尾 `INSERT public.schema_migrations`）
```sql
-- Date: 2026-06-16  FR-038 round flow binding
ALTER TABLE compliance.module_frames
  ADD COLUMN IF NOT EXISTS audit_flow_template_uid VARCHAR(36);
COMMENT ON COLUMN compliance.module_frames.audit_flow_template_uid IS '預設稽核流程範本 uid（→ compliance.flow_templates.uid，soft ref）';
ALTER TABLE compliance.project_audit_rounds
  ADD COLUMN IF NOT EXISTS flow_template_snapshot_uid VARCHAR(36),
  ADD COLUMN IF NOT EXISTS workflow_execution_uid     VARCHAR(36);
COMMENT ON COLUMN compliance.project_audit_rounds.flow_template_snapshot_uid IS '本輪凍結流程範本 snapshot uid（soft ref）';
COMMENT ON COLUMN compliance.project_audit_rounds.workflow_execution_uid IS '本輪 main workflow_execution uid（BPMN 驅動 round 狀態）';
INSERT INTO public.schema_migrations (version) VALUES ('2026-06-16-fr038-round-flow-binding') ON CONFLICT DO NOTHING;
```

- [ ] **Step 2: 套 DEV**
Run: `set -a; source .env; set +a; PGPASSWORD=<查.env> psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev --single-transaction -v ON_ERROR_STOP=1 -f scripts/sql/2026-06-16-fr038-round-flow-binding.sql`
Expected: `ALTER TABLE` ×2 + `INSERT 0 1`

- [ ] **Step 3: ORM model + entity + mapper 加欄**（`project_audit_round.py` 在 `poam_id` 後加 `flow_template_snapshot_uid` / `workflow_execution_uid`（`String(36)` nullable）；`module_frame.py` 加 `audit_flow_template_uid`；entity + mapper 同步，mirror 既有 `ssp_id` 寫法）

- [ ] **Step 4: `_to_dto` 加兩欄**（`audit_round_app_service.py:119` dict 加 `"flow_template_snapshot_uid": e.flow_template_snapshot_uid, "workflow_execution_uid": e.workflow_execution_uid`）— 否則 Task 8 測試假失敗

- [ ] **Step 5: Commit**
```bash
git add scripts/sql/2026-06-16-fr038-round-flow-binding.sql infra/grc/model/project_audit_round.py infra/grc/mapper/project_audit_round_mapper.py domain/grc/entities/project_audit_round_entity.py infra/module_frame/models/module_frame.py app/grc/service/audit_round_app_service.py
git commit -m "feat(FR-038): round flow binding 欄位 + 資源庫預設流程範本欄 + _to_dto"
```

---

### Task 2: Error code + i18n key

**Files:** Modify `common/code/grc_error_code.py`、`config/translations/zh_Hant_TW/*.po`、`config/translations/en/*.po`（或對應 json，依專案 i18n 載入機制）

- [ ] **Step 1: error code（序號續編）**
```python
GRC_ROUND_NO_WORKFLOW_BINDING = ("GRC_404xxx", "稽核輪次尚未綁定流程")
GRC_AP_REVIEWED_CONTROLS_REQUIRED = ("GRC_412xxx", "請先選定本輪查核控制項")
GRC_ROUND_PREP_TASKS_NOT_DONE = ("GRC_412xxx", "本輪準備任務尚未全部完成")
```

- [ ] **Step 2: i18n key**（precondition reason key + stage 按鈕 label；zh + en 都加）
```
lang.flow_engine.precondition.round_prep_tasks_done = 本輪準備任務尚未全部完成（{count} 項待完成）
lang.flow_engine.precondition.ap_reviewed_controls_required = 請先選定本輪查核控制項
```

- [ ] **Step 3: 編譯 + commit**
Run: `pybabel compile -d config/translations`（若用 .po）
```bash
git add common/code/grc_error_code.py config/translations/
git commit -m "feat(FR-038): round flow error codes + precondition i18n"
```

---

### Task 3: stage_object seed（加 ap_authoring + 收斂 planning）

**Files:** Append `scripts/sql/2026-06-16-fr038-round-flow-binding.sql`

**stage_object UPDATE 矩陣（D5 收斂後的最終狀態）:**

| code | complete_handler_key | precondition_key | allowed_predecessors | allowed_successors | 動作 |
|---|---|---|---|---|---|
| `planning` | `launch_audit`（原 activate_project）| `round_prep_tasks_done`（原 all_tasks_*）| `[]` | `["ap_authoring"]` | UPDATE |
| `ap_authoring` | `submit_ap` | `ap_reviewed_controls_set` | `["planning"]` | `["audit"]` | INSERT（新）|
| `audit` | `confirm_audit` | `ar_all_verdicts_filled` | `["ap_authoring"]` | `["poam"]` | UPDATE predecessors |
| `poam` | `close_round` | `poam_all_closed` | `["audit"]` | `["audit"]` | 不動 |
| `task_execution` / `review` | — | — | — | — | 保留 table（builtin 不用；未來變體可復用）|

- [ ] **Step 1: seed SQL**
```sql
-- Date: 2026-06-16  FR-038 ap_authoring 階段 + planning 收斂
INSERT INTO compliance.stage_objects
  (uid, code, name_i18n, kind, route_pattern, complete_button_label_i18n,
   complete_handler_key, precondition_key, default_main_roles, can_start, can_end,
   allowed_predecessors, allowed_successors, is_builtin, sort)
VALUES
  (gen_random_uuid(), 'ap_authoring',
   '{"zh_Hant_TW":"稽核計畫填寫","en":"Assessment Plan Authoring"}', 'routed',
   '/project/projects/:projectUID/rounds/:roundUID/ap',
   '{"zh_Hant_TW":"完成填寫，開始稽核","en":"Submit & Start Auditing"}',
   'submit_ap', 'ap_reviewed_controls_set', '["auditor"]', false, false,
   '["planning"]', '["audit"]', true, 15)
ON CONFLICT (code) DO NOTHING;
-- planning：handler activate_project→launch_audit；precondition→round_prep_tasks_done；successor→ap_authoring
UPDATE compliance.stage_objects
   SET complete_handler_key='launch_audit', precondition_key='round_prep_tasks_done',
       allowed_successors='["ap_authoring"]'
 WHERE code='planning';
-- audit：predecessor 改 ap_authoring（不再從 task_execution/review 進）
UPDATE compliance.stage_objects
   SET allowed_predecessors='["ap_authoring","poam"]'
 WHERE code='audit';
```

- [ ] **Step 2: 套 DEV + 驗**
Run: `psql ... -c "SELECT code, complete_handler_key, precondition_key, allowed_successors FROM compliance.stage_objects WHERE code IN ('planning','ap_authoring','audit') ORDER BY sort"`
Expected: planning→launch_audit/round_prep_tasks_done/["ap_authoring"]；ap_authoring→submit_ap/ap_reviewed_controls_set；audit predecessors 含 ap_authoring

- [ ] **Step 3: Commit**
```bash
git add scripts/sql/2026-06-16-fr038-round-flow-binding.sql
git commit -m "feat(FR-038): ap_authoring stage seed + planning 收斂（task_execution 收進 planning）"
```

---

### Task 4: builtin flow_templates BPMN 重 seed（canonical 4 階段）

**Files:** Append `scripts/sql/2026-06-16-fr038-round-flow-binding.sql`

**目標串:** 4 個 builtin（或本期至少 `builtin-full-audit`）BPMN 改成 `Start → planning → ap_authoring → audit → (Gateway: has_findings) → poam → audit / End`。每個 UserTask extension 帶 `stage_object_code` + `main_role`。**先 `SELECT bpmn_xml ... WHERE name='builtin-full-audit'` 取既有當底**，在 planning 與 audit 間插 `ap_authoring` UserTask，移除 task_execution/review 節點。

- [ ] **Step 1: BPMN patch SQL**（UPDATE flow_templates.bpmn_xml；每 builtin 一段）
- [ ] **Step 2: 套 DEV + 拓撲驗證**
Run: `POST /flow-engine/flow-templates/validate`（或直接呼叫 `app/flow_engine/util/bpmn_topology_validator.py`）對每個 builtin
Expected: `violations == []`（特別 rule 3 predecessors/successors 對齊 Task 3 矩陣）
- [ ] **Step 3: Commit**
```bash
git add scripts/sql/2026-06-16-fr038-round-flow-binding.sql
git commit -m "feat(FR-038): builtin flow templates 改 canonical 4 階段（planning→ap_authoring→audit→poam）"
```

---

### Task 5: registry interface + stage_advance retarget ⚠️ 高風險

**Files:** Modify `domain/flow_engine/service/stage_completion_registry.py`、`app/flow_engine/service/stage_advance_service.py`；Test `test/test_stage_advance_round_context.py`

- [ ] **Step 1: failing test**（round_uid 解 context + status 推導）
```python
def test_resolve_workflow_context_by_round(stage_advance_service, seeded_round_with_workflow):
    ctx = stage_advance_service._resolve_workflow_context(seeded_round_with_workflow.uid)
    assert ctx["workflow_execution_uid"] == seeded_round_with_workflow.workflow_execution_uid
    assert "workflow_template_xml" in ctx

def test_round_status_derived_from_stage(stage_advance_service, seeded_round_at_ap_authoring):
    # 推進 ap_authoring 後 round.status 應為 audit_planning（§4.1b 對照）
    ...
```
- [ ] **Step 2: 跑測試確認 fail**
Run: `python -m pytest test/test_stage_advance_round_context.py -v` → FAIL
- [ ] **Step 3: interface 簽章改**（`IStageCompletionHandler.execute` / `IStagePreconditionCheck.check` 第一參數 `ap_uid: str`→`round_uid: str`）
- [ ] **Step 4: `_resolve_workflow_context` 改查 round**
```python
def _resolve_workflow_context(self, round_uid: str) -> dict:
    rnd = self._audit_round_domain_service.get_by_uid(round_uid)
    if rnd is None or not rnd.workflow_execution_uid:
        raise NotFound(GrcErrorCode.GRC_ROUND_NO_WORKFLOW_BINDING)
    wf = self._workflow_execution_domain_service.get_workflow_execution(
        WorkflowExecutionQueryEntity(uid=str(rnd.workflow_execution_uid)))
    if wf is None:
        raise NotFound(GrcErrorCode.GRC_ROUND_NO_WORKFLOW_BINDING)
    return {"round": rnd, "assessment_plan_id": rnd.assessment_plan_id,
            "workflow_execution_id": wf.id, "workflow_execution_uid": str(rnd.workflow_execution_uid),
            "workflow_status": str(wf.status) if wf.status is not None else None,
            "workflow_template_xml": wf.workflow_template_xml, "template_id": wf.template_id}
```
- [ ] **Step 5: `advance_stage`/`get_current_stage_info` 全檔 `ap_uid`→`round_uid`**（含 log 行、`handler.execute(round_uid=...)` 呼叫點[原 line 339]、`_run_precondition(..., round_uid, ...)`[原 110/300]）；DI 加 `audit_round_domain_service`、移除 `assessment_plan_extension_domain_service`
- [ ] **Step 6: status 所有權搬到 stage_advance**（§4.1b + ordering）
  - stage_advance 推進成功後，依「新的當前 stage_object_code」查 §4.1b map → `audit_round_domain_service.update(status=...)`。
  - `audit_round_app_service` 的 `launch_audit`/`start_auditing`/`finalize_audit`/`close_round`：**移除 `e.status = STATUS_*` 寫死**（audit_round_app_service.py:201/229/252/254/289/301），**保留** status guard（讀推進前值，仍正確）+ **保留業務寫入**（`ssp_id`/`assessment_plan_id`/`ar_result_id`/`poam_id`）。
  - `finalize_audit`：回傳 gateway 變數 `has_findings`（`res["not_met_count"]>0`）供 BPMN 路由（`has_findings→poam`=remediation / `→End`=closed）；不自己寫 status。
- [ ] **Step 7: 跑測試 pass**
Run: `python -m pytest test/test_stage_advance_round_context.py -v` → PASS
- [ ] **Step 8: Commit**
```bash
git add domain/flow_engine/service/stage_completion_registry.py app/flow_engine/service/stage_advance_service.py test/test_stage_advance_round_context.py
git commit -m "refactor(FR-038): stage 引擎 retarget round_uid + round.status BPMN 推導"
```

---

### Task 6: handler 全改 wrap audit_round + 新 submit_ap ⚠️ 高風險

**Files:** Modify `app/grc/service/oscal_stage_handlers.py`；Test `test/test_round_stage_handlers.py`

> 既有 6 handler 原 wrap `app/project/service/oscal_audit_service.py`（2A dark no-op）。本任務**全部建構子改注入 `audit_round_app_service`**、`execute(round_uid, …)`。`activate_project` handler 退役。

- [ ] **Step 1: failing test**
```python
def test_submit_ap_handler_calls_start_auditing(submit_ap_handler, mock_round_service, seeded_round):
    submit_ap_handler.execute(round_uid=seeded_round.uid, project_uid="p", user_id=1, curr_user="u", ctx={})
    mock_round_service.start_auditing.assert_called_once()
```
（autouse logger patch fixture，mirror `test_ssp_excel_import_app_service.py:46`）
- [ ] **Step 2: 跑測試確認 fail** → FAIL
- [ ] **Step 3: 寫 SubmitApHandler + 改 5 既有 handler**
```python
class SubmitApOnCompleteHandler(IStageCompletionHandler):
    def __init__(self, audit_round_app_service): self._rounds = audit_round_app_service
    @property
    def key(self): return "submit_ap"
    def execute(self, round_uid, project_uid, user_id, curr_user, ctx):
        return self._rounds.start_auditing(round_uid=round_uid, curr_user=curr_user, curr_user_id=user_id)
```
其餘：`launch_audit`→`self._rounds.launch_audit`、`confirm_audit`→`finalize_audit`、`close_round`→`close_round`、`terminal_close`→round 標 closed、`review_decision` 保留（不 wire）。全部 `__init__(self, audit_round_app_service)`、`execute(round_uid, ...)`。
- [ ] **Step 4: 跑測試 pass**
Run: `python -m pytest test/test_round_stage_handlers.py -v` → PASS
- [ ] **Step 5: Commit**
```bash
git add app/grc/service/oscal_stage_handlers.py test/test_round_stage_handlers.py
git commit -m "feat(FR-038): stage handler 全改 wrap audit_round + 新 submit_ap"
```

---

### Task 7: precondition retarget + 新 2 個 + count_reviewed_controls

**Files:** Modify `app/grc/service/oscal_stage_preconditions.py`、`app/grc/service/assessment_plan_app_service.py`；Test `test/test_round_stage_preconditions.py`

- [ ] **Step 1: `count_reviewed_controls` 薄 method**（`assessment_plan_app_service.py`，內部 `ApReviewedControlsRepoImpl.get_by_ap(ap_id)` 取長度；mirror 既有 `_ap_detail:111` 用法）
```python
def count_reviewed_controls(self, ap_id: int) -> int:
    return len(self._reviewed_repo.get_by_ap(ap_id) or [])
```
- [ ] **Step 2: failing test**（reviewed-controls 空→fail、非空→ok；prep tasks 未完成→fail）
- [ ] **Step 3: 跑測試確認 fail** → FAIL
- [ ] **Step 4: 既有 4 precondition 簽章 `ap_uid`→`round_uid`**（`all_tasks_assigned`/`all_tasks_completed`/`all_controls_verdict_filled`/`all_poam_closed`：先 `get_by_uid(round_uid)` 取 round → 用 `assessment_plan_id` / round 上下文查）
- [ ] **Step 5: 新 `ApReviewedControlsSetCheck` + `RoundPrepTasksDoneCheck`**
```python
class ApReviewedControlsSetCheck(IStagePreconditionCheck):
    def __init__(self, assessment_plan_app_service, audit_round_domain_service):
        self._ap, self._rounds = assessment_plan_app_service, audit_round_domain_service
    @property
    def key(self): return "ap_reviewed_controls_set"
    def check(self, round_uid, project_uid, ctx):
        rnd = self._rounds.get_by_uid(round_uid)
        if not rnd or not rnd.assessment_plan_id:
            return PreconditionResult.fail("lang.flow_engine.precondition.ap_reviewed_controls_required")
        n = self._ap.count_reviewed_controls(rnd.assessment_plan_id)
        return PreconditionResult.ok() if n > 0 else \
            PreconditionResult.fail("lang.flow_engine.precondition.ap_reviewed_controls_required", count=n)
# RoundPrepTasksDoneCheck：per-AO 收證據 job 全 JobStatus.COMPLETED 才放行
#   linkage：JobExecution 直接以 workflow_execution_id 過濾（workflow_execution_control_mapping
#   無 job_execution_id FK，不可 join 它）。⚠️ 收證據 job 由 _generate_prep_jobs 在「專案成立」建、
#   為專案層級（非 round 綁定）→ scoping「哪些 job 算本輪 prep」於實作時確認：
#   暫定查專案所有 prep WE 的 job 全 COMPLETED；無 prep job（template_uid 未設）→ ok 放行（不硬擋）。
```
- [ ] **Step 6: 跑測試 pass** → PASS
- [ ] **Step 7: Commit**
```bash
git add app/grc/service/oscal_stage_preconditions.py app/grc/service/assessment_plan_app_service.py test/test_round_stage_preconditions.py
git commit -m "feat(FR-038): precondition retarget round + reviewed_controls/prep_tasks 守門 + count_reviewed_controls"
```

---

### Task 8: create_round 綁 flow_template snapshot

**Files:** Modify `app/grc/service/audit_round_app_service.py`；Test `test/test_create_round_flow_binding.py`

**範本來源（O1 a+b）:** 入參 `flow_template_uid`（選填）> 前一輪 `flow_template_snapshot_uid` 對應 master > 資源庫 `module_frames.audit_flow_template_uid`。

- [ ] **Step 1: failing test**
```python
def test_create_round_binds_flow_snapshot(round_service, seeded_project_with_resource_library):
    r = round_service.create_round(project_uid=..., name="R1", round_type="initial", curr_user="u", curr_user_id=1)
    assert r["flow_template_snapshot_uid"] is not None
    assert r["workflow_execution_uid"] is not None
```
- [ ] **Step 2: 跑測試確認 fail** → FAIL
- [ ] **Step 3: 改 create_round**（解析範本來源 → `workflow_template_snapshot_service.clone_master_as_snapshot`（mirror `oscal_project_service.py:196`）→ `workflow_execution_service.start_workflow_execution` → 回填兩欄；DI 注入 snapshot service + workflow_execution service + flow_template_domain_service + project_extension_domain_service[取資源庫]）
- [ ] **Step 4: 跑測試 pass** → PASS
- [ ] **Step 5: Commit**
```bash
git add app/grc/service/audit_round_app_service.py test/test_create_round_flow_binding.py
git commit -m "feat(FR-038): create_round clone flow snapshot + 綁 round（O1 a+b 來源）"
```

---

### Task 8b: 建專案順帶建第一輪（D6）+ serializer

**Files:** Modify `api/project/serializers/project.py`（`OscalProjectStartRequest` 加回 `flow_template_uid`）、`app/project/service/project_start_app_service.py`（`start_project` 建 round 1）；Test `test/test_project_start_creates_first_round.py`

- [ ] **Step 1: serializer 照新規格清理**（`OscalProjectStartRequest`：移除舊 `Meta.unknown=EXCLUDE` + 舊欄位相容註解；`resource_library_uid` required + 加 `flow_template_uid = fields.String(required=False, allow_none=True, load_default=None)`）
- [ ] **Step 2: failing test**（start_project 後該專案有一筆 round_no=1 / round_type=initial / flow_template_snapshot_uid 非空）
```python
def test_start_project_creates_initial_round_with_flow(project_start_service, seeded_resource_library):
    res = project_start_service.start_project(name="P", resource_library_uid=..., flow_template_uid=<uid>, curr_user="u", curr_user_uid=..., curr_user_id=1)
    rounds = audit_round_domain_service.list_by_project(res_project_id)
    assert len(rounds) == 1 and rounds[0].round_type == "initial"
    assert rounds[0].flow_template_snapshot_uid is not None
```
- [ ] **Step 3: 跑測試確認 fail** → FAIL
- [ ] **Step 4: start_project 建 round 1**（clone 資源庫 + project/extension/participants/prep jobs 後，呼叫 `audit_round_app_service.create_round(project_uid, name=「第 1 輪」, round_type='initial', flow_template_uid=…, curr_user, curr_user_id)`；未給 flow_template_uid → create_round 內 O1 fallback；DI 注入 `audit_round_app_service`）
- [ ] **Step 5: 跑測試 pass** → PASS
- [ ] **Step 6: Commit**
```bash
git add api/project/serializers/project.py app/project/service/project_start_app_service.py test/test_project_start_creates_first_round.py
git commit -m "feat(FR-038): 建專案順帶建第一輪 initial round + 綁所選流程範本（D6）"
```

---

### Task 9: round-scoped stage route + 退役舊 route（ap-scoped + 4 直接轉換）

**Files:** Modify `api/project/routes/audit_round_route.py`、`api/project/__init__.py`、`api/flow_engine/__init__.py`；Remove `api/flow_engine/routes/stage_advance_route.py`；Test `test/test_audit_round_stage_route.py`

- [ ] **Step 1: 加 round stage route**（`GET/POST /project/<project_uid>/audit-round/<round_uid>/stage/{info,advance}` → `stage_advance_service.get_current_stage_info/advance_stage` 傳 round_uid）
- [ ] **Step 2: 退役舊 ap-scoped route**（`git rm api/flow_engine/routes/stage_advance_route.py` + 移除 `api/flow_engine/__init__.py:30-31,79` 的 `StageInfoResource`/`StageAdvanceResource` 註冊）— 否則 boot 出指向舊簽章的壞端點
- [ ] **Step 3: 退役 4 個直接轉換 route**（O2 已定退役）：disable/移除 `audit_round_route.py` 的 `AuditRoundLaunchAuditRoute`(:85) / `AuditRoundStartAuditingRoute`(:97) / `AuditRoundFinalizeRoute`(:265) / `AuditRoundCloseRoute`(:109) — big-bang 後轉換一律走 stage/advance；保留直接 route 會繞過 status 推導 + 撞 status guard。`create_round` / `list_rounds` / `get_round` 等非轉換 route 保留
- [ ] **Step 4: smoke test**
Run: `python -m pytest test/test_audit_round_stage_route.py -v` → PASS
- [ ] **Step 5: boot 確認直接 route 已下線**（`grep -rn 'AuditRoundLaunchAuditRoute\|launch-audit' api/project/__init__.py` 應無註冊）
- [ ] **Step 6: Commit**
```bash
git add api/project/routes/audit_round_route.py api/project/__init__.py api/flow_engine/__init__.py test/test_audit_round_stage_route.py
git rm api/flow_engine/routes/stage_advance_route.py
git commit -m "feat(FR-038): round stage route + 退役舊 ap-scoped + 4 直接轉換 route（O2 收斂 stage/advance）"
```

---

### Task 10: DI wiring

**Files:** Modify `di_containers/grc/grc_containers.py`、`di_containers/flow_engine/*`

- [ ] **Step 1: handler/precondition DI**（既有 6 handler provider 改注入 `audit_round_app_service`；**拆掉 `_safe_register` try/except dark-skip**（grc_containers.py:~475-495）改正常 `register_handler`；新增 `SubmitApOnCompleteHandler` / `ApReviewedControlsSetCheck` / `RoundPrepTasksDoneCheck` 註冊；4 既有 precondition provider 依賴改 round）
- [ ] **Step 2: stage_advance DI**（注入 `audit_round_domain_service`、移除 `assessment_plan_extension_domain_service`）
- [ ] **Step 3: boot 驗證**
Run: `python -c "from core.app_factory import create_app; create_app()" 2>&1 | tail -20`（或 `python main_app.py` 起一次）
Expected: 無 DI / NameError / configure_mappers error；registry 有 `submit_ap`（grep log 或加臨時 assert）
- [ ] **Step 4: Commit**
```bash
git add di_containers/
git commit -m "feat(FR-038): DI wiring round stage handlers/preconditions + 拆 2A dark-skip"
```

---

### Task 11: 整合測試 + boot 驗證

- [ ] **Step 1: flow_engine + grc 全測**
Run: `python -m pytest test/ -k "stage or round or flow" -v`
Expected: PASS；無新回歸（baseline diff，[[feedback_baseline_worktree_regression_diff]]）
- [ ] **Step 2: e2e 手測 checklist（交 user）**
  - 建專案（選資源庫）→ 建輪次（範本帶資源庫預設）→ Banner 顯示 planning
  - planning：per-AO 收證據 job 未全完成 → 推進按鈕 disable（`round_prep_tasks_done` tooltip）
  - 全完成 → 推進 → snapshot SSP + AP 草稿 → 進 ap_authoring（round.status=audit_planning）
  - ap_authoring：未選 reviewed-controls → disable；選了 → 推進 → 建 AR 矩陣 → 進 audit（auditing）
  - audit → poam → close → round closed；`public.system_logs` 有 605x 推進紀錄
- [ ] **Step 3: 不 commit 收尾文件**（changelog/SUMMARY 等 user verify pass + 下令才寫，per CLAUDE.md「收尾必須等 user 下命令」）

---

## Plan Review
寫完 dispatch plan-document-reviewer（spec=round-flow-engine-reintegration-design.md）；❌ 修正重審；✅ handoff 執行。
