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

版本:v1(Phase 3) 建立日期:2026-05-12 依據design.md v2(含 §九 決議) 跨 repo:compliance-manager-be / compliance-manager-fe / compliance-manager-test


§1

〇、Pre-flight 假設驗證(Phase 5 開工前必跑一遍

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

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 同步差異後再開工。


§2

一、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)

§3

二、跨 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 完成

§4

三、Phase A — BE Schema + Registry 框架(無業務邏輯,純基建)

A.1 SQL migration(1 commit)

檔案scripts/sql/2026-05-XX-spec2-ap-extensions-and-handler-error-codes.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;

RollbackDROP TABLE compliance.assessment_plan_extensions CASCADE;

部署順序:先跑此 migration → 再部署 BE code(code 引用此表)。

A.2 新增 6 個 Error Code(1 commit)

檔案common/code/grc_error_code.py(追加)

# 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(新增)

"""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 wiringdi_containers/flow_engine/flow_engine_containers.py 加:

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 wiringdi_containers/grc/grc_containers.py 加 repo(Singleton)+ domain service(Factory)。

A.5 Phase A 完成定義

Commit 顆粒:A.1 / A.2 / A.3+A.4 共 3 個 commit(A.3 與 A.4 互相依賴 DI wiring,併一個 commit)


§5

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

B.1 OSCAL Stage Handlers 4 個(1 commit)

檔案app/grc/service/oscal_stage_handlers.py

"""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

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

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):

{ "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 步之後)

# 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 對應 poamaudit(無 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:

SQLscripts/sql/2026-05-XX-spec2-update-stage-object-handler-keys.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 完成定義

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


§6

五、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 加:

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

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 filessrc/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)

Commit 顆粒:C.1+C.2+C.3 共 1 個大 commit(內聚性高,難拆)


§7

六、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 完成定義

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


§8

七、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 規劃)


§9

八、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

§10

九、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

§11

十、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,純新增表 + 新增欄位值,部署順序可單向往前

§12

十一、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,無風險

§13

十二、預估工時(人/天)

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 天)


§14

十三、後續 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 文案差異

§15

十四、文件版本

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