狀態:v2 含 §九 決議段(A1~A4 + B1~B2 + error code caveat) 整體脈絡:
../README.md更新日期:2026-05-12
把目前 5 個既有 view(settings / overview / audit / poam / 待辦任務)改造成「BPMN UserTask 可推進」的階段;提供 stage 推進 API;UI 收口到 Banner。
來源:requirement.md §「想法(粗略)」§2、§3 末尾「上方功能按鈕 / Complete 按鈕 / 文案隨階段變化」
FlowPhaseBanner FE 元件v-if 邏輯每個 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)各自掛。
| 角色 | 在此階段做什麼 |
|---|---|
manager (PM) |
看 task 完成進度 + 看 reviewer 的 review_mark 進度,自行決定何時按 Banner「啟動稽核」推進 |
reviewer |
對控制項 / AO 用既有 review_mark 機制標記「審閱通過」(既有 API:POST /grc/.../controls/<uid>/review-mark、POST /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。
GET /flow-engine/stage-objects全部於 2026-05-12 收斂;具體決議見 §九。
對應 §八 5 條 + README §四 跨 spec 2 條,Phase 3 plan 前的最終收斂。
決議:新建 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;為什麼:
compliance.project_extensions pattern 完全一致,新開發者好理解未來反悔條件:若 jedi-oscal owner 同意納編,可移欄位回主表(migration 直接搬欄位 + drop ext)。
淘汰的暫態 patch:未追蹤的 domain/flow_engine/repository/i_ext_assessment_plan_repo.py 與 infra/.../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 刪除。
決議: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: ...為什麼:
force=false 設計一致,FE 不需要學新 pattern決議:採乾淨 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 業務)未來反悔條件:若 registry 只剩 4 個 handler 永遠不擴張,可塌成直接 import;但前提是確認不再有其他 domain 接 flow_engine。
決議:
| 項目 | 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 時,加:
stage_advance 接受 loopback_decision: 'close' | 'reaudit' param,據此設 BPMN process variable決議:
flow_template_snapshot_uid clone 範本 → 建立 execution → 推進到第一個 UserTask)planning stage(builtin 3 個範本第一個 UserTask 都是 planning)preparing 直到 user 按 Banner「啟動專案」OscalAuditService.activate → AP.status: preparing → active為什麼:與既有 4 個推進 API + spec 1 設計一致;不引入新 AP.status。
決議:
compliance.assessment_plan_extensions.flow_template_snapshot_uid 預留欄位; AP 建立時 hardcode 套用「完整稽核流程」builtin 範本(Spec 2 沒 UI 讓 user 選)compliance.project_extensions 加 default_flow_template_uid 欄位flow_template_snapshot_uid(不重選,與 OSCAL 控制項結構 clone 邏輯一致)為什麼:兼顧連貫與彈性;Spec 2 不負擔 wizard 設計。
既有
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。
本段紀錄 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。
Plan v1:oscal.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 前綴沒實質作用。
flow_templates 沒 stable code 欄位 → 用 name lookupPlan v1:flow_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 欄位後改一行即可。
complete_main_workflow_job 平行 method — 不重用既有 complete_jobPlan 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 內 → 必須跳過那些副作用。
Plan v1:BPMN engine _on_end_event_reached 內 ExtAssessmentPlanRepoImpl 寫 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 已落地。
Plan v1:src/views/grc/ProjectSettingsView.vue 拆「啟動專案」按鈕 實際:實際按鈕在 src/views/project/ProjectPlanningView.vue:1372 為何:ProjectSettingsView 是專案基本資料編輯頁,沒有 launch 按鈕;plan 文件落地前該驗 grep。Phase D handoff prompt 已修正。
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 溯源 為何:
ape.flow_template_snapshot_uid 欄位名本來就是 snapshot 語意(spec 設計時想好的,實作偷工)詳見 docs/analysis/2026-05-13-flow-template-snapshot-pattern.md。
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 不處理。
start_workflow_execution 假設 main process 只放 CallActivityPlan 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)真正生效這個。
Plan v1:condition_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 標準。
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。
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。
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。
Plan v1:只有 manager force / handler warning 場景開 ConfirmDialog,正常 path 直接 advance 實際:所有 advance path 都先彈 ConfirmDialog(commit 72fa8b7) 為何:階段推進不可復原,正常 path 也該二次確認避免誤觸發。
Plan v1:workflow 結案後 Banner 顯示「目前階段 —」空殼 row 實際:root div 加 v-if="!isFlowClosed" 整塊不渲染(commit 899bc77) 為何:UX 上結案後不該有空殼 banner 佔位。
Plan v1:plan 沒明確規範角色 gate(只在 Banner 推進按鈕走 main_roles) 實際:Overview 內:
f8233a0)1975d1a) 為何:spec 2 預設「推進按鈕」是主要角色專屬,但 Overview 的導航按鈕 plan 沒提,user 測試時點出來。btn_view_audit 條件擴大成包含 auditing 階段給非 manager 用。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 已加「外部套件異動規範」段落化。
| 版本 | 日期 | 變更 |
|---|---|---|
| v1 | 2026-05-12 | 初版(§一 ~ §八) |
| v2 | 2026-05-12 | 加 §九 決議段(7 個議題) |
| v3 | 2026-05-13 | 加 §十 Reconciliation(16 條落地偏差) |