# Spec 3：AP 套用流程

> **狀態**：v2 — brainstorm 收斂後完整版，待 review
> **整體脈絡**：`../README.md`
> **依賴**：Spec 1（已 ship）/ Spec 2（已 ship）

---

## 一、目標

建立新 AP 時 user 可挑選流程範本；AP 建立同時 snapshot clone 該範本 BPMN 到該 AP 專屬的 `workflow_execution`。後續範本被改 / 刪不影響進行中 AP；多 AP 之間流程獨立運作；未來可開放「中途調整 AP 自己的流程」也不會污染兄弟 AP 或 master。

---

## 二、Brainstorm 結論（v1 → v2 變更）

| Q | 主題 | 決議 |
|---|---|---|
| Q1 | dropdown 是否按合規資源庫過濾 | **純開放** — 全部 active 範本都列；YAGNI，不加 `compatible_frameworks` 欄位 |
| Q2 | 預覽 Dialog 呈現 | **跟 Spec 1 對齊** — reuse 既有 `FlowTemplatePreviewDialog.vue`（唯讀 BPMN + 階段清單並列） |
| Q3 | launch_new_round 預設值 | **Derived from latest AP** — 不加 project 層級欄位，dropdown default 取「最新一輪 AP 的 master template uid」 |
| Q4 | Snapshot 儲存方式 | **Reuse Spec 2** — 直接用 `WorkflowTemplateSnapshotService.clone_master_as_snapshot`，零 migration |
| Q5 | 第一輪 AP 入口 | **保留雙入口** — ProjectCreateView 跟 ProjectApListView 兩處都加 dropdown；Spec 2 lifecycle 假設不動 |
| API | flow_template_uid 必填 vs optional | **必填** — 一次拔乾淨 Spec 2 hardcode default 邏輯，不留技術債 |

---

## 三、Spec 2 補洞：launch_new_round 沒做 main workflow snapshot

審核 Spec 2 落地時發現的重要事實：

- `start_oscal_project:487` 已呼叫 `_bind_main_workflow_to_ap` → 第一輪 AP 有 snapshot + `assessment_plan_extensions` + main `workflow_execution`
- `launch_new_round` 從未呼叫該 helper → 後續輪 AP **沒**綁 main workflow，Banner 找不到 stage → 推進按鈕永遠 disabled

→ Spec 3 不只是「加 dropdown + 把 hardcode 換成 parameter」，**還要把 main workflow snapshot 整段 helper 邏輯接上 `launch_new_round`**。

這同時是 Phase E `launchProjectViaUi` / `launchAuditViaUi` 系列 fixture 撞 disabled button 的潛在根因（fixture 是新一輪 flow 上跑的）。

---

## 四、Snapshot 三層獨立性保證

| 場景 | 保證機制 |
|---|---|
| 兩個 AP 選同一個 master | 各自 clone 一份新的 snapshot row（不同 uid）；engine `template_id` 用各自 snapshot.id |
| Admin 改 master XML | engine 讀的是 snapshot row，現有 AP 的 snapshot.xml 不動 |
| Admin 軟刪 master | 現有 AP snapshot row 仍存在；engine 仍能跑；只影響後續新 AP 不能再選此 master |
| 未來「中途調整 AP 流程」 | snapshot row 是 per-AP 私有資料 → 直接 mutate 該 AP 的 snapshot.xml 不污染 master 也不污染兄弟 AP |

---

## 五、需求摘要

來源：requirement.md §「想法（粗略）」§3

1. **建專案**時 user 選流程範本，帶到自動建立的第一輪 AP
2. **稽核計畫頁建新一輪 AP** 時 user 選流程範本（default 帶最新一輪 master）
3. AP 建立同時 snapshot clone 範本 BPMN 到該 AP 專屬 `workflow_execution`
4. 後續範本被修改 / 刪除不影響進行中 AP（snapshot 不可變）
5. 多 AP 之間流程互不影響（snapshot per-AP）

---

## 六、範圍

### In scope
- BE `start_oscal_project` 簽章加 `flow_template_uid: str (required)`
- BE `launch_new_round` 簽章加 `flow_template_uid: str (required)` + 補做 main workflow snapshot
- BE `_bind_main_workflow_to_ap` 改為接 `flow_template_uid` 必填（拔 default lookup）
- BE 拔除 `FlowTemplateDomainService.DEFAULT_MAIN_WORKFLOW_TEMPLATE_NAME` / `get_default_main_workflow_template()`
- BE AP 詳情 API 補回 `flow_template` enrichment（snapshot uid / master uid / master name）
- BE error code 整理：刪 `GRC_FLOW_MAIN_TEMPLATE_NOT_FOUND`（412022）；reuse 既有 `GRC_FLOW_TEMPLATE_NOT_FOUND`（404027）
- FE `ProjectCreateView` 加流程範本 dropdown + 預覽 Dialog
- FE `ProjectApListView`「新增稽核計畫」Dialog 加流程範本 dropdown + default 帶值邏輯
- FE i18n（zh-tw / en）

### Out of scope
- 範本管理 UI（→ Spec 1，已 ship）
- Stage 推進 runtime（→ Spec 2，已 ship）
- 中途調整 AP 流程 UI（未來 spec）
- Overview shell 化重構（Banner 已在 Spec 2 掛好）
- Stage progress bar 互動跳轉（v1 純預覽，Spec 2 已處理）
- 範本按合規資源庫過濾（Q1 YAGNI）
- 跨 tenant 範本共享（v1 純 tenant 隔離；RLS 自動處理）

---

## 七、AP 建立入口（雙入口）

| 入口 | URL / View | 動作 | API |
|---|---|---|---|
| 建專案（自動帶第一輪 AP） | `/project/projects/new` → `ProjectCreateView` | 建立專案 + 自動建第一輪 AP | `POST /oscal/projects/start` → `OscalProjectService.start_oscal_project` |
| 建後續輪 AP | `/project/projects/:id` → `ProjectApListView`「新增稽核計畫」Dialog | 建新 AP | `POST /grc/project/<uid>/launch-new-round` → `OscalProjectService.launch_new_round` |

兩處共用同一個「流程範本 dropdown + 📋 詳細選擇」UX，預覽元件 reuse Spec 1 `FlowTemplatePreviewDialog.vue`。

---

## 八、架構：data flow

```
建專案 / 建後續輪 AP（任一入口）
  ↓ (使用者選範本 uid)
flow_template_uid (UUID, required) ──┐
                                     ▼
start_oscal_project / launch_new_round
                                     │
                                     ▼
_bind_main_workflow_to_ap(ap, project_uid, flow_template_uid, curr_user)
                                     │
                ┌────────────────────┴────────────────────┐
                ▼                                         ▼
  FlowTemplateDomainService                  GRC_FLOW_TEMPLATE_NOT_FOUND
    .get_by_uid(flow_template_uid)           （uid 不存在 / is_active=false）
                │
                ▼
  master (FlowTemplateEntity)
                │
                ▼
  WorkflowTemplateSnapshotService.clone_master_as_snapshot(
    master_uid=master.uid,
    master_name=master.name,
    xml=master.bpmn_xml,
    description=master.description,
    curr_user=curr_user,
  )
                │
                ▼
  snapshot (engine `public.workflow_templates`，per-AP immutable）
                │
                ▼
  workflow_execution_service.start_workflow_execution(template_id=snapshot.id, ...)
                │
                ▼
  main_wf (engine `workflow_executions`)
                │
                ▼
  ap_extension_domain_service.upsert(
    assessment_plan_id=ap.id,
    workflow_execution_uid=main_wf.uid,
    flow_template_snapshot_uid=snapshot.uid,
    curr_user=curr_user,
  )
                │
                ▼
  assessment_plan_extensions row written
                │
                ▼
  AP 詳情 API enrichment ◀── 從 ext 反查 snapshot → 反查 master → 三段組合回傳
```

---

## 九、BE 改動細節

### 9.1 `OscalProjectService.start_oscal_project`

```python
@transaction
def start_oscal_project(
    self,
    project_name,
    ...既有參數...,
    flow_template_uid: str,         # 新增，必填
    curr_user: str = None,
    locale: str = None,
) -> Project:
    ...既有 21 個 step 不動...

    # Step 22 改造：把 flow_template_uid 顯式傳入
    self._bind_main_workflow_to_ap(
        ap=ap,
        project_uid=str(project.uid),
        flow_template_uid=flow_template_uid,
        curr_user=curr_user,
    )
    return project
```

### 9.2 `OscalProjectService.launch_new_round`

```python
@transaction
def launch_new_round(
    self,
    project_uid: str,
    user_id: int,
    title: str,
    flow_template_uid: str,         # 新增，必填
    start_date=None, end_date=None,
    curr_user: str = None,
    locale: str = None,
) -> dict:
    ...既有 step 1–9 不動（含 sub-workflow per-task clone，跟 main 不衝突）...

    # 新增 step 10：補做 main workflow snapshot（Spec 2 的 gap）
    self._bind_main_workflow_to_ap(
        ap=ap,
        project_uid=str(project_entity.uid),
        flow_template_uid=flow_template_uid,
        curr_user=curr_user,
    )

    return {...既有...}
```

### 9.3 `_bind_main_workflow_to_ap` 改造

```python
def _bind_main_workflow_to_ap(
    self,
    ap,
    project_uid: str,
    flow_template_uid: str,         # 新增，必填
    curr_user: str,
) -> None:
    """Spec 3 — 接 caller 指定的 flow_template_uid，clone 一份 per-AP 凍結 snapshot
    到 engine 表，啟動 workflow_execution，寫 assessment_plan_extensions。

    Caller 已開 @transaction scope，本 helper 不再加。
    """
    if (self._flow_template_domain_service is None
            or self._ap_extension_domain_service is None
            or self._workflow_template_snapshot_service is None):
        raise PreconditionFailedError(GrcErrorCode.GRC_FLOW_TEMPLATE_NOT_FOUND)

    master = self._flow_template_domain_service.get_by_uid(flow_template_uid)
    if master is None or not master.is_active:
        raise NotFound(GrcErrorCode.GRC_FLOW_TEMPLATE_NOT_FOUND)

    snapshot = self._workflow_template_snapshot_service.clone_master_as_snapshot(
        master_uid=str(master.uid),
        master_name=master.name,
        xml=master.bpmn_xml,
        description=master.description,
        curr_user=curr_user,
    )
    main_wf = self.workflow_execution_service.start_workflow_execution(
        template_id=snapshot.id,
        name=f"AP-{ap.uid}-main",
        params={"ap_uid": str(ap.uid), "project_uid": project_uid},
        created_user=curr_user,
        description=f"AP main workflow for {ap.title}",
    )
    self._ap_extension_domain_service.upsert(
        assessment_plan_id=ap.id,
        workflow_execution_uid=str(main_wf.uid),
        flow_template_snapshot_uid=str(snapshot.uid),
        curr_user=curr_user,
    )
```

### 9.4 拔除 Spec 2 過渡 hardcode

`domain/flow_engine/service/flow_template_domain_service.py`：

```python
# 全部移除
# DEFAULT_MAIN_WORKFLOW_TEMPLATE_NAME = "完整稽核流程"
# def get_default_main_workflow_template(self) -> Optional[FlowTemplateEntity]: ...
```

`IFlowTemplateRepo.get_builtin_by_name()` — grep 確認無其他 caller 即刪。

### 9.5 Route schema 改動

| Endpoint | Request schema 改動 |
|---|---|
| `POST /oscal/projects/start` | 加 `flow_template_uid: fields.UUID(required=True)` |
| `POST /grc/project/<uid>/launch-new-round` | 同上 |

### 9.6 AP enrichment（list + detail）

**兩個 endpoint 都要回 `flow_template` enrichment**：

- `GET /grc/project/<uid>/aps`（列表）— FE default 帶值靠這個，必須有
- `GET /grc/project/<uid>/aps/<apUid>`（詳情）— FE 顯示「目前用的範本」靠這個

Response 加欄位：

```json
{
  ...既有欄位...,
  "flow_template": {
    "snapshot_uid":     "engine-snapshot-uid",
    "master_uid":       "compliance-master-uid",
    "master_name":      "完整稽核流程",
    "is_master_active": true
  }
}
```

Enrichment path（app service 層）：
1. 取 `assessment_plan_extensions` by `assessment_plan_id` → `flow_template_snapshot_uid`
2. 用 snapshot uid 取 engine `workflow_templates` row → `source_template_uid`
3. 用 master uid 取 `compliance.flow_templates` row → `name` + `is_active`

**Enrichment 行為（lenient）**：若任一段缺失（舊資料 ext 不存在 / master 已硬刪等 edge case），`flow_template` 回 `null`，**不擋 API 回應**。`is_master_active=false` 代表 master 已軟刪但 snapshot 仍正常運作（這 AP 還能跑完現有 lifecycle）。

**對比 — 建立路徑（strict，見 §十一）**：建 AP 時 `master.is_active=False` 直接 404 拒絕。語意：「進行中 AP 容忍 master 被刪、但新 AP 不能選已刪的 master」。

**列表 enrichment 效能考量**：列表 enrichment 會 N+1 — N AP × 3 段 query。實作要用 batch query：一次取所有 ext rows → 一次 batch 取 snapshot rows → 一次 batch 取 master rows，三段 dict join 完拼 response。

### 9.6.1 取代既有 `workflow_template_uid` 欄位（注意：清掉 §906）

`launch_new_round` 既有程式碼（line 906–924）對 task 動態 setattr `task.workflow_template_uid` — 那是 **sub-workflow per-task** 的 template uid，跟本 spec 加的 main workflow `flow_template` 是不同層級。**保留**，不衝突。但 §9.6 enrichment 必須清楚標 `flow_template` 是 main workflow（AP 級別），跟 task-level sub-workflow 分開不混淆。

### 9.7 Error code

| Code | 動作 |
|---|---|
| `GRC_FLOW_TEMPLATE_NOT_FOUND` (404027) | **reuse**，原本 Spec 1 已有 |
| `GRC_FLOW_MAIN_TEMPLATE_NOT_FOUND` (412022) | **刪除**（default lookup 拔了就用不到） |
| `GRC_AR_DATA_MISSING` (500001) | **順手 fix**：改 `412024`（Spec 2 carry-forward M1） |

---

## 十、FE 改動細節

### 10.1 `ProjectCreateView.vue`

加在「基本資訊」step（建議 Step 0 末尾）：

```
基本資訊
├── 專案名稱 *
├── 描述
├── 合規資源庫 *
├── 第一輪稽核計畫流程範本 *    ← 新增
│   ├── <Dropdown
│   │     v-model="form.flowTemplateUid"
│   │     :options="flowTemplates"
│   │     optionLabel="name"
│   │     optionValue="uid"
│   │     :loading="loading"
│   │     :class="{'p-invalid': v$.flowTemplateUid.$error}" />
│   └── <Button text size="small" @click="openPreview">📋 詳細選擇</Button>
└── ...
```

### 10.2 `ProjectApListView.vue` 新增稽核計畫 Dialog

```
新增稽核計畫
├── 計畫名稱 *
├── 起訖日期
├── 流程範本 *                  ← 新增
│   ├── <Dropdown ... v-model="form.flowTemplateUid" />
│   └── <Button text>📋 詳細選擇</Button>
└── ...
```

Default 帶值（依賴 §9.6 — AP **列表** API 必須含 `flow_template` enrichment）：

```js
const defaultFlowTemplateUid = computed(() => {
  // apList 排序：最新一輪在 [0]（既有排序慣例，新 spec 不改）
  // 依賴 GET /grc/project/<uid>/aps 回 flow_template enrichment
  const latest = props.apList?.[0]
  return latest?.flow_template?.master_uid ?? null
})

watch(() => props.visible, (v) => {
  if (v) form.value.flowTemplateUid = defaultFlowTemplateUid.value
})
```

**注意：** 若 §9.6 列表 enrichment 未實作，dropdown default 會永遠 null，user 每次都要重選。**列表 enrichment 是 dropdown UX 的硬依賴**，不能延後到 v2 補。

### 10.3 預覽 Dialog reuse

```vue
<FlowTemplatePreviewDialog
  v-model:visible="previewVisible"
  :template-uid="form.flowTemplateUid"
/>
```

Spec 1 已 ship `src/views/flow-template/components/FlowTemplatePreviewDialog.vue` —
input 是 master uid，內部自己拉 detail + render BPMN + 列階段清單。

### 10.4 Service / API client

`src/service/FlowTemplateService.js`（Spec 1 已有 list / detail），新增方法可直接 reuse。

### 10.5 i18n

zh-tw：

```yaml
project:
  flowTemplate:
    label: 流程範本
    firstRoundLabel: 第一輪稽核計畫流程範本
    placeholder: 請選擇流程範本
    detailBtn: 詳細選擇
    requiredError: 請選擇流程範本
    inheritedHint: 預設沿用上一輪稽核計畫的流程範本
```

en 對應 key 一併補。

---

## 十一、驗證與 Error handling

| 情境 | 行為 |
|---|---|
| 缺 `flow_template_uid` | 400 by webargs schema validation |
| `flow_template_uid` 非 UUID 格式 | 400 by webargs schema validation |
| UUID 形式正確但查無此 master（不存在 / 軟刪） | 404 `GRC_FLOW_TEMPLATE_NOT_FOUND` |
| `master.is_active = False` | 404 同上 |
| Cross-tenant master | RLS 自動 filter → 404 |
| Engine snapshot 建立失敗 / start_workflow_execution 失敗 | 整個 `@transaction` rollback；不留半成品 AP |

---

## 十二、測試策略

### 12.1 BE pytest

| 案例 | 範圍 | 對應 |
|---|---|---|
| `start_oscal_project` 帶 valid uid → 第一輪 AP 有 snapshot + ext | integration | §9.1 |
| `start_oscal_project` 帶 invalid uid → 404 | unit | §11 |
| `start_oscal_project` 缺 uid → 400 | unit | §11 |
| `launch_new_round` 帶 valid uid → 新輪 AP 有獨立 snapshot + ext（與第一輪不同 snapshot uid） | integration | §9.2 + §三 |
| Snapshot 隔離：建 AP 後改 master XML → AP snapshot.xml 不動 | integration | §四 |
| AP 詳情 API 回 `flow_template` enrichment | integration | §9.6 |
| AP 詳情 enrichment 在 master 已軟刪時 `is_master_active=false` | integration | §9.6 |

順手補 Spec 2 carry-forward test gap（同 commit 或鄰近 commit）：

- `WorkflowTemplateSnapshotService` — clone 行為 unit test
- `StageAdvanceService` — stage 推進 path unit test
- `oscal_stage_preconditions` — precondition function 個別 unit test
- `oscal_stage_handlers` — handler 個別 unit test
- `AssessmentPlanExtensionRepoImpl` — upsert / 查詢 unit test

### 12.2 FE E2E（compliance-manager-test）

留到 Spec 3 BE+FE 都 ship 後 — 跟 Phase E 4 fixture bug 一起重做：

- 修 `ProjectCreatePage.addFourParticipantsWithRoles`（適配 wizard step 數變化 + 新增範本 dropdown）
- 修 `launchProjectViaUi` / `launchAuditViaUi` / `launchRemediationViaUi`
- 重寫 Spec 2 Banner 3 個 scenario（adapt 新範本選擇 UI）

### 12.3 Manual smoke（必過項）

```
□ 建專案選範本 A「完整稽核流程」→ 第一輪 AP1
  └ Banner 顯示 stage = 規劃 → 推進按鈕作用 → status preparing→active
  └ 連續推完 4 個 stage 到 closed
□ 同專案 launch_new_round 選範本 B「自我評估稽核」（少 poam）
  └ AP2 snapshot uid 跟 AP1 不同
  └ Banner stage 組合對應 B 的 BPMN（規劃→執行→稽核→closed，跳 poam）
  └ 推到 audit 完成後直接 closed
□ DB 驗：
  - assessment_plan_extensions 兩列，flow_template_snapshot_uid 不同
  - public.workflow_templates 兩列 snapshot row，source_template_uid 對應 A/B master
  - 兩個 workflow_execution 互不影響
□ Smoke 通不過 → 根因落 Spec 2 stage_advance_service / oscal_stage_handlers
  → 此時補 Spec 2 carry-forward 5 service test gap 是必做（不再延後）
```

---

## 十三、落地順序

| 里程碑 | 範圍 | 提交策略 |
|---|---|---|
| M0 | **Pre-flight verify**：grep `get_default_main_workflow_template` / `get_builtin_by_name` / `DEFAULT_MAIN_WORKFLOW_TEMPLATE_NAME` 所有 caller；確認除了 `_bind_main_workflow_to_ap` 外無其他 reference | 無 commit，純檢查 |
| M1 | BE error code 整理（刪 412022、fix 500001→412024） | 獨立 commit |
| M2 | BE AP **列表 + 詳情** API 補 `flow_template` enrichment（純加欄位，不破 caller） | 獨立 commit |
| M3 | **BE Spec 2 carry-forward 5 service test gap 補測試**（move ahead — 防 M4 改動踩到沒測過的 service） | 獨立 commit |
| M4 | BE `_bind_main_workflow_to_ap` 接 `flow_template_uid`；`start_oscal_project` + `launch_new_round` 加參數；拔 default lookup；加 `launch_new_round` 呼叫 helper | 獨立 commit |
| M5 | FE `ProjectCreateView` dropdown + 預覽 Dialog + i18n | 獨立 commit |
| M6 | FE `ProjectApListView` Dialog dropdown + default 帶值 + i18n | 獨立 commit |
| M7 | **Manual smoke**（必過項，§12.3）+ 收尾 changelog | 必過 |
| M8 | （事後）Phase E 4 fixture bug 修補 + 3 個 Banner BDD scenario 重寫 | 跨 repo |

**M3/M4 順序理由**：M4 改 `_bind_main_workflow_to_ap` + 拔 `get_default_main_workflow_template` 會影響 `StageAdvanceService` / `WorkflowTemplateSnapshotService` 等 carry-forward 服務。先 M3 補測試，再做 M4 改動，M4 出 regression 時 test 立刻 fail，不會拖到 M7 smoke 才發現。

---

## 十四、對外依賴

| 依賴 | 提供方 | 狀態 |
|---|---|---|
| `GET /flow-engine/flow-templates`（列表） | Spec 1 | ✅ 已 ship |
| `GET /flow-engine/flow-templates/<uid>`（detail + BPMN XML） | Spec 1 | ✅ 已 ship |
| `FlowTemplatePreviewDialog.vue` | Spec 1 FE | ✅ 已 ship |
| `WorkflowTemplateSnapshotService.clone_master_as_snapshot` | Spec 2 | ✅ 已 ship |
| `_bind_main_workflow_to_ap` helper（既有 backbone） | Spec 2 | ✅ 已 ship（Spec 3 改造參數） |
| `assessment_plan_extensions` table + repo | Spec 2 | ✅ 已 ship |

---

## 十五、開放問題（v2 已 resolved）

v1 列的 5 個 open questions 全部 resolved（見 §二）。

v2 沒有新增 open question — 進入 Phase 3 implementation-plan.md 階段。

---

## 十六、Implementation Reality / Reconciliation（v3 加，summary 收尾時）

Phase 5 實作 + M7 smoke + Phase 6 review 後，下列為實作偏離 v2.1 plan 的條目。記錄目的：給未來讀者保留決策軌跡，不是補救。

### 16.1 Plan 偏差

| # | Plan 寫的 | 實作的 | 原因 |
|---|---|---|---|
| 1 | M2 plan 寫「修改 jedi-flow-engine 加 `get_many_by_uids`」 | 不動套件，用 enricher 內 `_engine_get_many_by_uids` adapter 把既有 `get_workflow_templates_by_uids` list 結果轉 dict | jedi-flow-engine 0.0.27 已有此 method（返回 list），M2 implementer 評估後 reuse |
| 2 | M2.4 plan 範例 AP detail GET endpoint | `AssessmentPlanDetailResource` **只有 PUT/DELETE 沒 GET** | grep 後發現 FE 也未呼叫 detail GET；enricher 改在 PUT response 補；FE 未來要 detail GET 時再加 |
| 3 | M2.4 plan 寫 integration test 用 `app_client` + DB fixture | test/ 目錄無 DB fixture infra | 改 service-level unit test 用 mock enricher 驗 service 把 enrichment 塞到 dto；integration coverage deferred |
| 4 | M4.2 plan 寫的 schema class 名 `StartProjectRequestSchema` | 實際是 `OscalProjectStartRequest`（`StartProjectRequest` 是 module_frame 另一 route 用） | Plan 寫錯 class 名 |
| 5 | M4.3 plan 寫 `launch_new_round` 簽章 `flow_template_uid: str`（無 default required positional） | 實際 `flow_template_uid: str = ""` + 內部 raise `BadRequestError(GRC_FLOW_TEMPLATE_UID_REQUIRED)` | 與既有 schema integration test pattern 對齊；schema 已 enforce required，service 層的 default 是 defensive guard（後續 PR-TD2 計畫改回必填） |
| 6 | M0 Step 6 `poetry show jedi-flow-engine` 預期輸出 location path | 實際只輸出 version 0.0.27 | 用 `grep pyproject.toml` 直接驗 path mode 取代 |
| 7 | Plan 假設 dropdown 落點在 `ProjectBasicInfoForm.vue` | 實際加在 `ProjectCreateView.vue` 內 inline 跟既有 profile dropdown 同 slot 層 | M0 Step 8 grep 後決定（ProjectBasicInfoForm 是 generic component） |
| 8 | Plan 範例 FE service method `FlowTemplateService.getList({ is_active: true })` | 實際既有 service `listFlowTemplates({page, size, name, is_builtin})` 回 `{items, total, page, size}` | M5 implementer 對齊既有 API；BE service 已固定 `is_active=True` filter，FE 不用傳 |
| 9 | Plan 範例 `FlowTemplatePreviewDialog` 接 `:template-uid` | 實際 prop 是 `:template` Object（含 `bpmn_xml`） | 既有 component 設計；M5 加 `openFlowTemplatePreview` helper 串接 list summary + detail fetch |

### 16.2 M7 smoke 發現的 Spec 2 carry-forward bug（M9–M13）

| # | 問題 | 修補 commit |
|---|---|---|
| M9 | 自建「規劃→執行→End」流程：執行任務 stage advance 後 AP 卡 auditing 出不來，因為 `task_execution` stage 的 hardcode handler 是 `launch_audit`（假設下一步是 audit stage） | BE `a365d71` — 加 `_is_terminal_user_task` 偵測 + `TerminalCloseHandler` + `OscalAuditService.terminal_close_ap` 寧包子 close |
| M10 | 自建流程沒含 audit/poam stage 時 FE「稽核結果」「改善計劃」按鈕仍依 status 顯示但點進去無資料 | BE `d0e6465` — enricher 加 `stage_codes` from BPMN；FE `152d1a1` — 兩個 view button v-if 加 stage gate |
| M11 | 「規劃→End」流程：AP 已 closed 但 `project.status` 卡 PENDING（M9 漏寫 project.status sync），列表顯示「未開始」跟詳情「已結案」不一致 | BE `02512b9` — `terminal_close_ap` 加 project_uid 參數 + project.status → COMPLETED；dev DB 既有資料用 SQL 手動修 |
| M12 | 自建流程沒掛 OSCAL task 時 completion_rate 永遠 0%；AP closed 後 user 預期 100% 但仍 0% | BE `728add2` + `b979a71`（hotfix `String` import）— 4 處 completion_rate 計算路徑加「`ap_status='closed' → 100%`」分支 |
| M13 | ApListView row buttons 只判斷 status+stage_codes，缺角色維度；ProjectAuditorOverview 右上同名 button 已對齊角色但 ApListView 沒 | FE `695baad` — 加 `isAuditor` computed + v-else-if 三維度判斷對齊 |

### 16.3 §11 strict-vs-lenient 行為對比實際落地

| Path | 設計 | 實作 |
|---|---|---|
| 建立 AP (`_bind_main_workflow_to_ap`) | strict — master 不存在 / `is_active=False` 拒絕 404 | ✅ `raise NotFound(GRC_FLOW_TEMPLATE_NOT_FOUND)` |
| 列表 / 詳情 enrichment (`FlowTemplateEnricher`) | lenient — master 軟刪 `is_master_active=False` 不擋 API | ✅ 三段 batch query lenient fallback，缺 ext / 缺 master 都回 None 或 partial |
| BPMN parse 失敗 (`_extract_stage_codes`) | M10 新加 — lenient（不擋 enrichment） | ✅ `try/except` 回 `[]` 加 log warning |
| terminal stage detection (`_is_terminal_user_task`) | M9 新加 — `get_next_elements` 空 → terminal | ✅ 含 Gateway 視為 non-terminal（user 確認的 rule） |

### 16.4 v3 已知 follow-up（不影響 ship）

整理自 Phase 6 review `docs/issues/pending/2026-05-13-spec3-phase-6-followups.md`：

| 項目 | 優先級 |
|---|---|
| `_sync_project_status_to_completed` helper 統一三處不一致 | High |
| `terminal_close_ap` 加角色檢查 | High |
| `launch_audit` dead code 54 行刪除 | Medium |
| M12 completion_rate 補 unit test | Medium |
| FE default flow_template guard（master 軟刪 UX） | Medium |
| Type design 重構（payload dataclass / StartProjectDto 必填 / StageInfoDto） | 中期 |

### 16.5 Spec plugin 化討論

User Phase 5 + smoke 收尾後提出「stage 物件能否 plugin 化」討論，當前不做，紀錄在 `docs/analysis/2026-05-13-stage-plugin-architecture-thoughts.md`。3 個 phase（FE button config / StagePlugin 整合 / auto-discovery）+ 觸發條件已記錄。

---

## 十七、文件版本

| 版本 | 日期 | 變更 |
|---|---|---|
| v1 | 2026-05-13 | 初版大方向，5 條 open questions 待 brainstorm |
| v2 | 2026-05-13 | brainstorm 收斂完整版：Q1–Q5 全部 resolved；補 §三 launch_new_round Spec 2 gap 認知；補 §四 snapshot 三層獨立性；BE/FE 改動細節到位；落地順序拆 M1–M8 |
| v2.1 | 2026-05-13 | spec reviewer 反饋整合：§9.6 列表 + 詳情兩個 endpoint 都要 enrichment（strict vs lenient 行為對比）；§10.2 標明 default 帶值靠 list enrichment（硬依賴）；§13 加 M0 pre-flight verify、M3/M4 順序對調防 regression |
| v3 | 2026-05-13 | summary 收尾加 §16 Implementation Reality / Reconciliation：plan 偏差 9 條 + M7 smoke 補 M9–M13 5 條 + strict/lenient 落地對照 + Phase 6 follow-up 引用 + plugin 化討論 reference |
