狀態:v1 — brainstorm 收斂版,待 review 整體脈絡:
../README.md依賴:Spec 1 / Spec 2 / Spec 3(皆已 ship) 建立日期:2026-05-14
兩個獨立但相關的子題:
reviewer 角色主導的人工過審節點;不過時可由範本指定退回到任一上游 UserTask;過/退回都可附留言。兩題共用同一份 spec 因 schema 動的是同一張 compliance.stage_objects 表、driver code 都在 FlowTemplateAppService._validate_bpmn 與 StageAdvanceService.advance_stage。
| 來源 | 重點 |
|---|---|
docs/draft_requirement_spec/project-flow-engine-integrate/requirement.md |
requirement.md 沒明列 review 階段,這是後續加的訴求 |
| 本次 brainstorm Q1 | 「插入一個審核工作節點,不合格可退回上一階段,可留言更好」 |
| 本次 brainstorm Q2 | 「流程相依性問題 — 例如第一步就是缺失改善是不合理的;範本存檔時要檢查方向性 + 防呆」 |
| # | 主題 | 決議 |
|---|---|---|
| Q1 | 審核形態 | 新增第 5 個 stage_object review(kind=stateful,主要角色 reviewer) |
| Q2 | 退回 granularity | 由 BPMN 範本決定(在 review UserTask 後接 ExclusiveGateway,reject sequenceFlow 可指向任一上游 UserTask) |
| Q3 | 留言 | Reuse 既有 JobCommentService;所有 stage advance 共用 — 不只 review reject |
| Q4 | 驗證規則來源 | stage_object 加上下游規則欄位(declarative);validator 讀表,新增 stage 不改 code |
| Q5 | Reverse edge 處理 | Gateway 的 reverse edge 不受 allowed_successors 拘束 — 用 condition 含 reject 或 extension property reverse=true 識別 |
| Q6 | 驗證時機 | BE create/update 反 400 + FE editor inline warning(FE 拖 sequenceFlow 時呼叫 /validate preview) |
| Q7 | 預設規則寬鬆度 | 全部 stage 都 can_end=true(容許「練習設定」),但 can_start 限 planning(避開 start → poam 等不合理開頭) |
| 公開 1 | builtin 範本配置 | 並列:保留既有 builtin-full-audit,新增 builtin-full-audit-with-review 為另一可選範本 |
| 公開 2 | review approve precondition | 不卡:reviewer 自由判斷,沿用 Spec 2 §五 v1 不強制 gate |
| 公開 3 | comment 約束 | BE 強制:decision=reject AND comment IS NULL/empty → 400 |
A — 審核階段
compliance.stage_objects seed 新增 review 一列StageAdvanceService.advance_stage 接受 decision: 'approve' | 'reject' + comment payload_build_condition_param 加 review 分支:{"decision": "approve" | "reject"}ReviewDecisionHandler(不動 AP.status;純粹 dispatch BPMN 推進方向)${decision == 'reject'} 條件式(既有 condition_param 機制,不改套件)decision=reject 必填 comment(webargs schema layer)B — 全 stage 共用 comment
StageAdvanceRequestSchema 加 optional comment 欄位(reject 場景 required,由 marshmallow validates schema 控制)StageAdvanceService.advance_stage 推進 BPMN 後同步 JobCommentService.add_comment(job_uid=當前 PROCESSING job_execution.uid, ...)C — 流程方向性驗證
compliance.stage_objects 加 4 個欄位:can_start BOOLEAN、can_end BOOLEAN、allowed_predecessors JSONB、allowed_successors JSONBFlowTemplateAppService._validate_bpmn_topology() 演算法(7 步檢查,見 §七)POST /flow-engine/flow-templates/validate(純驗證不存檔)D — Builtin 範本
builtin-full-audit-with-review(並列於既有 builtin-full-audit)all_controls_reviewed precondition(公開議題 2 決議不卡)review_mark API 行為改動(reviewer 仍可逐控制項標記,純資訊性)compliance.stage_objects 加欄位ALTER TABLE compliance.stage_objects
ADD COLUMN can_start BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN can_end BOOLEAN NOT NULL DEFAULT TRUE,
ADD COLUMN allowed_predecessors JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN allowed_successors JSONB NOT NULL DEFAULT '[]'::jsonb;預設值意涵:新加 stage 預設「不能當 start、可以 end、上下游皆空(→ 必須明確 seed 才合法)」— 避免 builtin seed 漏設導致 validator silent pass。
-- planning:起點,自由銜接任意 stage
UPDATE compliance.stage_objects
SET can_start = TRUE,
can_end = TRUE,
allowed_predecessors = '[]'::jsonb,
allowed_successors = '["task_execution", "review", "audit", "poam"]'::jsonb
WHERE code = 'planning';
-- task_execution:通常承 planning 或 review reverse
UPDATE compliance.stage_objects
SET can_start = FALSE,
can_end = TRUE,
allowed_predecessors = '["planning", "review"]'::jsonb,
allowed_successors = '["audit", "review"]'::jsonb
WHERE code = 'task_execution';
-- review:v1 新加
INSERT INTO compliance.stage_objects (
code, name_i18n, kind, route_pattern,
complete_button_label_i18n, complete_handler_key, precondition_key,
default_main_roles, is_builtin, sort,
can_start, can_end, allowed_predecessors, allowed_successors
) VALUES (
'review',
'{"zh_Hant_TW": "審核", "en": "Review"}'::jsonb,
'stateful', NULL,
'{"zh_Hant_TW": "送出審核", "en": "Submit Review"}'::jsonb,
'review_decision', NULL,
'["reviewer"]'::jsonb, TRUE, 25,
FALSE, TRUE,
'["planning", "task_execution"]'::jsonb,
'["audit"]'::jsonb
) ON CONFLICT (code) DO NOTHING;
-- audit:承 task_execution / review / poam
UPDATE compliance.stage_objects
SET can_start = FALSE,
can_end = TRUE,
allowed_predecessors = '["task_execution", "review", "poam"]'::jsonb,
allowed_successors = '["poam"]'::jsonb
WHERE code = 'audit';
-- poam:純承 audit,forward 只能接 End;poam → audit 是 reverse(走 §七 B.3 例外)
UPDATE compliance.stage_objects
SET can_start = FALSE,
can_end = TRUE,
allowed_predecessors = '["audit"]'::jsonb,
allowed_successors = '[]'::jsonb
WHERE code = 'poam';規則表只列 forward:reverse edge(如 review reject → task_execution、poam → audit)不在 allowed_successors 內,靠 §七 B.3 reverse 例外規則放行。這讓規則表保持「合法 forward 拓樸」單一語意,validator 對 reverse 走獨立判斷分支。
整理過後的查詢矩陣:
| code | can_start | can_end | allowed_predecessors | allowed_successors(forward 唯一來源) |
|---|---|---|---|---|
| planning | ✓ | ✓ | — | task_execution, review, audit, poam |
| task_execution | ✗ | ✓ | planning, review | audit, review |
| review | ✗ | ✓ | planning, task_execution | audit |
| audit | ✗ | ✓ | task_execution, review, poam | poam |
| poam | ✗ | ✓ | audit | — |
task_execution.allowed_successors=["audit", "review"] 兩條 forward 路徑:
builtin-full-audit)builtin-full-audit-with-review)poam.allowed_successors=[] 配 poam.can_end=true 表達「poam 唯一 forward 是接 End;poam → audit 走 reverse」。
stage_object_predecessors / stage_object_successors 關聯表表達力更完整,但這份「規則表」量級在 N²(N=5~10)、語意上是 stage_object 的內在屬性、且很少獨立查詢,用 JSONB 集中放在同一列:
StageAdvanceRequestSchema 擴充class StageAdvanceRequestSchema(Schema):
project_uid = fields.UUID(required=True)
ap_uid = fields.UUID(required=True)
force = fields.Boolean(load_default=False)
# v1 新增
decision = fields.String(
load_default=None,
validate=validate.OneOf(["approve", "reject"]),
)
comment = fields.String(load_default=None)
@validates_schema
def _validate_reject_requires_comment(self, data, **kwargs):
# 公開議題 3:BE 強制
if data.get("decision") == "reject" and not (data.get("comment") or "").strip():
raise ValidationError(
{"comment": ["reject 必須附留言"]},
field_name="comment",
)非 review stage 送 decision 的處理(reviewer 反饋):schema 不額外驗證;StageAdvanceService.advance_stage 在 stage_code != "review" 時把 ctx["decision"] 忽略並 logger.warning("decision payload ignored for non-review stage %s", stage_code)。不 raise 400,因為這純粹是 caller 多送資料,不影響業務正確性;忽略 + log 比阻擋更友善。
StageAdvanceResponseSchema 加欄位(optional)current_stage_info 內含 current_stage.complete_button_label_i18n 已存在。review stage 的 reject 按鈕 label 由 FE 直接從 i18n bundle 取(key flowEngine.banner.review.reject,見 §10.4)— 不在 stage_objects 表加新欄位、不在 response 額外回,因為:
未來若多個 stage 需要 reject 按鈕,再考慮把 reject_button_label_i18n 加進 stage_objects 列。
POST /flow-engine/flow-templates/validatePOST /flow-engine/flow-templates/validate
{ "bpmn_xml": "<?xml ..." }
Response 200:
{
"valid": true,
"warnings": [] // 未來 lenient warning list 預留
}
Response 400(既有 envelope):
{
"code": 0,
"msg": "BPMN topology validation failed",
"data": {
"error_code": "GRC_400060",
"violations": [
{
"rule": "successor_not_allowed",
"reason_i18n_key": "flow_engine.validator.successor_not_allowed",
"context": {
"source_stage": "planning",
"target_stage": "poam",
"sequence_flow_id": "Flow_xx"
}
}
]
}
}
FlowTemplateAppService.create / .update 內部復用同一 validator function — 違規會 raise BadRequestError(GRC_FLOW_TEMPLATE_TOPOLOGY_INVALID) with error.context = {"violations": [...]} 攜帶細節。
_validate_bpmn(xml) ← 既有,保留
↓
_validate_bpmn_topology(util) ← v1 新增
↓ pass
return / persist
↓ fail
raise BadRequestError(GRC_FLOW_TEMPLATE_TOPOLOGY_INVALID,
data={"violations": [...]})
| # | 規則 | 違規 error reason_key |
|---|---|---|
| 1 | StartEvent 後第一個 UserTask 的 stage_object_code 對應 stage_object 的 can_start = TRUE |
start_stage_not_allowed |
| 2 | 每個 UserTask 的 stage_object_code 存在於 compliance.stage_objects 表 |
unknown_stage_code |
| 3 | 每條 forward UserTask→UserTask 的 sequenceFlow,target.stage_code ∈ source.allowed_successors(reverse edge 跳過此檢查,見 B.3) |
successor_not_allowed |
| 4 | 每條連到 EndEvent 的 sequenceFlow,來源 UserTask 對應 stage_object 的 can_end = TRUE |
end_stage_not_allowed |
| 5 | 每個 ExclusiveGateway 至少 1 條 outgoing 帶 conditionExpression(不允許全空 default-only) |
gateway_missing_condition |
| 6 | 每個 UserTask 從 StartEvent 走 forward edge 可達(BFS 時略過所有 reverse edge);且至少存在一條由 forward edge 組成的 StartEvent → EndEvent 路徑 | unreachable_node / no_end_path |
| 7 | 同一 stage_object_code 可重複出現多次(例:兩道 review)— 不擋;但若違反 §3 successor 規則仍會被擋 | (不擋) |
對每條 sequenceFlow(source=Gateway 或 UserTask、target=UserTask):
# Primary:extension property(穩定來源,FE editor 拖出 reject 流程時自動加上)
is_reverse_by_property =
sequenceFlow has <camunda:property name="reverse" value="true"/>
# Fallback:condition 字串嚴格 anchored regex(給手寫 / 舊範本兼容)
REVERSE_CONDITION_REGEX = re.compile(
r"^\s*\$\{\s*[A-Za-z_][A-Za-z_0-9]*\s*==\s*['\"]reject['\"]\s*\}\s*$"
)
is_reverse_by_condition =
REVERSE_CONDITION_REGEX.fullmatch(sequenceFlow.conditionExpression or "")
is_reverse = is_reverse_by_property OR bool(is_reverse_by_condition)
if is_reverse:
skip §7.2 step 3 successor check
skip §7.2 step 6 reachability 時把這條 edge 視為不存在
設計選擇:
<camunda:property name="reverse" value="true"/>;穩定、不靠字串 match${var == 'reject'}(exact)— 變數名 decision / review_status / verdict 都可,但 quote 後必須緊接 }(含可選空白)${review_status == 'rejected_in_form'}(false positive 防範)→ quote 內必須完全是 reject${decision != 'approve'}(false negative 接受)→ 不靠 condition 識別,需 FE 補 extension property${decision == 'Reject'}(大小寫敏感)→ 必須小寫 reject,rationale:BPMN best practice 是 lowercase enum${ a == 'reject' && b == 'foo' }(複合條件)→ 不 match,建議改用 extension property未來反悔條件:若客戶提複合 reject condition 需求,加 stage_object.reverse_condition_keywords JSONB 欄位讓 user 自訂;或乾脆把 condition match 邏輯改成「在 BPMN engine 端 evaluate 條件值」(成本較高)。
ExclusiveGateway 的核心是「分支」。若所有 outgoing 都無 condition,runtime 永遠走 default flow → gateway 形同 sequence pass-through,等同無意義節點且暗示範本設計錯誤。直接 400 反而給 user 明確 signal。
| 範本 | 過 validator? | 需要動的事 |
|---|---|---|
builtin-full-audit |
需 patch | Flow_poam_audit (poam → audit) sequenceFlow 需加 <camunda:property name="reverse" value="true"/> 才被識別為 reverse;不加會違反 poam.allowed_successors=[] |
builtin-internal-check |
✓ pass | task_execution.can_end=true,無 reverse edge |
builtin-self-assessment |
✓ pass | audit.can_end=true,無 reverse edge |
builtin-full-audit-with-review(v1 新增) |
✓ pass | 建範本時直接加好 Flow_review_reject + Flow_poam_audit 兩條 reverse property |
自訂範本 backfill(reviewer 提醒):Spec 1 已 ship 三週,使用者可能已 duplicate 過 builtin-full-audit 成自訂範本(XML 不含 reverse property)。Migration 必須同時:
compliance.flow_templates where is_builtin=false,若 bpmn_xml 含 Flow_poam_audit (sourceRef=UserTask_poam && targetRef=UserTask_audit),補相同 extension property掃描 + 修改用 lxml / xmltodict 處理,dry-run 先列要動的 row讓 user 確認後再執行(避免炸壞客製範本)。實作放 M4 milestone。
ReviewDecisionHandlerapp/grc/service/oscal_stage_handlers.py 加:
class ReviewDecisionHandler(IStageCompletionHandler):
"""review stage 推進 — 純 dispatch decision,不動 AP.status。
- decision=approve:BPMN 走 default forward;AP.status 由下個 stage 的 handler 接管
- decision=reject:BPMN 走 reject sequenceFlow,回到上游 UserTask;
AP.status 保持 active(既有,task_execution stage 對應的 status)
不做業務 precondition(公開議題 2 決議 reviewer 自由判斷)。
"""
@property
def key(self) -> str:
return "review_decision"
def execute(self, ap_uid, project_uid, user_id, curr_user, ctx) -> dict:
decision = (ctx or {}).get("decision") or "approve"
return {"decision": decision, "warning": None}註冊在 di_containers/grc/grc_containers.py 啟動時加入 stage_registry。
StageAdvanceService.advance_stage 接 decision/comment payload@transaction
def advance_stage(
self,
project_uid: str,
ap_uid: str,
user_id: int,
curr_user: str,
force: bool = False,
decision: Optional[str] = None, # v1 新增
comment: Optional[str] = None, # v1 新增
ctx: Optional[dict] = None,
) -> dict:
ctx = dict(ctx or {})
ctx.setdefault("force", force)
if decision:
ctx["decision"] = decision
... 既有 stage / role / precondition 驗證 ...
handler_result = handler.execute(..., ctx=ctx)
... 既有 warning short-circuit ...
# condition_param 組裝:review stage 多接 decision
condition_param = self._build_condition_param(
stage_code=stage_object.code,
handler_result=handler_result,
)
self._workflow_execution_service.complete_main_workflow_job(
workflow_execution_uid=wf_ctx["workflow_execution_uid"],
job_id=curr_job_template.id,
user=curr_user,
comment=comment or f"stage advance: {stage_object.code}",
condition_param=condition_param,
user_nickname=ctx.get("user_nickname"),
)
# v1 新增:寫 GRC JobComment(reuse 既有 service)
# CRITICAL(已驗證):JobCommentService.add_comment 接受的 job_uid 是 JobExecution.uid (UUID),
# 不是 BPMN template element id(如 "UserTask_review")。
# curr_job_template.id 是 BPMN element id,不能直接傳;必須先查 PROCESSING JobExecution 拿 uid。
if comment:
try:
job_execution = self._job_execution_domain_service.get_job_execution(
JobExecutionQueryEntity(
workflow_execution_id=wf_ctx["workflow_execution_id"],
template_job_id=curr_job_template.id,
# 注意:此時 BPMN 推進已完成,curr_job_execution.status 已是 COMPLETED;
# 不能用 status=PROCESSING filter,改用 template_job_id 取最新一筆
)
)
if job_execution is None:
logger.warning(
"JobExecution not found for template_job_id=%s — skip JobComment write",
curr_job_template.id,
)
else:
self._job_comment_service.add_comment(
job_uid=str(job_execution.uid), # ← UUID,不是 BPMN element id
content=comment,
user_id=user_id,
author_nickname=ctx.get("user_nickname") or curr_user,
)
except Exception:
logger.warning("write GRC JobComment failed (non-blocking)", exc_info=True)
return {...}lenient 寫 comment:BPMN engine 端 complete_main_workflow_job 已寫 element_variable 一份(既有行為)作為 BPMN-side 元資料;GRC JobComment 是面向 user 的留言層(接得到 /job-executions/<uid>/comments API)。雙寫各有用途;GRC 端失敗 log warning 不擋 advance。
Order of operations:JobExecution 查詢必須在 complete_main_workflow_job 之後做,因為當前 job 已從 PROCESSING → COMPLETED;用 template_job_id filter 取得對應 row。advance 本身是 @transaction scope,這幾個 query/insert 共用同一 transaction。
_build_condition_param 加 review 分支def _build_condition_param(self, stage_code, handler_result) -> Optional[dict]:
if stage_code == "audit": # 既有
...
return {"has_findings": new_status == "remediation"}
if stage_code == "review": # v1 新增
if isinstance(handler_result, dict):
decision = handler_result.get("decision", "approve")
return {"decision": decision}
return {"decision": "approve"}
return Nonebuiltin-full-audit-with-reviewStartEvent
↓
UserTask_planning (main_role=manager)
↓
UserTask_task_execution (main_role=manager)
↓
UserTask_review (main_role=reviewer) ← v1 新加
↓
Gateway_review_decision (default=Flow_review_forward)
├─ Flow_review_forward (default, approve) → UserTask_audit
└─ Flow_review_reject (${decision=='reject'}, reverse=true) → UserTask_task_execution
↑(回到執行)
UserTask_audit
↓
Gateway_has_findings (default=Flow_gateway_end)
├─ Flow_gateway_poam (${has_findings == true}) → UserTask_poam
└─ Flow_gateway_end (default, 無缺失) → EndEvent
UserTask_poam
↓
Flow_poam_audit (reverse=true) → UserTask_audit
為什麼並列而非取代既有 builtin-full-audit(公開議題 1):
Spec 2 與 Spec 4 都會在 BPMN 內看到 reverse edge,但 runtime 行為不同 — 設計上必須對齊使用者心智模型:
| 場景 | BPMN 結構 | Runtime 行為 | 是否新建 round |
|---|---|---|---|
| Spec 2:poam → audit | Flow_poam_audit (reverse=true) |
close_round handler 接管:建新 ar_data run_no+1、複製 ar_controls、AP.status → auditing、BPMN 推 audit UserTask |
✓ 新 round(覆核模式) |
| Spec 4:review reject → task_execution | Flow_review_reject (reverse=true, condition=${decision == 'reject'}) |
ReviewDecisionHandler 純 dispatch decision;BPMN 推回 task_execution UserTask;不建新 round、AP.status 不動 |
✗ 不建 round |
為什麼設計不同:
Long-term reconsideration(紀錄供未來 reference):Spec 2 §9.4「Loop back v1 簡化版」原本希望 poam → audit 加 PM Dialog 二擇一(close vs reaudit);Spec 4 引入真 reverse 機制後,若客戶要求 poam 也支援「純 BPMN reverse 不建 round」選項,可考慮在 poam stage advance 也加 decision payload(reject_to_audit / close_round)— 但這要等 Spec 5+ 再評估,本 spec 不動 poam 行為。
| 場景 | UI 行為 |
|---|---|
| current_stage = review | 主按鈕「送出審核(approve)」+ 次按鈕「退回(reject)」 |
| 其他 stage | 既有主按鈕(無變動) |
雙按鈕都先彈 ConfirmDialog,內含 <Textarea v-model="comment" />:
editor 內 sequenceFlow drag-end 事件 → debounce 500ms → POST /flow-engine/flow-templates/validate:
存檔仍由 BE 決定(不送出 / 送出 → BE 再驗一次當作守門員)。
stage_object_code=review + main_role=reviewer新增 key:
flowEngine:
validator:
successor_not_allowed: "「{source_stage}」階段後不可接「{target_stage}」階段"
start_stage_not_allowed: "流程不可從「{stage}」階段開始"
end_stage_not_allowed: "「{stage}」階段不可直接結案"
unknown_stage_code: "未知的階段代碼:{code}"
gateway_missing_condition: "決策節點「{gateway_id}」缺少分支條件"
unreachable_node: "節點「{node_id}」無法從開始點抵達"
no_end_path: "找不到從開始到結束的完整路徑"
banner:
review:
submit: "送出審核"
reject: "退回"
rejectReasonRequired: "請說明退回原因(必填)"
commentOptional: "可選填說明"| Code | 訊息 | 用途 |
|---|---|---|
GRC_FLOW_TEMPLATE_TOPOLOGY_INVALID (GRC_400060) |
範本流程結構不合法 | validator 違規總碼,response.data 帶 violations array |
GRC_STAGE_REVIEW_REJECT_REQUIRES_COMMENT (GRC_400061) |
退回審核必須附留言 | 由 schema validator 翻譯成 400;envelope.msg 顯示 |
GRC_STAGE_REVIEW_DECISION_INVALID (GRC_400062) |
decision 值必須為 approve 或 reject | schema validator |
既有 GRC_FLOW_TEMPLATE_BPMN_INVALID (400040) 保留,覆蓋「XML 格式錯誤、必要元素缺漏」這類「結構性」錯誤;topology 違規用新 code 分離,方便 FE 對應不同 UI(inline 紅框 vs 全域 toast)。
Validator unit test(純函式,無 DB):
| Case | 預期 |
|---|---|
| 既有 builtin-full-audit + reverse property | pass |
| 既有 builtin-internal-check | pass |
| 既有 builtin-self-assessment | pass |
| 新 builtin-full-audit-with-review | pass |
| 「start → poam」(poam.can_start=false) | fail start_stage_not_allowed |
| 「planning → poam」(poam ∉ planning.allowed_successors) | fail successor_not_allowed |
| 「task_execution → end」 | pass(task_execution.can_end=true) |
| 「review → end」 | pass(review.can_end=true) |
| 「planning → end」(純練習設定) | pass(planning.can_end=true) |
| Gateway 全 default | fail gateway_missing_condition |
| 孤立 UserTask(不可達) | fail unreachable_node |
| 純循環 A → B → A 兩條都標 reverse(無 end path) | fail no_end_path |
| 未知 stage_code | fail unknown_stage_code |
Reverse edge 識別 regex 邊界 case(§7.3 對應):
| Condition 字串 | 預期 is_reverse | 理由 |
|---|---|---|
${decision == 'reject'} |
✓ | 標準格式 |
${review_status == 'reject'} |
✓ | 變數名不限 decision |
${ decision == 'reject' } |
✓ | 內部空白容許 |
${review_status == 'rejected_in_form'} |
✗ | quote 內必須完全是 reject |
${decision == 'Reject'} |
✗ | 大小寫敏感 |
${decision == "reject"} |
✓ | 雙引號等價 |
${decision != 'approve'} |
✗ | 不靠語意推導;FE 補 extension property |
${a == 'reject' && b == 'foo'} |
✗ | 複合條件不 match;FE 補 extension property |
同時帶 extension reverse=true + condition |
✓ | 任一條成立即可(不重複算) |
Integration test(DB):
create_flow_template 帶不合法 BPMN → 400 with violationsupdate_flow_template 同上POST /flow-engine/flow-templates/validate happy path + 400 pathadvance_stage decision=approve + comment → BPMN 走 default flow + JobComment 寫入advance_stage decision=reject + comment → BPMN 走 reject flow + JobComment 寫入advance_stage decision=reject 無 comment → 400 from schema validatoradvance_stage 非 review stage 帶 decision → ignore decision(手動忽略,不擋)□ 建專案選範本「完整稽核含審核」(builtin-full-audit-with-review)
→ AP1 第一輪走 planning → task_execution → review
□ reviewer 在 review banner 按「退回」+ 寫退回原因
→ BPMN 回到 task_execution UserTask
→ JobComment 列表可見退回原因
□ PM 重新走 task_execution → review,reviewer 「送出審核」approve(comment optional)
→ BPMN 走到 audit UserTask
□ 走完 audit → 有缺失 → poam → audit → 無缺失 → End
□ 同專案 launch_new_round 切回 builtin-full-audit(無 review)
→ AP2 走原本 4-stage 流程,行為與 Spec 2 落地一致
□ 範本管理頁編輯:拖 sequenceFlow 從 Gateway 指向 poam(違反 audit.allowed_successors)
→ 側邊欄即時警告 + 畫布紅框
□ 強制存檔 → BE 回 400 with violations
| M | 範圍 | 提交策略 | 大小估 |
|---|---|---|---|
| M0 | Pre-flight verify:grep 既有 builtin XML 是否含其他 reverse 模式;確認 complete_main_workflow_job 不需動 |
純檢查 | XS |
| M1 | BE schema migration:compliance.stage_objects 加 4 欄 + seed review row + 4 stage 規則回填 |
1 commit | S |
| M2 | BE _validate_bpmn_topology 演算法 + 6 error code + violations payload |
1 commit | M |
| M3 | BE POST /flow-engine/flow-templates/validate endpoint |
1 commit | S |
| M4 | BE seed builtin-full-audit-with-review BPMN + 既有 builtin-full-audit patch reverse extension property |
1 commit | S |
| M5 | BE StageAdvanceRequestSchema 加 decision/comment 欄位 + reject required comment validator |
1 commit | S |
| M6 | BE ReviewDecisionHandler + StageAdvanceService 接 decision/comment + _build_condition_param review 分支 + JobComment 整合 |
1 commit | M |
| M7 | BE error code / unit + integration test | 1 commit | M |
| M8 | FE BPMN editor palette + inline warning(呼叫 /validate) |
1 commit | M |
| M9 | FE Banner approve/reject 雙按鈕 + ConfirmDialog comment textarea | 1 commit | M |
| M10 | FE 全 stage advance ConfirmDialog 加 optional comment textarea | 1 commit | S |
| M11 | Manual smoke + 跨 repo e2e + changelog 收尾 | — | — |
M1→M2 順序:schema 必須先動(M1)才能讓 validator(M2)能讀 stage_object 規則,這個 dep 是硬的;測試與 endpoint(M3)建在 validator 之上。M4 builtin patch 放 M3 後因 validator 完成才有底氣 patch(patch 後跑 M2 validator 自我驗證)。
| 依賴 | 提供方 | 狀態 |
|---|---|---|
compliance.stage_objects 表 + seed |
Spec 1 | ✅ 已 ship |
compliance.flow_templates + _validate_bpmn |
Spec 1 | ✅ 已 ship |
StageAdvanceService / IStageCompletionHandler registry |
Spec 2 | ✅ 已 ship |
complete_main_workflow_job 與 condition_param |
Spec 2 | ✅ 已 ship(既有機制可直接用,不改套件) |
JobCommentService + /job-executions/<uid>/comments |
Spec 既有 GRC 模組 | ✅ 已 ship |
BPMN editor + FlowTemplatePreviewDialog |
Spec 1 FE | ✅ 已 ship |
assessment_plan_extensions + snapshot pattern |
Spec 2/3 | ✅ 已 ship |
不依賴:jedi-flow-engine 套件變更(既有 condition_param + ExclusiveGateway 足以表達 reject 分支;BPMN engine 端的 string-match 解析 condition expression ${decision == 'reject'} 已在 Spec 2 §十.9 修正後支援 Camunda 標準格式)。
無。Brainstorm Q1~Q7 已 resolved;3 個公開議題已由 user 拍板(並列 / 不卡 / BE 強制)。
進入 Phase 3 — implementation-plan.md。
err.context 在 create/update 400 路徑被 jedi_common handler 吞掉| 項目 | 內容 |
|---|---|
| Plan / design §6.3 | 範例 400 response 把 violations array 放在 data.violations |
| 實際落地 | jedi_common.handler.handle_client_error 只序列化 error_code + msg;err.context 是 dead data。create / update 的 400 不含 violations 細節 |
| 為什麼可以接受 | FE 走 POST /flow-engine/flow-templates/validate endpoint(M6)取完整 violations array;create/update 400 只當「存檔守門員」(design §10.2 已說明此分工) |
| 未來修法選項 | (1) patch jedi_common.handler 加 getattr(error, "context", None) 序列化;(2) custom Exception class 帶 violations field handler 識別。本 spec 不做 |
| 影響檔案 | app/flow_engine/service/flow_template_app_service.py:_validate_bpmn_topology(保留 err.context 賦值,未來 jedi_common 改後即可生效,無需改主專案) |
問題假設(design v1.0 / v1.1):M7 plan 把「reject 必填 comment」用 @validates_schema + raise ValidationError({"comment": [...]}, field_name="comment") 處理,預期 FE 能拿到 GRC_400061 error code 與 field 路徑。
實際限制:jedi-common 的 handler.register_error_handlers 對 ValidationError 固定回 COMMON_BAD_REQUEST generic 訊息,不讀 exception.messages(jedi-common/jedi_common/handler/handler.py:39-43)。schema-level message + field path 全部 collapse 成同一個 400。
M8 解法:
StageAdvanceService.advance_stage 在 stage_code == 'review' 時做業務驗證:
BadRequestError(GRC_STAGE_REVIEW_DECISION_INVALID, GRC_400062)BadRequestError(GRC_STAGE_REVIEW_REJECT_REQUIRES_COMMENT, GRC_400061)@validates_schema 留著當第二道防線(type / OneOf 仍能擋格式錯)。未來反悔條件:若 jedi-common 升級後 ValidationError handler 開始讀 exception.messages,可以把 service 層業務驗證刪掉只留 schema validator(單一 source of truth)。
影響檔案:
app/flow_engine/service/stage_advance_service.py:advance_stage(service 層新增 review 業務驗證 + BadRequestError raise)api/flow_engine/serializers/stage_advance.py:StageAdvanceRequestSchema(schema-level @validates_schema 保留作第二道防線)問題假設(design v1.0 / §8.1):design §8.1 只規範 review stage 推進時 ReviewDecisionHandler 不動 AP.status,沒處理上游 task_execution → review 的 status 行為。M0–M9 ship 後沿用 spec 2 的 TaskExecutionOnCompleteHandler → launch_audit,該 method 一定把 AP.status 改成 'auditing'。
user M11.1 smoke 撞到的症狀(2026-05-14):
ProjectAuditorOverview.vue:1160 條件 status === 'auditing' 提前成立)M11.1 解法(user 拍板 A 案,2026-05-14):拆 launch_audit 的「業務驗證」與「正式啟動稽核」階段:
submit_for_review:只做權限檢查 + AP 存在 + incomplete_tasks 驗證(spec 2 launch_audit 的 step 0-2 subset),不建 AR data、不改 AP.statussubmit_for_reviewlaunch_audit(spec 2 原行為)oscal_audit_service 並 call launch_audit(force=True) —— 此時才正式 materialize AR data + AP.status='auditing'。force=True 是因為 task_execution 推進時 submit_for_review 已驗過 incomplete_tasks,不需重驗stage_object_code 塞 ctx['_next_stage_code'](不解析 gateway condition;review → Gateway 場景跳過 Gateway 不影響本機制,因為 ReviewDecisionHandler 自己處理 decision 分流)未來反悔條件:
force=True 走法可能要改 change_status_only 之類專屬 flag影響檔案:
app/project/service/oscal_audit_service.py(新加 submit_for_review method,launch_audit 不動)app/grc/service/oscal_stage_handlers.py(TaskExecutionOnCompleteHandler.execute 加 review-aware 分流;ReviewDecisionHandler.__init__ 注入 oscal_audit_service + execute(approve) call launch_audit)app/flow_engine/service/stage_advance_service.py(新加 _peek_next_stage_code + advance_stage dispatch handler 前塞 ctx['_next_stage_code'])di_containers/grc/grc_containers.py(review_decision_handler 注入 oscal_audit_service)src/components/grc/FlowPhaseBanner.vue(reject 成功 toast 改 reject_success i18n)flow_engine.banner.review.reject_success(zh-tw + en)問題假設(§16.3 修法):原 §16.3 寫的 ReviewDecisionHandler.execute(approve) 無條件 call launch_audit,假設審核之後一定是 audit。
M11.1 smoke 實際撞到的延伸場景:
task_execution → review1 → review2 → audit)—— review1 approve 不該觸發 launch_audit,要繼續推到 review2launch_audit 內部 _check_participant_role(["manager", "auditor"]) 白名單,approve 走 review_decision → launch_audit path 直接 403_peek_next_stage_code 用 get_next_elements 停在 Gateway,回 None;review handler 無法判斷下游解法(commit 1c8d08c):
stage_advance_service._peek_next_stage_code_via_jobs:新加 helper 用 get_next_jobs 穿越 ExclusiveGateway 走 default 路徑,回下一個 UserTask 的 stage_object_code
_peek_next_stage_code 職責分離:前者給 advance_stage 的 handler 分流(停在 gateway,回 None 不解析);後者給 review handler 看 BPMN 真實下游ReviewDecisionHandler.execute(approve) 加 next_stage_code 判斷:
next_stage_code == "audit" → call launch_audit(trusted_caller=True) 真正啟動稽核next_stage_code == "review" → 不 call launch_audit,BPMN 自然推進到下一個 review stageOscalAuditService.launch_audit 新加 trusted_caller: bool = False 參數:
_check_participant_role 二次驗(StageAdvanceService.advance_stage 已驗過 reviewer 角色,無需重複)未來反悔條件:若範本演化成「review approve 後可選分支(multi-outgoing gateway)」,_peek_next_stage_code_via_jobs 走 default 路徑的假設會 break,要改成解析 decision-driven condition
對應 changelog:
docs/changelog/2026-05-15-tweak-multi-review-stage-support.mddocs/changelog/2026-05-15-fix-reviewer-role-blocked-from-launch-audit.md問題假設(design §10.1 + spec 2 遺留):FlowPhaseBanner 推進按鈕的 label 寫在當前 stage 的 complete_button_label_i18n,例如:
M11.1 smoke 撞到的不一致:
task_execution → review → audit 後,task_execution stage 推進按鈕仍顯示「啟動稽核」(user 看到不直覺,因為下一站是 review)解法(commit 1c8d08c,BE-driven):
compliance.stage_objects 新增 entry_button_label_i18n JSONB 欄位review.entry_button_label_i18n = "送審" + audit.entry_button_label_i18n = "啟動稽核"StageAdvanceService.get_current_stage_info 動態算按鈕:用 _peek_next_stage_code_via_jobs 查下一個 stage 的 entry_button_label_i18n,空則 fallback 到當前 stage 的 complete_button_label_i18nFlowPhaseBanner.vue 移除 isReviewStage 硬碼覆寫,primaryButtonLabel 統一讀 BE 提供的值行為差異:
| 階段 | 修前按鈕 | 修後按鈕 |
|---|---|---|
| 執行任務 | 啟動稽核 | 送審 |
| 審核(approve) | 送出審核 | 啟動稽核 |
| 審核(reject 次按鈕) | 退回 | 退回(不變) |
對應 changelog:docs/changelog/2026-05-15-tweak-flow-engine-banner-dynamic-button-label.md
問題假設(plan §九.9 引用 spec 2 §十.9 修正):Camunda BPMN gateway condition expression 用單引號 ${decision == 'reject'},spec 4 BPMN seed 也照此格式寫。
M11.1 smoke 撞到的 root cause:
jedi-flow-engine 0.0.28 的 BpmnUtils._parse_condition_param_to_string._fmt 對 str 值產生雙引號 ${decision == "reject"}Flow_review_forward = UserTask_audit解法(jedi-flow-engine 0.0.29,67e979f):
_fmt(str) 輸出單引號(Camunda FEEL 標準)pyproject.toml pin jedi-flow-engine==0.0.29對應 changelog:docs/changelog/2026-05-15-fix-bpmn-gateway-condition-quote-mismatch.md
問題假設(plan §五.2 規則表 v1.1 reconciliation):原 stage_objects 規則只設計單一 review → audit,假設審核之後一定接稽核。
flow-template BPMN editor 開放給 user 後撞到的不一致: user 可建立含「多重審核」(review → review)或「審核退回到設定/執行任務」的 BPMN 範本,但拓撲驗證器報錯不讓發布。
解法(commit 3ff38d9 同 session):
| stage | 欄位 | 修前 | 修後 |
|---|---|---|---|
review |
allowed_predecessors | ["planning", "task_execution"] |
["planning", "task_execution", "review"] |
review |
allowed_successors | ["audit"] |
["audit", "review", "task_execution", "planning"] |
planning |
allowed_predecessors | [] |
["review"] |
影響:現有 BPMN 範本不受影響(規則放寬,不是收緊)。
對應 changelog:docs/changelog/2026-05-15-fix-stage-object-topology-rules-too-restrictive.md 對應 migration:scripts/sql/2026-05-15-stage-object-topology-rules-fix.sql
範圍外延伸:本 spec 原本 plan 未含 flow_template 生命週期管理;M11.1 smoke 期間 user 反饋「使用者建範本可能還沒寫完就被誤用」,同 branch 衍生加入 draft/published 狀態機。屬於 flow-template 模組獨立 feature,不是 spec 4 review stage 修補,紀錄在此 reconciliation 因為共用同一 release branch。
狀態機:
| 動作 | 前狀態 | 後狀態 |
|---|---|---|
POST /flow-templates(create) |
— | draft |
PUT /flow-templates/<uid> 僅名稱描述異動 |
不變 | 不變 |
PUT /flow-templates/<uid> BPMN XML 異動 |
任何 | draft |
POST /flow-templates/<uid>/duplicate |
— | draft |
PUT /flow-templates/<uid>/publish(完整 BPMN + topology 驗證) |
draft |
published |
PUT /flow-templates/<uid>/unpublish |
published |
draft |
稽核案建立防護:POST /project/start 若選用 status='draft' 範本 → 400 GRC_400063(流程範本尚未發布)
新加 BPMN editor 智能 Gateway condition UI(FE):
stage_object_code:
review → 語義下拉「通過(預設)/ 退回」,退回寫 ${decision == 'reject'}audit → 「無缺失(預設)/ 有缺失」,有缺失寫 ${has_findings == true}writeCamundaProp bpmn-js command stack 不追蹤導致首次寫入不生效(改用 modeling.updateModdleProperties())對應 changelog:
docs/changelog/2026-05-15-feat-flow-template-draft-publish-status.mddocs/changelog/2026-05-15-feat-flow-template-unpublish.md 對應 analysis:docs/analysis/2026-05-15-flow-template-bpmn-editor-design.md 對應 migration:scripts/sql/2026-05-15-flow-template-status.sql| 版本 | 日期 | 變更 |
|---|---|---|
| v1 | 2026-05-14 | 初版,brainstorm Q1~Q7 + 3 公開議題收斂完整版 |
| v1.1 | 2026-05-14 | spec reviewer 反饋整合:(1) §5.2 規則表 reconciliation(移除 audit→review / review→task_execution forward 列項,明示 reverse 走 §7.3 例外);(2) §6.1 非 review stage 送 decision 行為寫死 ignore + log warning;(3) §6.2 reject_button_label 改從 FE i18n bundle 取,不動 stage_objects schema;(4) §7.2 step 6 reachability 明示「只走 forward edge」;(5) §7.3 reverse 識別改 extension property 為 primary、嚴格 anchored regex 為 fallback;(6) §7.5 加自訂範本 backfill migration(scan duplicated builtin-full-audit);(7) §8.2 critical JobComment wiring fix — 先查 JobExecution.uid (UUID) 再傳,不能用 BPMN element id;(8) §9.1 新增「Spec 2 vs Spec 4 reverse runtime 行為差異表」對齊使用者心智模型;(9) §12.1 補 regex 邊界 case 9 條 + 純循環無 end path test |
| v1.2 | 2026-05-14 | M5 code reviewer 反饋:新增 §16 Implementation Reality / Reconciliation,紀錄 err.context 在 create/update 400 路徑被 jedi_common handler 吞掉的偏差;原 §16 文件版本改為 §17 |
| v1.3 | 2026-05-14 | M8 code reviewer 反饋:新增 §16.2 — Marshmallow ValidationError 訊息被 jedi-common handler 吞掉 → review 業務驗證(decision 缺值 / reject 必填 comment)改在 StageAdvanceService.advance_stage 用 BadRequestError(GRC_400061/062) 處理,schema-level @validates_schema 留作第二道防線 |
| v1.4 | 2026-05-15 | M11.1 manual smoke 反饋:新增 §16.3 — task_execution → review 推進不能提前把 AP.status 改成 'auditing'。拆 launch_audit 為 submit_for_review(驗證 only)+ launch_audit(驗證 + materialize + status change);TaskExecutionOnCompleteHandler review-aware 分流;ReviewDecisionHandler approve 時注入 oscal_audit_service 並 call launch_audit(force=True);FE FlowPhaseBanner reject 成功 toast 改 reject_success i18n |
| v1.5 | 2026-05-15 | M11.1 後續整輪修補:(1) §16.4 multi-review 串接 + reviewer 角色 trusted_caller flag(commit 1c8d08c);(2) §16.5 entry_button_label_i18n 動態按鈕文字(stage_objects 加欄位 + BE peek 下一個 stage);(3) §16.6 BPMN gateway condition Camunda FEEL 單引號標準(jedi-flow-engine 0.0.29 — 問題 3「按退回卻推進稽核」的真正 root cause);(4) §16.7 stage_object topology 規則放寬(支援 review→review / review→planning / review→task_execution);(5) §16.8 衍生 flow_template draft/published 狀態機 + BPMN editor 智能 Gateway condition UI(同 branch 但屬獨立 feature) |