# Spec 2：階段抽象整合 — 實作計畫

> **版本**：v1（Phase 3）
> **建立日期**：2026-05-12
> **依據**：`design.md` v2（含 §九 決議）
> **跨 repo**：compliance-manager-be / compliance-manager-fe / compliance-manager-test

---

## 〇、Pre-flight 假設驗證（**Phase 5 開工前必跑一遍**）

Plan 寫好到開工經常 days/weeks，期間 method 改名 / entity 改 shape / error code 序號被佔。實作前先 grep 核對：

```bash
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be

# 1. OscalAuditService 4 個 method 簽名仍存在
grep -n "def activate_project\|def launch_audit\|def confirm_audit\|def close_round" \
     app/project/service/oscal_audit_service.py
# 預期：line 77 / 169 / 323 / 419，簽名 (project_uid, [user_id,] ap_uid, [force,] curr_user)

# 2. WorkflowExecutionService.start_workflow_execution / complete_job 仍存在
grep -n "def start_workflow_execution\|def complete_job" \
     app/flow_engine/service/workflow_execution_service.py
# 預期：line 129 / 419

# 3. Stage object schema 已 seed 4 個
psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
     -c "SELECT code, name_zh, complete_handler_key, precondition_key FROM compliance.stage_objects ORDER BY id;"
# 預期：planning / task_execution / audit / poam 4 列

# 4. Spec 2 預留 error code 序號未被佔
grep -E "GRC_412021|GRC_403050|GRC_400050|GRC_404029|GRC_400051|GRC_400052" \
     common/code/grc_error_code.py
# 預期：6 個都查不到

# 5. compliance.assessment_plan_extensions 表還沒建
psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
     -c "\d compliance.assessment_plan_extensions"
# 預期：Did not find any relation
```

任一條對不上 → 暫停實作，回 design.md 同步差異後再開工。

---

## 一、Scope & Deliverables

### In scope（Spec 2）
- 1 張 DB 表：`compliance.assessment_plan_extensions`
- 6 個新 error code
- 1 個 Domain Registry + 2 個 Interface（StageCompletionHandler / StagePreconditionCheck）
- 1 個 Stage Advance App Service + 1 個 Route
- 1 個 Stage Status Query API（FE Banner 查當前 stage 用）
- 4 個 OSCAL Stage Completion Handler + 3 個 Precondition Check（grc 模組）
- 修改 `start_oscal_project` 流程：建 AP 時同步寫 extension + 啟動 workflow_execution
- 修改既有 EndEvent handling：透過 close_round handler 自動結案，**刪除暫態** `ExtAssessmentPlanRepoImpl`
- FE：`FlowPhaseBanner.vue` + `useStageInfo` composable + 4 個 view 注入 + 4 個推進按鈕拆除
- E2E：3 個 BDD scenario（happy path / 前置條件 fail / 角色擋）

### Out of scope（明確不做）
- 範本 CRUD（Spec 1 已交付）
- 建 AP 時範本 wizard（Spec 3）
- BPMN loop back PM Dialog（→ Spec 3+）
- 階段物件 CRUD UI（後續 spec）
- 觀察者進度條互動跳轉（v1 純預覽）
- Banner 上的「審閱通過 / 任務完成度」輔助統計（v1 選做，留 follow-up）

---

## 二、跨 repo 任務依賴圖

```
Phase A: BE schema + Registry interface
    ↓
Phase B: BE stage advance API + grc handlers + AP create flow 改造
    ↓ (BE 可獨立 manual test：用 curl + dev DB)
Phase C: FE Banner 元件 + composable + service
    ↓
Phase D: FE 4 個 view 注入 + 拆既有按鈕
    ↓ (BE+FE 跨 repo manual smoke test)
Phase E: E2E BDD scenarios
    ↓
Phase F: Phase 6 review + changelog + push
```

**並行機會**：
- Phase A.1（migration SQL）寫好後 → Phase A.2（error code）+ Phase A.3（registry interface）可並行
- Phase B 與 Phase C 完全並行（FE 用 mock 回應做 banner 元件）
- Phase E 等 Phase B+D 完成

---

## 三、Phase A — BE Schema + Registry 框架（無業務邏輯，純基建）

### A.1 SQL migration（1 commit）

**檔案**：`scripts/sql/2026-05-XX-spec2-ap-extensions-and-handler-error-codes.sql`

**內容**：
```sql
-- Date: 2026-05-XX
-- Spec 2 階段抽象整合 — AP extension table + RLS

-- 1. assessment_plan_extensions 延伸表 (1:1 of oscal.assessment_plans) (2026-05-XX)
CREATE TABLE compliance.assessment_plan_extensions (
    id                           SERIAL PRIMARY KEY,
    assessment_plan_id           INTEGER UNIQUE NOT NULL,
    workflow_execution_uid       UUID,
    flow_template_snapshot_uid   UUID,
    created_at                   TIMESTAMPTZ DEFAULT NOW() NOT NULL,
    updated_at                   TIMESTAMPTZ DEFAULT NOW() NOT NULL,
    created_user                 VARCHAR(50),
    updated_user                 VARCHAR(50),
    CONSTRAINT fk_ape_assessment_plan
        FOREIGN KEY (assessment_plan_id)
        REFERENCES oscal.assessment_plans(id)
        ON DELETE CASCADE
);

-- 2. 索引 (2026-05-XX)
CREATE INDEX ix_ape_workflow_execution_uid
    ON compliance.assessment_plan_extensions(workflow_execution_uid);
CREATE INDEX ix_ape_flow_template_snapshot_uid
    ON compliance.assessment_plan_extensions(flow_template_snapshot_uid);

-- 3. 權限授予（CLAUDE.md 規範）(2026-05-XX)
GRANT SELECT, INSERT, UPDATE, DELETE ON compliance.assessment_plan_extensions TO cm_app;
GRANT USAGE, SELECT ON SEQUENCE compliance.assessment_plan_extensions_id_seq TO cm_app;

-- 4. RLS policy — 跟著 assessment_plans 走（tenant 透過 AP JOIN 過濾） (2026-05-XX)
--    不開 RLS：本表純為 AP 1:1 延伸欄位，無獨立 tenant 隔離需求；
--    AP 本身在 oscal schema 由 jedi_common.session_scope 透過 GUC 處理 RLS。
--    若未來需 RLS：可考慮 EXISTS subquery 比對 oscal.assessment_plans 的 metadata。

-- 5. 補既有 AP 的 ext 列（dev/stg 既有資料 backfill） (2026-05-XX)
INSERT INTO compliance.assessment_plan_extensions (assessment_plan_id, created_user, updated_user)
SELECT id, COALESCE(created_user, 'system'), COALESCE(updated_user, 'system')
FROM oscal.assessment_plans
WHERE id NOT IN (SELECT assessment_plan_id FROM compliance.assessment_plan_extensions)
ON CONFLICT (assessment_plan_id) DO NOTHING;
```

**Rollback**：`DROP TABLE compliance.assessment_plan_extensions CASCADE;`

**部署順序**：先跑此 migration → 再部署 BE code（code 引用此表）。

### A.2 新增 6 個 Error Code（1 commit）

**檔案**：`common/code/grc_error_code.py`（追加）

```python
# Spec 2 Stage Integration (2026-05-XX)
GRC_STAGE_PRECONDITION_FAILED        = ("階段推進前置條件未滿足",       "GRC_412021")
GRC_STAGE_ROLE_FORBIDDEN             = ("使用者不具備此階段推進權限",   "GRC_403050")
GRC_STAGE_NO_NEXT_NODE               = ("流程推進失敗：找不到下一節點", "GRC_400050")
GRC_AP_NO_WORKFLOW_BINDING           = ("稽核計畫未綁定流程實例",       "GRC_404029")
GRC_STAGE_HANDLER_NOT_REGISTERED     = ("stage handler 未註冊",         "GRC_400051")
GRC_STAGE_OBJECT_NOT_BOUND           = ("BPMN UserTask 未綁定階段物件", "GRC_400052")
```

### A.3 Domain Registry + Interfaces（1 commit）

**檔案**：`domain/flow_engine/service/stage_completion_registry.py`（新增）

```python
"""Stage completion registry — flow_engine domain abstraction.

flow_engine 不知道 OSCAL business logic 存在。具體 handler 在 app/grc/ 註冊。
"""
from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Optional


@dataclass
class PreconditionResult:
    passed: bool
    reason_i18n_key: Optional[str] = None
    context: dict = field(default_factory=dict)


class IStageCompletionHandler(ABC):
    """副作用 handler — stage 推進時呼叫。"""

    @property
    @abstractmethod
    def key(self) -> str:
        """對應 stage_objects.complete_handler_key"""
        ...

    @abstractmethod
    def execute(
        self,
        ap_uid: str,
        project_uid: str,
        user_id: int,
        curr_user: str,
        ctx: dict,
    ) -> dict:
        """執行副作用；ctx 含 force / loopback_decision 等選用參數。
        回傳 dict 直接送回 stage_advance API 的 response data。
        """
        ...


class IStagePreconditionCheck(ABC):
    """前置條件檢查 — stage 推進前呼叫。"""

    @property
    @abstractmethod
    def key(self) -> str:
        """對應 stage_objects.precondition_key"""
        ...

    @abstractmethod
    def check(
        self, ap_uid: str, project_uid: str, ctx: dict
    ) -> PreconditionResult:
        ...


class StageRegistry:
    """Singleton — 啟動時 DI container 註冊 handler / precondition instance。

    flow_engine app service 透過此 registry 查 handler，不直接 import grc。
    """

    def __init__(self):
        self._handlers: dict[str, IStageCompletionHandler] = {}
        self._preconditions: dict[str, IStagePreconditionCheck] = {}

    def register_handler(self, handler: IStageCompletionHandler) -> None:
        self._handlers[handler.key] = handler

    def register_precondition(self, check: IStagePreconditionCheck) -> None:
        self._preconditions[check.key] = check

    def get_handler(self, key: str) -> Optional[IStageCompletionHandler]:
        return self._handlers.get(key)

    def get_precondition(self, key: str) -> Optional[IStagePreconditionCheck]:
        return self._preconditions.get(key)
```

**DI wiring**：`di_containers/flow_engine/flow_engine_containers.py` 加：
```python
stage_registry = providers.Singleton(StageRegistry)
```

### A.4 AP Extension Repo（1 commit）

**新增**：
- `domain/grc/entities/assessment_plan_extension_entity.py` — Entity + QueryEntity
- `domain/grc/repository/i_assessment_plan_extension_repo.py` — Interface
- `domain/grc/service/assessment_plan_extension_domain_service.py` — DomainService
- `infra/grc/model/assessment_plan_extension.py` — ORM Model
- `infra/grc/mapper/assessment_plan_extension_mapper.py` — Mapper
- `infra/grc/repository/assessment_plan_extension_repo_impl.py` — Impl（繼承 BaseRepositoryImpl）

**核心 method**：
- `get_by_ap_id(ap_id: int) -> Optional[Entity]`
- `get_by_ap_uid(ap_uid: str) -> Optional[Entity]` — via JOIN
- `set_workflow_execution_uid(ap_id, workflow_execution_uid, curr_user)`
- `set_flow_template_snapshot_uid(ap_id, template_uid, curr_user)`
- 透過 BaseRepositoryImpl 既有 `add / update / deactivate`

**DI wiring**：`di_containers/grc/grc_containers.py` 加 repo（Singleton）+ domain service（Factory）。

### A.5 Phase A 完成定義
- [ ] migration SQL dev DB 跑過、表確認建立
- [ ] error code import 不爆
- [ ] StageRegistry 可手動註冊 mock handler 並 get 回來（單元測試 1 條）
- [ ] AssessmentPlanExtensionRepo `get_by_ap_id` 可查到 backfill 的列

**Commit 顆粒**：A.1 / A.2 / A.3+A.4 共 3 個 commit（A.3 與 A.4 互相依賴 DI wiring，併一個 commit）

---

## 四、Phase B — BE Stage Advance API + Handlers + AP Create 改造

### B.1 OSCAL Stage Handlers 4 個（1 commit）

**檔案**：`app/grc/service/oscal_stage_handlers.py`

```python
"""OSCAL stage completion handlers — bridge between flow_engine and OSCAL audit lifecycle.

每個 handler key 對應 stage_objects.complete_handler_key，由 DI container 啟動時註冊到 StageRegistry。
"""
from domain.flow_engine.service.stage_completion_registry import IStageCompletionHandler


class PlanningOnCompleteHandler(IStageCompletionHandler):
    """planning stage 推進 → 呼叫 OscalAuditService.activate_project."""

    def __init__(self, oscal_audit_service):
        self._oscal_audit_service = oscal_audit_service

    @property
    def key(self) -> str:
        return "oscal.planning.activate"

    def execute(self, ap_uid, project_uid, user_id, curr_user, ctx) -> dict:
        force = ctx.get("force", False)
        return self._oscal_audit_service.activate_project(
            project_uid=project_uid, ap_uid=ap_uid, user_id=user_id,
            force=force, curr_user=curr_user,
        )


class TaskExecutionOnCompleteHandler(IStageCompletionHandler):
    key = "oscal.task_execution.launch_audit"  # 同上 pattern, 略

class AuditOnCompleteHandler(IStageCompletionHandler):
    key = "oscal.audit.confirm_audit"

class PoamOnCompleteHandler(IStageCompletionHandler):
    key = "oscal.poam.close_round"
```

**注意**：4 個 handler 不在自己內加 `@transaction` — 由上層 `StageAdvanceService` 開 scope。OscalAuditService 各 method 自己有 `@transaction`，**會嵌套**。需確認 `jedi_common.transaction` 是否支援嵌套（既有 `confirm_audit` 內部也呼叫 `close_round`，已是嵌套用法，pattern 安全）。

### B.2 OSCAL Stage Preconditions 3 個（1 commit）

**檔案**：`app/grc/service/oscal_stage_preconditions.py`

```python
class TasksThresholdCheck(IStagePreconditionCheck):
    """task_execution → audit 推進前檢查未完成 task 數。

    與 launch_audit 內既有 unassigned/incomplete 檢查邏輯 align；
    若 force=true 由 caller 在 StageAdvanceService 跳過此 check。
    """
    @property
    def key(self): return "oscal.tasks_threshold_check"

    def check(self, ap_uid, project_uid, ctx):
        incomplete = self._oscal_audit_query.get_incomplete_task_count(ap_uid)
        if incomplete > 0:
            return PreconditionResult(
                passed=False,
                reason_i18n_key="flow_engine.precondition.tasks_not_complete",
                context={"incomplete": incomplete},
            )
        return PreconditionResult(passed=True)


class AllControlsVerdictedWithFinding(IStagePreconditionCheck):
    key = "oscal.all_controls_verdicted_with_finding"
    # 對應 GRC_412003 + GRC_412011 既有檢查


class AllPoamClosed(IStagePreconditionCheck):
    key = "oscal.all_poam_closed"
    # 對應 GRC_412005 既有檢查
```

**注意**：這些 check 與 OscalAuditService 內既有 raise PreconditionFailedError 重複；
本 spec 把 check **前置** 到 stage advance API，提早回給 FE disable 按鈕 + tooltip；
真正寫入時 OscalAuditService 內仍會再 raise（雙保險，避併發 race）。

### B.3 Stage Advance App Service（1 commit）

**檔案**：`app/flow_engine/service/stage_advance_service.py`

```python
class StageAdvanceService:
    def __init__(
        self,
        stage_registry,
        stage_object_domain_service,
        ap_extension_domain_service,
        workflow_execution_service,
        flow_template_domain_service,
        project_participant_domain_service,
        grc_project_domain_service,
        user_domain_service,
    ):
        ...

    @transaction
    def get_current_stage_info(self, ap_uid: str, user_id: int) -> dict:
        """Banner 用：回傳當前 stage code + name + progress bar + 推進按鈕 metadata.

        Response:
        {
            "current_stage": {
                "code": "planning",
                "name_i18n": "規劃",
                "complete_button_label_i18n": "啟動專案",
                "complete_handler_key": "oscal.planning.activate",
                "precondition_key": null,
                "main_roles": ["manager"]
            },
            "user_can_advance": true,
            "precondition": {"passed": true, "reason_i18n_key": null, "context": {}},
            "progress": [
                {"code": "planning", "name_i18n": "規劃", "status": "done"},
                {"code": "task_execution", "name_i18n": "執行任務", "status": "current"},
                {"code": "audit", "name_i18n": "稽核", "status": "pending"},
                {"code": "poam", "name_i18n": "缺失改善", "status": "pending"}
            ],
            "ap_status": "active",
            "workflow_execution_uid": "<uuid>"
        }
        """
        # 1. 查 AP extension 拿 workflow_execution_uid + flow_template_snapshot_uid
        # 2. 查 workflow_execution 當前 user task → 找到對應 stage_object
        # 3. 查 user 的 participant role → 判 user_can_advance
        # 4. 跑 precondition check（不 raise，回 PreconditionResult）
        # 5. 解 flow_template snapshot BPMN 取出 stage sequence 算 progress
        ...

    @transaction
    def advance_stage(self, ap_uid: str, user_id: int, curr_user: str, force: bool = False, ctx: dict = None) -> dict:
        """推進當前 stage：
        1. 查 AP extension → workflow_execution
        2. 查 current user task → stage_object
        3. 主要角色驗證（manager / auditor / reviewer / viewer，依 stage default_main_roles 或 BPMN UserTask 覆寫）
        4. precondition.check()；若 fail 且 not force：raise PreconditionFailedError(GRC_STAGE_PRECONDITION_FAILED)
        5. handler.execute() ← 副作用呼叫 OscalAuditService（包 force / ctx）
        6. workflow_execution_service.complete_job() ← 推進 BPMN 到下個節點
        7. 若 BPMN 已到 EndEvent → 由 close_round handler 已處理（不額外動）
        8. 回傳新 stage info（recursive call get_current_stage_info）
        """
        ...
```

**關鍵設計**：
- 「主要角色」資料來源：先看 BPMN UserTask 屬性的 `mainRole` extension（spec 1 編輯器存的），fallback 到 stage_object.default_main_roles
- `force` 僅 manager 角色可用（FE 不傳給其他角色 + BE 驗證）
- `ctx` 內可帶 `loopback_decision` (預留，v1 不用)

### B.4 Stage Advance Route（1 commit）

**檔案**：`api/flow_engine/routes/stage_advance_route.py`

| Endpoint | Method | Resource |
|---|---|---|
| `/project/<project_uid>/ap/<ap_uid>/stage/info` | GET | `StageInfoResource` |
| `/project/<project_uid>/ap/<ap_uid>/stage/advance` | POST | `StageAdvanceResource` |

**Request schema**（advance）：
```json
{ "force": false, "ctx": {} }
```

**Schema 命名**：
- `api/flow_engine/serializers/stage_advance_request_schema.py`
- `api/flow_engine/serializers/stage_info_response_schema.py`

**RBAC capability**：
- 沿用既有 GRC 路由的 capability（無需新增 capability，因為角色驗證在 service 層 stage main role 邏輯）
- 但「能看 Banner」= 能進 view = `audit_module.read` 等既有 capability
- 「能推進 stage」= 看 stage main role（不是獨立 capability）

### B.5 修改 `start_oscal_project` 接入 workflow_execution（1 commit）

**位置**：`app/project/service/oscal_project_service.py:start_oscal_project` (line 143)

**新增步驟（Step 22, 在既有 21 步之後）**：

```python
# Step 22 — 建 AP Extension + 啟動 main workflow_execution
from app.flow_engine.service.flow_template_app_service import ...
from app.flow_engine.service.workflow_execution_service import ...

# 22.1 取 hardcode default 範本（Spec 2）：builtin-full-audit
default_template = self._flow_template_app_service.get_by_code("builtin-full-audit")

# 22.2 啟動 main workflow_execution（依範本 BPMN）
main_wf = self._workflow_execution_service.start_workflow_execution(
    template_id=default_template.id,
    name=f"AP-{ap.uid}-main",
    params={"ap_uid": str(ap.uid), "project_uid": project_uid},
    created_user=curr_user,
)

# 22.3 寫 AP extension
self._ap_extension_domain_service.add(
    AssessmentPlanExtensionEntity(
        assessment_plan_id=ap.id,
        workflow_execution_uid=main_wf.uid,
        flow_template_snapshot_uid=default_template.uid,
        created_user=curr_user,
        updated_user=curr_user,
    )
)
```

**try/except**：22.2 / 22.3 失敗 → log error 但不 rollback AP 建立（沿用既有 Step 7 的容錯策略）；user 看到「workflow 啟動失敗」warning，可手動 retry。

**注意**：`start_workflow_execution` 既有實作會建立 task-level workflow execution 給各 AP task；本 Spec 2 加的是 **main workflow** 給 AP lifecycle，這是**獨立 instance**（不同 template）。需確認 spec 1 的 workflow_execution table 是否支援這雙層結構 — 預期支援，spec 1 已 seed `is_main` 欄位。

### B.6 EndEvent 副作用收口（1 commit）

**現況**：
- 未追蹤檔 `domain/flow_engine/repository/i_ext_assessment_plan_repo.py` + `infra/.../ext_assessment_plan_repo_impl.py` 走 raw SQL 直接 UPDATE `oscal.assessment_plans.status='closed'`
- 觸發點：BPMN engine `FlowExecutionService._on_end_event_reached`（spec 1 文件提及）

**改造**：
- **刪除** 兩個未追蹤檔
- `_on_end_event_reached` 改：若 BPMN ctx 含 `ap_uid` 且當前 last completed UserTask 對應 `poam` 或 `audit`（無 poam 路徑）stage，**不額外動 AP 狀態**
  → close_round / confirm_audit 由 stage advance API 在 user 按 Banner 時就已呼叫
- EndEvent 觸發時只清 main workflow execution 自身狀態（archive / mark completed），AP 已在 handler 內被 close

**為什麼**：避免「兩條路徑同時改 AP 狀態」造成幽靈轉換；handler 是 single source of truth。

### B.7 Stage Object Seed 補欄位（1 commit）

**位置**：spec 1 已 seed 4 個 stage object，但 `complete_handler_key` / `precondition_key` 是 placeholder。本 spec 補實 handler key：

**SQL**：`scripts/sql/2026-05-XX-spec2-update-stage-object-handler-keys.sql`

```sql
-- Spec 2 — 補 stage_objects 的 handler key 與 precondition key (2026-05-XX)
UPDATE compliance.stage_objects SET complete_handler_key = 'oscal.planning.activate',
       precondition_key = NULL WHERE code = 'planning';
UPDATE compliance.stage_objects SET complete_handler_key = 'oscal.task_execution.launch_audit',
       precondition_key = 'oscal.tasks_threshold_check' WHERE code = 'task_execution';
UPDATE compliance.stage_objects SET complete_handler_key = 'oscal.audit.confirm_audit',
       precondition_key = 'oscal.all_controls_verdicted_with_finding' WHERE code = 'audit';
UPDATE compliance.stage_objects SET complete_handler_key = 'oscal.poam.close_round',
       precondition_key = 'oscal.all_poam_closed' WHERE code = 'poam';
```

### B.8 Phase B 完成定義
- [ ] curl + dev DB 跑 happy path：建專案 → AP 自動建 ext + workflow → GET stage/info 看到 `planning` → POST stage/advance 進 `task_execution`（AP.status: preparing→active）
- [ ] precondition fail case：POST stage/advance 在 task_execution 階段（有未完成 task）→ 回 412 GRC_STAGE_PRECONDITION_FAILED + context
- [ ] force=true bypass：同上加 `{"force": true}` → 200 進 `audit`
- [ ] 角色擋：auditor 在 planning 推進 → 403 GRC_STAGE_ROLE_FORBIDDEN
- [ ] 完整跑 4 個 stage 到 EndEvent → AP.status=closed
- [ ] pytest 過

**Commit 顆粒**：B.1+B.2 / B.3+B.4 / B.5 / B.6 / B.7 共 5 個 commit

---

## 五、Phase C — FE Banner 元件 + Service

### C.1 API service（1 commit）

**新增**：`src/service/StageService.js`（繼承 BaseService）
- `getStageInfo(projectUid, apUid)`
- `advanceStage(projectUid, apUid, payload)`

**API 常數**：`src/config/api/api.js` 加：
```js
STAGE_INFO: '/project/:projectUid/ap/:apUid/stage/info',
STAGE_ADVANCE: '/project/:projectUid/ap/:apUid/stage/advance',
```

### C.2 `useStageInfo` composable（1 commit）

**新增**：`src/composables/useStageInfo.js`

```js
export function useStageInfo(projectUid, apUid) {
  const stageInfo = ref(null)
  const loading = ref(false)
  const error = ref(null)

  async function fetchStageInfo() { ... }
  async function advance({ force = false, ctx = {} } = {}) { ... }

  return { stageInfo, loading, error, fetchStageInfo, advance }
}
```

### C.3 `FlowPhaseBanner` 元件（1 commit）

**新增**：`src/components/grc/FlowPhaseBanner.vue`

**Props**：
- `projectUid: String` (required)
- `apUid: String` (required)

**內部結構**（對齊 design.md §四）：
```
<div class="flow-phase-banner">
  <div class="stage-header">
    <span class="stage-name">{{ t(stageInfo.current_stage.name_i18n) }}</span>
  </div>
  <StageProgressBar :progress="stageInfo.progress" />        <!-- v1 純預覽 -->
  <div class="stage-hint">{{ t(stageInfo.current_stage.hint_i18n) }}</div>
  <Button
    v-if="stageInfo.current_stage && canAdvance"
    :disabled="!preconditionPassed"
    v-tooltip.left="preconditionTooltip"
    @click="onAdvanceClick">
    {{ t(stageInfo.current_stage.complete_button_label_i18n) }}
  </Button>
  <ConfirmDialog ... />   <!-- force override 用 -->
</div>
```

**子元件**：`src/components/grc/StageProgressBar.vue`（純展示 4 dot + label，CSS only）

**互動**：
- precondition 未過 → 按鈕 disable，hover 顯示 tooltip（i18n reason + context）
- precondition 未過 + manager 角色 → 按鈕不 disable，點下跳 ConfirmDialog，確認 `force=true` 送出
- 推進成功 → `fetchStageInfo` refresh banner + emit `stage-advanced` 給父 view（讓父 view refresh 自己的資料）
- 推進失敗 → toast 顯示後端 error msg

**i18n key 設計**：
- `flow_engine.banner.stage_name.<code>` ← 從 stage_object.name_i18n
- `flow_engine.banner.button_label.<code>` ← 從 stage_object.complete_button_label_i18n
- `flow_engine.precondition.<reason_key>` ← 從後端 PreconditionResult.reason_i18n_key

**i18n files**：`src/i18n/zh-tw.js` + `src/i18n/en.js` 加 reason key（4 條：tasks_not_complete / verdict_incomplete / finding_required / poam_not_all_closed）

### C.4 Phase C 完成定義（不掛 view）
- [ ] Banner 元件可獨立 storybook / playground 渲染，mock 4 種 stage 狀態 + 2 種 precondition 狀態
- [ ] `useStageInfo` 在 mock API 下 fetch / advance 都正常

**Commit 顆粒**：C.1+C.2+C.3 共 1 個大 commit（內聚性高，難拆）

---

## 六、Phase D — FE 4 個 view 注入 + 拆既有按鈕

### D.1 ProjectSettingsView（planning stage）

**位置**：`src/views/grc/ProjectSettingsView.vue`
**改動**：
- 頂部加 `<FlowPhaseBanner :project-uid="..." :ap-uid="..." />`（apUid 從 route param 或 store 取）
- 拆掉既有「啟動專案」按鈕（其按鈕 onClick 原本呼叫 `activateProject` API，整個區塊刪除）
- 監聽 banner `stage-advanced` → router push 或 refresh dashboard

### D.2 ProjectAuditorOverview（overview，含 task_execution）

**位置**：`src/views/grc/ProjectAuditorOverview.vue`
**改動**：
- 頂部加 `FlowPhaseBanner`
- 拆掉右上角的「啟動稽核」按鈕（line 1180~1184，design.md §六 已標位置）
- 4 個既有「導航按鈕」（專案規劃 / 進入稽核 / 查看稽核 / 改善計畫）**不動**

### D.3 AuditReviewView（audit stage）

**位置**：`src/views/grc/AuditReviewView.vue`
**改動**：
- 頂部加 `FlowPhaseBanner`
- 拆掉「提交稽核」按鈕

### D.4 PoamView（poam stage）

**位置**：`src/views/grc/PoamView.vue`
**改動**：
- 頂部加 `FlowPhaseBanner`
- 拆掉「完成改善」按鈕

### D.5 MyTasksView（task_execution，**不掛 Banner**）

**位置**：`src/views/grc/MyTasksView.vue`
**改動**：**無**（spec 明確不在此頁掛 Banner，待辦任務由 AP.status + 角色過濾資料）

### D.6 i18n 補齊 + frontend-overview cheatsheet 更新（1 commit）

- `src/i18n/zh-tw.js` / `en.js` 補 4 個 stage 的 banner 文案
- `docs/claude/frontend-overview.md` 加 Banner 元件條目

### D.7 Phase D 完成定義
- [ ] manual smoke test：完整跑一輪稽核 4 個 stage，每個 view 進去都看到 Banner，推進按鈕一致
- [ ] 既有 4 個推進按鈕完全消失（grep code 確認）
- [ ] 既有 4 個導航按鈕仍在 + v-if 邏輯不變

**Commit 顆粒**：D.1~D.4 各 1 commit + D.6 1 commit = 5 commits

---

## 七、Phase E — E2E BDD（compliance-manager-test repo）

### E.1 Scenarios（feature-test-planner agent 細化在 Phase 4 test-plan.md）

預計 3 個 BDD scenario：

1. **Happy path — 完整 BPMN 推進**
   - Given manager 登入有專案 + AP（status=preparing）
   - When 進 settings → 點 Banner「啟動專案」
   - Then AP 進 active；進 overview 看到「啟動稽核」按鈕 in Banner
   - When 完成所有 task → 點「啟動稽核」
   - Then AP 進 auditing；進 audit page 看到「提交稽核」按鈕
   - When 完成所有 verdict + finding → 點「提交稽核」
   - Then AP 進 remediation；進 poam 看到「完成改善」按鈕
   - When 所有 POA&M closed → 點「完成改善」
   - Then AP closed；Banner 顯示「已結案」

2. **前置條件未滿足 — task_execution 階段**
   - Given AP=active 但有未完成 task
   - When 進 overview
   - Then Banner「啟動稽核」按鈕 disabled，tooltip 顯示「尚有 12 / 50 任務未完成」

3. **角色擋 — auditor 在 planning 嘗試推進**
   - Given auditor 角色登入
   - When 進 settings 看 Banner
   - Then 「啟動專案」按鈕**不顯示**（user_can_advance=false）

### E.2 Phase 4 test-plan.md 詳細展開
（由 feature-test-planner agent 處理 — 包含 BE pytest + FE component test + BDD step / page object 規劃）

---

## 八、Phase F — Review + Changelog + Push

### F.1 Code review（pr-review-toolkit）
- 跑 `code-reviewer` x 2（review BE 跨 5 commits / review FE 跨 5 commits）
- 跑 `silent-failure-hunter`（特別檢 try/except 區塊：Step 22 容錯邏輯 / handler chain）
- 跑 `pr-test-analyzer`（test coverage gap）
- 跑 `type-design-analyzer`（Registry + Interface 設計）

### F.2 Changelog
- BE: `docs/changelog/2026-05-XX-feat-stage-integration-be.md`
- FE: `compliance-manager-fe/docs/changelog/2026-05-XX-feat-stage-integration-fe.md`
- Test: `compliance-manager-test/docs/changelog/2026-05-XX-feat-stage-integration-e2e.md`

### F.3 Reconciliation 段
若實作過程發現偏差（90% 機率會有），補 `design.md §十 Reconciliation` 段 — 跟 Spec 1 §九 同 pattern。

### F.4 Push 順序
1. BE branch push（含 SQL migration 標記）
2. FE branch push
3. Test branch push
4. dev DB 部署完整跑：rollback → re-migrate → 重啟 BE → manual smoke test

---

## 九、CLAUDE.md 規範 Checklist

| 規範 | 落實位置 | 自我檢查 |
|---|---|---|
| Route 不查 DB | api/flow_engine/routes/stage_advance_route.py | ✓ Resource 只 call app service |
| App Service `@transaction` | StageAdvanceService.get_current_stage_info / advance_stage | ✓ |
| Repo session lazy (`@property` 或繼承 BaseRepositoryImpl) | AssessmentPlanExtensionRepoImpl | ✓ 繼承 BaseRepositoryImpl |
| 寫入 API 角色檢查 | advance_stage 內 stage main role 驗證 | ✓ raise GRC_STAGE_ROLE_FORBIDDEN |
| 前置條件驗證 | precondition check 在 advance_stage 內 | ✓ raise GRC_STAGE_PRECONDITION_FAILED |
| Error code 命名 `GRC_<HTTP><3 位序號>` | grc_error_code.py 加 6 條 | ✓ 412021 / 403050 / 400050 / 404029 / 400051 / 400052 |
| SQL migration 日期註解 + GRANT | scripts/sql/2026-05-XX-spec2-*.sql | ✓ |
| 審計欄位 created_user + created_user_name | StageInfoResponseSchema 內如有 user_id 必補 nickname | ✓（v1 response 不含 user_id） |
| RLS | AP extension 不開 RLS（透過 AP 關聯自然隔離） | ✓ 文件已說明 |
| DDD 嚴格規範 | flow_engine domain 不 import grc / oscal | ✓ Registry pattern |
| 跨模組依賴 | DI container wiring | ✓ grc_containers 註冊 handler 到 flow_engine registry |

---

## 十、Migration 順序 & Rollback

### 部署順序
```
1. dev DB 跑 scripts/sql/2026-05-XX-spec2-ap-extensions-and-handler-error-codes.sql
2. dev DB 跑 scripts/sql/2026-05-XX-spec2-update-stage-object-handler-keys.sql
3. 重啟 BE（pkill -9 + 重起，CLAUDE.md 規範）
4. FE npm run build:DEV → deploy dist/
5. E2E 跑一次完整 scenarios
```

### Rollback 路徑
- DB：`DROP TABLE compliance.assessment_plan_extensions CASCADE;` + UPDATE stage_objects SET ... = NULL
- BE code：revert phase A~B commits
- FE code：revert phase C~D commits
- 前提：rollback 前 user 沒有開新 AP（既有 AP 的 ext 列會跟著 CASCADE drop）

### Prod 部署 caveat
- 與 Spec 1 同 pattern：要先跑 fix-up SQL（若有），再跑 schema SQL（順序顛倒會中英並存）
- 本 spec 沒有 rename / fix-up，純新增表 + 新增欄位值，部署順序可單向往前

---

## 十一、Risk Register

| Risk | 機率 | 影響 | Mitigation |
|---|---|---|---|
| `start_workflow_execution` 雙層（main + task）table 不支援 | 低 | 高 | Pre-flight grep workflow_executions schema 確認 `is_main` 欄位；若無需另開欄位 |
| `@transaction` 嵌套 commit 邏輯衝突 | 低 | 中 | 既有 `confirm_audit→close_round` 已嵌套，pattern 安全 |
| Handler chain 中 OscalAuditService raise 後 BPMN advance 已執行 → 資料不一致 | 中 | 高 | 在 stage advance service 內**先 handler.execute → 再 complete_job**；handler 走 OscalAuditService 既有 `@transaction` rollback，BPMN 推進在 commit 後 |
| Spec 1 builtin BPMN 的 `pm_decides_loopback` placeholder 在 v1 不被 evaluate → gateway 走錯路徑 | 中 | 中 | Phase B 確認 BPMN engine 對 placeholder default 行為（預期走 default flow=closed） |
| FE Banner 在 task_execution stage（無 routed view 對應）顯示位置 | 低 | 低 | 仍掛在 overview 頁，stage_object.kind=stateful 時 banner 文案改「您正在執行任務階段...」 |
| backfill SQL 把暫時沒對應 AP 的孤兒 ext 列建出來 | 低 | 低 | INSERT ... ON CONFLICT DO NOTHING + WHERE NOT IN，無風險 |

---

## 十二、預估工時（人/天）

| Phase | 工作量 | 累計 |
|---|---|---|
| Pre-flight + Phase A | 0.5 | 0.5 |
| Phase B | 1.5 | 2.0 |
| Phase C | 1.0 | 3.0 |
| Phase D | 1.0 | 4.0 |
| Phase E（test-plan + scenario） | 1.5 | 5.5 |
| Phase F（review + changelog + push） | 0.5 | 6.0 |

**合計：6 人/天**（單一 BE session 連續跑可壓到 4 天）

---

## 十三、後續 Spec 預埋

| 預埋項目 | 將在哪個 Spec 啟用 |
|---|---|
| `assessment_plan_extensions.flow_template_snapshot_uid` 欄位（Spec 2 已寫但用 hardcode） | Spec 3：建 AP wizard 寫入 user 選的範本 uid |
| StageAdvanceService.advance_stage 的 `ctx.loopback_decision` param | 未來 spec：PM Dialog 二擇一 |
| Stage progress bar 的 click handler | 未來 spec：互動式跳轉 |
| Stage object schema 的「kind=stateful」分支 | 已用：task_execution banner 文案差異 |

---

## 十四、文件版本

| 版本 | 日期 | 變更 |
|---|---|---|
| v1 | 2026-05-12 | 初版實作計畫（Phase 3 產出，依 design.md v2 §九 決議） |
