# Spec 4：審核階段 + 流程方向性驗證

> **狀態**：v1 — brainstorm 收斂版，待 review
> **整體脈絡**：`../README.md`
> **依賴**：Spec 1 / Spec 2 / Spec 3（皆已 ship）
> **建立日期**：2026-05-14

---

## 一、目標

兩個獨立但相關的子題：

1. **審核（review）階段**：在「執行任務」與「稽核」之間插入一個由 `reviewer` 角色主導的人工過審節點；不過時可由範本指定退回到任一上游 UserTask；過/退回都可附留言。
2. **流程方向性驗證**：範本 create/update 時 BE 主動驗證 BPMN 圖結構是否「合理」（如禁止 start → poam 開頭），給 stage_object 補上下游規則欄位作為「真實來源」。

兩題共用同一份 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 | 「流程相依性問題 — 例如第一步就是缺失改善是不合理的；範本存檔時要檢查方向性 + 防呆」 |

---

## 三、Brainstorm 結論

| # | 主題 | 決議 |
|---|---|---|
| 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` |

---

## 四、範圍

### In scope

**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 推進方向）
- BPMN engine 已支援 `${decision == 'reject'}` 條件式（既有 condition_param 機制，不改套件）
- BE 強制驗證 `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, ...)`
- FE Banner ConfirmDialog 加 comment textarea（approve / 一般 stage advance optional；reject required）

**C — 流程方向性驗證**
- `compliance.stage_objects` 加 4 個欄位：`can_start BOOLEAN`、`can_end BOOLEAN`、`allowed_predecessors JSONB`、`allowed_successors JSONB`
- 5 個 stage 規則回填（含新 review）
- `FlowTemplateAppService._validate_bpmn_topology()` 演算法（7 步檢查，見 §七）
- 新 endpoint `POST /flow-engine/flow-templates/validate`（純驗證不存檔）
- 6 個新 error code（含具體違規 reason context）

**D — Builtin 範本**
- 新增 `builtin-full-audit-with-review`（並列於既有 builtin-full-audit）
- 既有 3 個 builtin 範本驗證能通過新 validator（poam → audit reverse edge 需補 extension property）

### Out of scope（明確不做）

- 修改 jedi-flow-engine 套件（既有 ExclusiveGateway / condition_param 已足用）
- review approve 的 `all_controls_reviewed` precondition（公開議題 2 決議不卡）
- 既有 `review_mark` API 行為改動（reviewer 仍可逐控制項標記，純資訊性）
- 用 Comment 表做「審計軌跡 timeline view」UI（→ 後續 spec）
- 跨範本「stage 物件 CRUD UI」（→ 後續 spec，本 spec 僅 seed）
- Multi-reviewer 串聯審核（每個 review UserTask 仍是單一推進角色集合）

---

## 五、Schema 改動

### 5.1 `compliance.stage_objects` 加欄位

```sql
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。

### 5.2 5 個 stage 規則（覆蓋既有 4 個 + 新 review）

```sql
-- 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 路徑：

- → audit：對應「無 reviewer 流程」（既有 `builtin-full-audit`）
- → review：對應「有 reviewer 流程」（新 `builtin-full-audit-with-review`）

`poam.allowed_successors=[]` 配 `poam.can_end=true` 表達「poam 唯一 forward 是接 End；poam → audit 走 reverse」。

### 5.3 為什麼用 JSONB 而不是關聯表

`stage_object_predecessors` / `stage_object_successors` 關聯表表達力更完整，但這份「規則表」量級在 N²（N=5~10）、語意上是 stage_object 的內在屬性、且很少獨立查詢，用 JSONB 集中放在同一列：

- 讀寫一筆即拿到完整規則（validator 一次 query 5 列）
- migration 簡單（不用維護 FK 一致性）
- 未來需要關聯式查詢時可派生 view，不需重構

---

## 六、API 改動

### 6.1 `StageAdvanceRequestSchema` 擴充

```python
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 比阻擋更友善。

### 6.2 `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 額外回，因為：

- v1 只有 review 有 reject 按鈕（特化行為），不值得 schema 動
- i18n 文案修改流程在 FE 完整（FE i18n PR 直接調，BE 不參與）

未來若多個 stage 需要 reject 按鈕，再考慮把 `reject_button_label_i18n` 加進 `stage_objects` 列。

### 6.3 新 endpoint `POST /flow-engine/flow-templates/validate`

```
POST /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": [...]}` 攜帶細節。

---

## 七、Validator 演算法

### 7.1 整體流程

```
_validate_bpmn(xml)                     ← 既有，保留
    ↓
_validate_bpmn_topology(util)            ← v1 新增
    ↓ pass
return / persist
    ↓ fail
raise BadRequestError(GRC_FLOW_TEMPLATE_TOPOLOGY_INVALID,
                      data={"violations": [...]})
```

### 7.2 檢查步驟（7 條）

| # | 規則 | 違規 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 規則仍會被擋 | （不擋） |

### 7.3 Forward vs Reverse edge 判定（B.3 例外）

對每條 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 視為不存在
```

**設計選擇**：

- **extension property 是 primary**：FE editor 拖 reject sequenceFlow 時自動填 `<camunda:property name="reverse" value="true"/>`；穩定、不靠字串 match
- **嚴格 anchored regex 是 fallback**：只 match 形如 `${var == 'reject'}`（exact）— 變數名 `decision` / `review_status` / `verdict` 都可，但 quote 後必須緊接 `}`（含可選空白）
- **明確不 match 的 case**：
  - `${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 條件值」（成本較高）。

### 7.4 為什麼 Gateway 全空 default-only 不允許（step 5）

ExclusiveGateway 的核心是「分支」。若所有 outgoing 都無 condition，runtime 永遠走 default flow → gateway 形同 sequence pass-through，等同無意義節點且暗示範本設計錯誤。直接 400 反而給 user 明確 signal。

### 7.5 既有範本兼容性 + 自訂範本 backfill

| 範本 | 過 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 必須同時：

1. patch builtin 本體 XML
2. scan 所有 `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。

---

## 八、Handler / Service 改動

### 8.1 `ReviewDecisionHandler`

`app/grc/service/oscal_stage_handlers.py` 加：

```python
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。

### 8.2 `StageAdvanceService.advance_stage` 接 decision/comment payload

```python
@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。

### 8.3 `_build_condition_param` 加 review 分支

```python
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 None
```

---

## 九、Builtin 範本 — `builtin-full-audit-with-review`

```
StartEvent
    ↓
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）**：

- 既有 4 個 builtin 已被 Spec 1 ship 三週、可能被自訂範本 duplicate 引用過；移除既有範本會擾動既有 user 範本 default 值與 launch_new_round 的「沿用上一輪」邏輯
- 新增成本低，並列讓 user 可在「沒有 reviewer 角色」與「有 reviewer 流程」之間自由選擇

### 9.1 Reverse edge 兩種 runtime 行為差異（重要）

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 |

**為什麼設計不同**：

- poam → audit 是「稽核覆核」業務流程：缺失改完後需要 auditor 重新驗證，每次 iteration 需新一份 ar_data 紀錄
- review reject → task_execution 是「審核退件」純流程退回：PM 重補資料後送 reviewer 再審，沒有「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 行為。

---

## 十、FE 改動概要

### 10.1 Banner

| 場景 | UI 行為 |
|---|---|
| current_stage = review | 主按鈕「送出審核（approve）」+ 次按鈕「退回（reject）」 |
| 其他 stage | 既有主按鈕（無變動） |

雙按鈕都先彈 `ConfirmDialog`，內含 `<Textarea v-model="comment" />`：

- approve（包含其他 stage）：comment optional，placeholder「可選填說明」
- reject：comment required，提示「請說明退回原因（必填）」，未填時 disable 「確認」按鈕

### 10.2 BPMN editor inline warning

editor 內 sequenceFlow drag-end 事件 → debounce 500ms → POST `/flow-engine/flow-templates/validate`：

- valid：清除側邊欄 warning panel
- 400：把 violations array 渲染為側邊欄警告列表，違規 sequenceFlow 在畫布上加紅色 outline + tooltip 顯示 reason

存檔仍由 BE 決定（不送出 / 送出 → BE 再驗一次當作守門員）。

### 10.3 Palette

- Palette 加「審核」UserTask 預設 stencil：拖出時自動填 `stage_object_code=review` + `main_role=reviewer`
- 既有 stage（planning / task_execution / audit / poam）palette 不動

### 10.4 i18n

新增 key：

```yaml
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: "可選填說明"
```

---

## 十一、Error code

| 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）。

---

## 十二、測試策略

### 12.1 BE pytest

**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 violations
- `update_flow_template` 同上
- `POST /flow-engine/flow-templates/validate` happy path + 400 path
- `advance_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 validator
- `advance_stage` 非 review stage 帶 decision → ignore decision（手動忽略，不擋）

### 12.2 Manual smoke（必過項）

```
□ 建專案選範本「完整稽核含審核」(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
```

### 12.3 跨 repo（compliance-manager-test）

- 新 BDD scenario：「reviewer 退回後 PM 重新提交可走完整輪」
- adapt 既有 Spec 2 Banner BDD（無 review stage 的流程仍 pass）

---

## 十三、落地里程碑

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

---

## 十六、Implementation Reality / Reconciliation（M5 加）

### 16.1 `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 改後即可生效，無需改主專案）|

### 16.2 Marshmallow ValidationError 訊息被 jedi-common 吞掉 → 業務驗證移到 service 層

**問題假設**（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'` 時做業務驗證：
  - decision 為 None / 空 → `BadRequestError(GRC_STAGE_REVIEW_DECISION_INVALID, GRC_400062)`
  - decision == 'reject' + comment 空 → `BadRequestError(GRC_STAGE_REVIEW_REJECT_REQUIRES_COMMENT, GRC_400061)`
- schema 內 `@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` 保留作第二道防線）

### 16.3 task_execution → review 推進不能提前把 AP.status 改成 'auditing'

**問題假設**（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）：
1. 「檢視稽核結果」按鈕在 review 階段就出現（`ProjectAuditorOverview.vue:1160` 條件 `status === 'auditing'` 提前成立）
2. progress / 狀態欄位顯示「稽核」但 BPMN 還在 review UserTask
3. user 心智模型不通：「執行任務 → 審核 → 稽核 — 通過審核狀態才該進稽核」

**M11.1 解法**（user 拍板 A 案，2026-05-14）：拆 `launch_audit` 的「業務驗證」與「正式啟動稽核」階段：

- **OscalAuditService 新增 `submit_for_review`**：只做權限檢查 + AP 存在 + incomplete_tasks 驗證（spec 2 launch_audit 的 step 0-2 subset），**不**建 AR data、**不**改 AP.status
- **TaskExecutionOnCompleteHandler** review-aware 分流：
  - 下一站是 review（builtin-full-audit-with-review）→ 走 `submit_for_review`
  - 下一站是 audit（builtin-full-audit / 其他既有範本）→ 走 `launch_audit`（spec 2 原行為）
- **ReviewDecisionHandler.execute(approve)** 注入 `oscal_audit_service` 並 call `launch_audit(force=True)` —— 此時才正式 materialize AR data + AP.status='auditing'。`force=True` 是因為 task_execution 推進時 submit_for_review 已驗過 incomplete_tasks，不需重驗
- **ReviewDecisionHandler.execute(reject)**：不動 AP.status，BPMN 走 reverse 回 task_execution
- **StageAdvanceService.advance_stage** dispatch handler 前 peek BPMN 下一個 UserTask 的 `stage_object_code` 塞 `ctx['_next_stage_code']`（不解析 gateway condition；review → Gateway 場景跳過 Gateway 不影響本機制，因為 ReviewDecisionHandler 自己處理 decision 分流）

**未來反悔條件**：
- 若 launch_audit 加入「不可重入」防呆（同一 AP 不能跑兩次），ReviewDecisionHandler approve 的 `force=True` 走法可能要改 `change_status_only` 之類專屬 flag
- 若範本設計演化成「review 後可重新指派 task」（reject 後改 task 完成度），需要在 review reject 路徑加 AP.status 退回 'active' 邏輯（目前 reject 不動 status，user 還在 task_execution 階段可繼續用，沒問題）

**影響檔案**：
- `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）
- locale `flow_engine.banner.review.reject_success`（zh-tw + en）

### 16.4 multi-review 串接 + reviewer 角色 trusted_caller

**問題假設**（§16.3 修法）：原 §16.3 寫的 `ReviewDecisionHandler.execute(approve)` 無條件 call `launch_audit`，假設審核之後一定是 audit。

**M11.1 smoke 實際撞到的延伸場景**：
1. 範本可串接多個審核（`task_execution → review1 → review2 → audit`）—— review1 approve 不該觸發 launch_audit，要繼續推到 review2
2. reviewer 角色不在 `launch_audit` 內部 `_check_participant_role(["manager", "auditor"])` 白名單，approve 走 review_decision → launch_audit path 直接 403
3. `_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 stage
- **`OscalAuditService.launch_audit`** 新加 `trusted_caller: bool = False` 參數：
  - True 時跳過 `_check_participant_role` 二次驗（`StageAdvanceService.advance_stage` 已驗過 reviewer 角色，無需重複）
  - False 時保留原 spec 2 行為（直接 API 呼叫 path 仍完整驗角色）

**未來反悔條件**：若範本演化成「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.md`
- `docs/changelog/2026-05-15-fix-reviewer-role-blocked-from-launch-audit.md`

### 16.5 entry_button_label_i18n — 按鈕文字由「下一個 stage」決定，非當前 stage hardcode

**問題假設**（design §10.1 + spec 2 遺留）：FlowPhaseBanner 推進按鈕的 label 寫在當前 stage 的 `complete_button_label_i18n`，例如：
- task_execution stage 的 button = "啟動稽核"（spec 2 設計直接接 audit）
- review stage 的 button = "送出審核"（FE i18n hardcode）

**M11.1 smoke 撞到的不一致**：
- spec 4 改成 `task_execution → review → audit` 後，task_execution stage 推進按鈕仍顯示「啟動稽核」（user 看到不直覺，因為下一站是 review）
- review approve 按鈕顯示「送出審核」（user 認為應該是「啟動稽核」，因為下一站才是 audit）

**解法**（commit `1c8d08c`，BE-driven）：

- `compliance.stage_objects` 新增 `entry_button_label_i18n JSONB` 欄位
- seed `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_i18n`
- FE `FlowPhaseBanner.vue` 移除 `isReviewStage` 硬碼覆寫，`primaryButtonLabel` 統一讀 BE 提供的值

**行為差異**：

| 階段 | 修前按鈕 | 修後按鈕 |
|---|---|---|
| 執行任務 | 啟動稽核 | 送審 |
| 審核（approve） | 送出審核 | 啟動稽核 |
| 審核（reject 次按鈕）| 退回 | 退回（不變）|

**對應 changelog**：`docs/changelog/2026-05-15-tweak-flow-engine-banner-dynamic-button-label.md`

### 16.6 BPMN gateway condition 引號 — Camunda FEEL 單引號標準

**問題假設**（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"}`
- BPMN engine exact string match 失敗，無 flow 符合 → 走 default = `Flow_review_forward = UserTask_audit`
- user 看到「按退回卻推進稽核」（**問題 3 的真正 root cause** — 不是 status 顯示誤判，BPMN 真的走錯）

**解法**（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`

### 16.7 stage_object topology 規則放寬 — 支援多重審核 / 退回前置 stage

**問題假設**（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`

### 16.8 flow_template draft / published 狀態機（衍生 spec）

**範圍外延伸**：本 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）：
- ExclusiveGateway 屬性面板溯源上游 UserTask 的 `stage_object_code`：
  - `review` → 語義下拉「通過（預設）/ 退回」，退回寫 `${decision == 'reject'}`
  - `audit` → 「無缺失（預設）/ 有缺失」，有缺失寫 `${has_findings == true}`
  - 其他 → fallback InputText 自由輸入
- 修 `writeCamundaProp` bpmn-js command stack 不追蹤導致首次寫入不生效（改用 `modeling.updateModdleProperties()`）

**對應 changelog**：
- `docs/changelog/2026-05-15-feat-flow-template-draft-publish-status.md`
- `docs/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） |
