Spec 2:階段抽象整合

狀態:v2 含 §九 決議段(A1~A4 + B1~B2 + error code caveat) 整體脈絡../README.md 更新日期:2026-05-12


§1

一、目標

把目前 5 個既有 view(settings / overview / audit / poam / 待辦任務)改造成「BPMN UserTask 可推進」的階段;提供 stage 推進 API;UI 收口到 Banner。


§2

二、需求摘要

來源:requirement.md §「想法(粗略)」§2、§3 末尾「上方功能按鈕 / Complete 按鈕 / 文案隨階段變化」

  1. AP 上掛 1:1 main workflow_execution(BPMN 主流程實例)
  2. 既有 5 個 view 接入「當前階段」概念,上方顯示 Banner
  3. 使用者按 Banner「完成此階段」推進 BPMN 到下一節點
  4. 推進前驗證主要角色權限 + 前置條件
  5. 推進時觸發舊按鈕的副作用(原本「啟動專案 / 啟動稽核 / 提交稽核 / 完成改善」做的事)
  6. EndEvent 觸發 AP 自動結案
  7. 「執行任務」階段不綁頁面 — 待辦任務頁與總覽控制項導覽區塊靠 AP.status + 角色過濾資料

§3

三、範圍

In scope

  • AP ↔︎ workflow_execution 1:1 關聯(schema 改動)
  • Stage 推進 API(complete current stage)
  • 前置條件 hook 框架(每階段物件可註冊)
  • 副作用 handler 框架 — handler 本質上是呼叫 Spec 1 §3.2 定義的 4 個既有 API
  • AP.status 與 stage 推進連動(接 §3.2 既有 API 自動處理)
  • FlowPhaseBanner FE 元件
  • 4 個 routed view 接入 Banner(settings / overview / audit / poam)
  • 4 個推進按鈕拆下來搬 Banner
    • settings 頁的「啟動專案」
    • overview 右上角的「啟動稽核」(AP.status=active 時顯示那顆)
    • audit 頁的「提交稽核」
    • poam 頁的「完成改善」
  • 待辦任務頁資料層過濾(既有「執行任務」邏輯不動,確認可 work)

Out of scope(明確不做)

  • 範本 CRUD(→ Spec 1)
  • 建 AP 時選範本(→ Spec 3)
  • 過渡期:暫用寫死的 default 範本給 AP,Spec 3 完成後改為使用者選擇
  • 既有 4 個導航按鈕不動(overview 右上角的「專案規劃 / 進入稽核 / 查看稽核 / 改善計畫」),沿用既有 v-if 邏輯
  • Overview shell 化重構(→ Spec 3,本 spec 只在既有 overview 上掛 Banner)
  • Stage progress bar 互動式跳轉(v1 進度條純預覽不可點)

§4

四、Banner UI 結構

每個 routed view(settings / overview / audit / poam)上方掛 FlowPhaseBanner,內容:

┌──────────────────────────────────────────────────────────────┐
│ 目前在「執行任務」階段                                          │
│ ●━━━━●━━━○━━━○  (規劃 ✓ 執行 ◆ 稽核 改善)  ← 純預覽,不可點   │
│ 提示文字(隨 stage 變化,從 stage metadata 取)                  │
│                                              [▶ 啟動稽核]      │
└──────────────────────────────────────────────────────────────┘
Banner 元素 來源 / 行為
階段名稱 從 stage name_i18n
進度條 從 AP 的 BPMN snapshot 解析所有 stage 序列;當前 stage 高亮,過去 stage 標 ✓;v1 不可點(visualization only)
提示文字 從 stage metadata(i18n key)
推進按鈕 文字 = stage complete_button_label_i18n;顯示條件 = 使用者擁有 stage default_main_roles(或 BPMN UserTask 覆寫的 main_roles)
前置條件未滿足 按鈕 disable + tooltip 顯示原因(如「尚有 12 / 50 任務未完成」)

「執行任務」stage(stateful)只掛 Banner 在 overview 頁;其他三個 routed view(settings / audit / poam)各自掛。


§5

五、task_execution 階段的「PM + 審查人員協作」設計

角色 在此階段做什麼
manager (PM) 看 task 完成進度 + 看 reviewer 的 review_mark 進度,自行決定何時按 Banner「啟動稽核」推進
reviewer 對控制項 / AO 用既有 review_mark 機制標記「審閱通過」(既有 API:POST /grc/.../controls/<uid>/review-markPOST /grc/.../ao/<uid>/review-mark,不需新做)
auditor / viewer 唯讀

Banner 上的輔助統計(v1 可選做、可延後):

任務完成度:38 / 50 控制項
審閱通過:22 / 50 控制項  (reviewer 已標記)

「review_mark 是否強制 gate」v1 不做強制 — PM 可自由判斷推進;既有 launch-audit API 的 force=false 邏輯已可擋未完成任務並彈 confirm dialog。


§6

六、高階任務拆解

後端

前端


§7

七、對外依賴

  • 依賴 Spec 1:階段物件 schema 與 seed、GET /flow-engine/stage-objects
  • 過渡期:暫用寫死的 default 範本給 AP,等 Spec 3 完成後改為使用者選擇

§8

八、開放問題(已決議,紀錄保留供溯源)

全部於 2026-05-12 收斂;具體決議見 §九。


§9

九、決議(2026-05-12)

對應 §八 5 條 + README §四 跨 spec 2 條,Phase 3 plan 前的最終收斂。

9.1 AP ↔︎ workflow_execution 1:1 — 採延伸表

決議:新建 compliance.assessment_plan_extensions 表(mirror 既有 compliance.project_extensions pattern),不修改 jedi-oscal oscal.assessment_plans

Schema 草案(細節 plan.md 落地):

CREATE TABLE compliance.assessment_plan_extensions (
    id                           SERIAL PRIMARY KEY,
    assessment_plan_id           INTEGER UNIQUE NOT NULL,            -- FK to oscal.assessment_plans(id), 1:1
    workflow_execution_uid       UUID,                                -- BPMN main workflow execution(AP 建立時同步寫入)
    flow_template_snapshot_uid   UUID,                                -- AP 採用的範本 snapshot(Spec 2 暫用 hardcode default,Spec 3 由 user 選)
    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
);
CREATE INDEX ix_ape_workflow_execution_uid ON compliance.assessment_plan_extensions(workflow_execution_uid);
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;

為什麼

  • jedi-oscal 套件邊界保持乾淨(不動共用套件)
  • compliance.project_extensions pattern 完全一致,新開發者好理解
  • 1:1 由 UNIQUE constraint 強制;cross-schema FK 在 PostgreSQL 合法

未來反悔條件:若 jedi-oscal owner 同意納編,可移欄位回主表(migration 直接搬欄位 + drop ext)。

淘汰的暫態 patch:未追蹤的 domain/flow_engine/repository/i_ext_assessment_plan_repo.pyinfra/.../ext_assessment_plan_repo_impl.py 走 cross-schema raw SQL 直接更新 oscal.assessment_plans.status,Spec 2 接手後改走 grc 模組既有 OscalAuditService.close_round(via hook handler),這兩個檔在 Phase 5 刪除。

9.2 前置條件 UX — disable + tooltip + manager force override

決議:Banner 推進按鈕預設 UX:

情境 按鈕行為 訊息來源
前置條件 pass 可點
前置條件 fail disabled + tooltip 顯示原因 precondition function 回傳的 (passed=False, reason_i18n_key, context)
Manager 角色強制推進 點下 → 跳 ConfirmDialog 列原因 → 確認後送 force=true 沿用 launch-audit 既有 pattern

Precondition 簽名

@dataclass
class PreconditionResult:
    passed: bool
    reason_i18n_key: Optional[str] = None   # e.g. "flow_engine.precondition.tasks_not_complete"
    context: Optional[dict] = None           # e.g. {"completed": 38, "total": 50}

class IStagePreconditionCheck(ABC):
    @property
    @abstractmethod
    def key(self) -> str: ...                # 對應 stage_object.precondition_key

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

為什麼

  • 與既有 launch-audit force=false 設計一致,FE 不需要學新 pattern
  • disable + tooltip 直接看到原因,比 toast/dialog 不打斷流程

9.3 副作用 handler — Hook Interface + Registry

決議:採乾淨 DDD 方案,flow_engine 不知道 OSCAL 存在

架構

domain/flow_engine/service/
├── stage_completion_registry.py     ← Registry singleton
│   ├── IStageCompletionHandler      ← Interface(key + execute)
│   ├── IStagePreconditionCheck      ← Interface(key + check)
│   ├── PreconditionResult           ← Dataclass
│   └── StageRegistry                ← register / get_handler / get_precondition

app/flow_engine/service/
└── stage_advance_service.py         ← 從 registry 查 handler 呼叫,不知 OSCAL 細節

app/grc/service/
└── oscal_stage_handlers.py          ← 4 個 Handler class(OSCAL business logic)
    ├── PlanningOnComplete           → 呼叫 OscalAuditService.activate
    ├── TaskExecutionOnComplete      → 呼叫 OscalAuditService.launch_audit
    ├── AuditOnComplete              → 呼叫 OscalAuditService.confirm_audit
    └── PoamOnComplete               → 呼叫 OscalAuditService.close_round

app/grc/service/
└── oscal_stage_preconditions.py     ← 3 個 Precondition class(task_execution / audit / poam)

di_containers/grc/grc_containers.py  ← 啟動時 register handlers + preconditions 到 registry

Stage object schema 對映(已在 spec 1 定義):

stage code complete_handler_key precondition_key(v1 提案)
planning oscal.planning.activate (v1 無,永遠 pass)
task_execution oscal.task_execution.launch_audit oscal.tasks_threshold_check(force 可 bypass)
audit oscal.audit.confirm_audit oscal.all_controls_verdicted_with_finding
poam oscal.poam.close_round oscal.all_poam_closed

為什麼

  • flow_engine 模組仍可獨立給其他 domain 用(非 OSCAL 業務)
  • 新增 stage 業務時:寫一個 Handler class + 註冊;不動 flow_engine
  • 跨模組 import 只在 DI container(合法位置)

未來反悔條件:若 registry 只剩 4 個 handler 永遠不擴張,可塌成直接 import;但前提是確認不再有其他 domain 接 flow_engine。

9.4 Loop back v1 簡化版

決議

項目 Spec 2 v1 落地
builtin-full-audit 範本 BPMN 保留 audit ←─ poam gateway 結構(範本層不動)
ExclusiveGateway condition ${pm_decides_loopback == true} placeholder
Runtime PM Dialog 二擇一 不做(留 Spec 3+)
poam on_complete 實際走 OscalAuditService.close_round 直接結案(走 gateway default flow)
顯示效果 看到 poam 完成 → AP closed;audit ←─ poam 路徑 BPMN 圖上可見但 runtime 不會走

為什麼:避免 Spec 2 範圍爆炸;客戶實際遇到 loop back 需求時再加 Dialog。既有 close_round 邏輯穩定可用。

未來反悔條件:客戶要求多輪 audit iteration 時,加:

  • FE:poam Banner 點「完成改善」前先跳 Dialog
  • BE:stage_advance 接受 loopback_decision: 'close' | 'reaudit' param,據此設 BPMN process variable

9.5 AP 建立後預設停在 planning UserTask

決議

  • AP 建立流程結尾 同步啟動 workflow_execution(依 flow_template_snapshot_uid clone 範本 → 建立 execution → 推進到第一個 UserTask)
  • 第一個 UserTask = planning stage(builtin 3 個範本第一個 UserTask 都是 planning)
  • AP.status 維持 preparing 直到 user 按 Banner「啟動專案」
  • planning on_complete → OscalAuditService.activate → AP.status: preparingactive

為什麼:與既有 4 個推進 API + spec 1 設計一致;不引入新 AP.status。

9.6 launch_new_round 範本預設值 — 專案層級 default

決議

  • Spec 2 範圍compliance.assessment_plan_extensions.flow_template_snapshot_uid 預留欄位; AP 建立時 hardcode 套用「完整稽核流程」builtin 範本(Spec 2 沒 UI 讓 user 選)
  • Spec 3 範圍
    • compliance.project_extensionsdefault_flow_template_uid 欄位
    • 建專案時選範本 → 存 project default
    • 建 AP 時自動帶入 project default,可在 wizard override
    • launch_new_round 時沿用上一輪的 flow_template_snapshot_uid(不重選,與 OSCAL 控制項結構 clone 邏輯一致)

為什麼:兼顧連貫與彈性;Spec 2 不負擔 wizard 設計。

9.7 Error code 序號(caveat)

既有 GRC_412010 已被 GRC_AP_NOT_CLOSED 佔用,Spec 2 編號往後挪。

用途 編號 訊息
Stage 推進前置條件未滿足 GRC_412021 「{reason_i18n_key}」(動態 i18n key 透過 context fill)
使用者不具備此 stage 主要角色 GRC_403050 「使用者不具備此階段推進權限」
BPMN 無下一個節點 / 推進邏輯異常 GRC_400050 「流程推進失敗:找不到下一節點」
AP 未綁定 workflow_execution GRC_404029 「稽核計畫未綁定流程實例」
Stage 推進 handler key 未註冊 GRC_400051 「stage handler 未註冊」
Stage object 未綁定到 BPMN UserTask GRC_400052 「BPMN UserTask 未綁定階段物件」

新增 6 個 error code 寫入 common/code/grc_error_code.py


§10

十、Reconciliation — 實作偏離設計的紀錄(Phase B/D smoke 期間累積,2026-05-13)

本段紀錄 plan v1 → 實作落地過程的偏差與成因,給未來讀者保留決策軌跡。 對應 issue docs/issues/resolved/2026-05-13-spec2-step22-snapshot-pattern.md

  • changelog docs/changelog/2026-05-13-fix-spec2-step22-snapshot-and-corrections.md

10.1 Handler key 命名移除 namespace 前綴

Plan v1oscal.planning.activate / oscal.task_execution.launch_audit實際:seed 為 activate_project / launch_audit / confirm_audit / close_round(無前綴) 為何:BE Phase B.1 落地時 stage_objects.complete_handler_key 直接用 service method 名稱,FE 也不分辨,namespace 前綴沒實質作用。

10.2 flow_templates 沒 stable code 欄位 → 用 name lookup

Plan v1flow_template_app_service.get_by_code("builtin-full-audit") 實際flow_template_domain_service.get_default_main_workflow_template() 內 hardcode name = "完整稽核流程",透過 get_builtin_by_name為何:spec 1 flow_templates 表沒 code 欄位,加 column 屬於 spec 1 schema 變動超出 spec 2 範圍。單點 hardcode 在 domain service,未來加 code 欄位後改一行即可。

10.3 complete_main_workflow_job 平行 method — 不重用既有 complete_job

Plan v1:直接 call engine 既有 complete_job(workflow_execution_uid, job_id, ...) 實際:主專案 workflow_execution_service.py 加新 method complete_main_workflow_job 為何:既有 complete_job 假設 workflow_execution 對應到單一 AP task(透過 assessment_plan_task_workflow_execution_mapping),會跑 task-level notification / project status update 等副作用。Main workflow 是 AP-level 不在 mapping 內 → 必須跳過那些副作用。

10.4 EndEvent handler 設計改為「handler 為 single source of truth」

Plan v1:BPMN engine _on_end_event_reachedExtAssessmentPlanRepoImpl 寫 raw SQL UPDATE AP.status='closed' 實際:刪除 ExtAssessmentPlanRepoImpl + _on_end_event_reached 不動 AP 狀態,AP 結案完全由 confirm_audit handler 處理 為何:避免「兩條路徑同時改 AP 狀態」造成幽靈轉換;handler 是 single source of truth。Plan §B.6 已落地。

10.5 Plan §D.1 view 路徑寫錯 — 啟動專案按鈕在 ProjectPlanningView 不在 ProjectSettingsView

Plan v1src/views/grc/ProjectSettingsView.vue 拆「啟動專案」按鈕 實際:實際按鈕在 src/views/project/ProjectPlanningView.vue:1372 為何:ProjectSettingsView 是專案基本資料編輯頁,沒有 launch 按鈕;plan 文件落地前該驗 grep。Phase D handoff prompt 已修正。

10.6 Snapshot pattern — Step 22 範本表 bridge

Plan v1(§B.5)start_workflow_execution(template_id=spec1_template.id, ...),假設 spec1 compliance.flow_templates 跟 engine public.workflow_templates 雙表 id 可直接互通 實際:兩表完全獨立(min engine id = 7247,spec1 id = 1-6)→ AttributeError。改為 per-AP snapshot pattern:建 AP 時 clone master 內容到 engine 表產生新 row(凍結),engine row 的 source_template_uid 記回 master uid 溯源 為何

  • 合規 traceability:「這個 AP 用什麼版本 workflow 稽核」要永遠回得出來
  • Admin 編輯 master 不影響 in-flight AP(snapshot 凍結)
  • ape.flow_template_snapshot_uid 欄位名本來就是 snapshot 語意(spec 設計時想好的,實作偷工)

詳見 docs/analysis/2026-05-13-flow-template-snapshot-pattern.md

10.7 主專案 workflow_execution_service.py fork pattern — 改套件對 spec 2 路徑無效

Plan v1:改 engine 套件 start_workflow_execution 就 done 實際:主專案 app/flow_engine/service/workflow_execution_service.py 是 fork-and-extend(14 個 method 跟套件同名 fork,加 17 個額外 method),Step 22 走的是主專案 wrapper 不是套件。改套件對 spec 2 無效,要改兩處為何:spec 1 / 既有 audit lifecycle 設計時對 engine 加了一堆業務客製(notification / project status / AP-task mapping),fork 出來自己管。長期 tech debt(建議重構為「繼承套件 class + override」),本 spec 不處理。

10.8 Engine start_workflow_execution 假設 main process 只放 CallActivity

Plan v1:相信 engine start_workflow_execution 會 instantiate 第一個 UserTask 實際:engine 原邏輯只處理 sub_workflow CallActivity 的 first jobs;「完整稽核流程」master BPMN 把 UserTask 直接寫在 main process(非 CallActivity 結構)→ engine 沒 instantiate 任何 UserTask 為何:BPMN 標準允許 main process 內直接放 UserTask(線性 4-stage),engine 既有實作太特殊化(assume 只放 CallActivity)。套件層 fix 77bfc83 補 main process UserTask 邏輯作為 engine 增強;主專案 wrapper 也補同樣邏輯(commit f1f83bc)真正生效這個。

10.9 BPMN gateway condition_param Camunda format mismatch

Plan v1condition_param={"has_findings": True} 丟給 engine 實際:engine _parse_condition_param_to_string 把 Python True 字串化成 "has_findings == True",但 BPMN conditionExpression 是 ${has_findings == true}(Camunda 標準,${} 包覆 + lowercase)— exact string match 永遠 fail,gateway 都走 default flow(end) 為何:engine 既有 string-match 設計脆弱(不是 eval expression),spec 2 是第一個用 gateway 的 use case 才暴出來。套件層 fix 02713bc 改輸出 Camunda 標準。

10.10 close_round 接管「發起覆核」副作用

Plan v1:close_round 只設 ar_data.completed_at,AP 維持 remediation,等 user 點「發起覆核」按鈕(call launch_audit force=True)→ AP→auditing + 建新 round 實際:Banner 拆掉「發起覆核」按鈕(Phase D.4 commit c535af6),close_round handler 必須接管覆核模式 4 個副作用:建新 ar_data run_no+1 + 複製 ar_controls(POA&M 對應 reset + 其他繼承)+ AP→auditing + 回傳 status="auditing" 為何:Banner 「完成改善」是唯一 user 推進入口,close_round 必須對齊 BPMN poam → audit loop back 的語意。Commit 353e9fc

10.11 Planning 加 all_tasks_assigned precondition(plan 沒列)

Plan v1:Planning stage precondition_key = NULL(無檢查) 實際:補 all_tasks_assigned — 啟動專案前要先把任務全指派 為何:合規 use case 上「啟動 = 任務該都已分派」是基本要求;plan 設計時遺漏(task_execution / audit / poam 都有檢查獨缺 planning)。Commit 2538e74

10.12 Banner 在多 view 需要 sub-action 觸發 refresh

Plan v1:Banner onMounted + props 變更時 fetchStageInfo 即可 實際:sub-action(verdict 變更 / POA&M 結案)會改 precondition state,但 Banner 不知道 → 按鈕保持 disabled,要重整頁才會 enable 為何:Banner 用 defineExpose 暴露 fetchStageInfo,parent view 用 ref 在 sub-action callback 內手動 trigger。AuditReviewView (98d5aac) / PoamView (f875aa3) 加 ref pattern。ProjectPlanningView / ProjectAuditorOverview 目前不需要(無 sub-action 改 precondition),未來補 Planning all_tasks_assigned 跨任務頁面 refresh 時要補同樣 pattern。

10.13 Banner 推進按鈕加二次確認 ConfirmDialog

Plan v1:只有 manager force / handler warning 場景開 ConfirmDialog,正常 path 直接 advance 實際:所有 advance path 都先彈 ConfirmDialog(commit 72fa8b7為何:階段推進不可復原,正常 path 也該二次確認避免誤觸發。

10.14 Banner 結案後整塊隱藏

Plan v1:workflow 結案後 Banner 顯示「目前階段 —」空殼 row 實際:root div 加 v-if="!isFlowClosed" 整塊不渲染(commit 899bc77為何:UX 上結案後不該有空殼 banner 佔位。

10.15 角色 gate — Overview 「進入稽核」/「改善計畫」按鈕

Plan v1:plan 沒明確規範角色 gate(只在 Banner 推進按鈕走 main_roles) 實際:Overview 內:

  • 「進入稽核」(auditing) 只 manager 可見,其他角色看「檢視稽核結果」(commit f8233a0
  • 「改善計畫」(remediation/closed) 只 manager 可見(commit 1975d1a為何:spec 2 預設「推進按鈕」是主要角色專屬,但 Overview 的導航按鈕 plan 沒提,user 測試時點出來。btn_view_audit 條件擴大成包含 auditing 階段給非 manager 用。

10.16 dev path mode 而不是套件 publish

Plan v1:套件改動 bump 0.0.27 → 0.0.28 + 推 Nexus + 主專案 poetry update 實際:dev 階段用 pip install -e editable + pyproject.toml 改 path dep,不 bump version;feature 完成 user 指示才一次性發版 為何:dev 期間多輪調整,每次 bump + 推 Nexus + poetry update 成本太高(poetry 卡 google-ai SAT 解析數十分鐘)。CLAUDE.md 已加「外部套件異動規範」段落化。


§11

十一、文件版本

版本 日期 變更
v1 2026-05-12 初版(§一 ~ §八)
v2 2026-05-12 加 §九 決議段(7 個議題)
v3 2026-05-13 加 §十 Reconciliation(16 條落地偏差)