---
title: "專案規劃頁新增任務與平行流程 — 設計文件 (FR-112)"
brand: "Guidant AI · **FR-112** 規劃頁新增任務與平行流程"
eyebrow: "FR-112 · 設計文件 · 2026-09-19（D1–D7 全數定案）"
h1: "任務在規劃頁上開，流程圖只管先後順序 — 設計定案"
lede: "PM 要替一個查核項目多開一個任務，動線回到專案規劃頁：按「新增任務」、填名稱、存檔。新任務預設掛成**平行分支**——誰先做都行、全部做完這個查核項目才算過。流程圖編輯器降級為純粹的走向設計工具，屬性面板只留名稱，不再是填任務資料的地方。連帶修掉一個既有引擎缺口：**合流閘道不等兄弟分支就放行**。"
chips: [
  {text: "D1–D7 全數定案", kind: ok},
  {text: "4 子需求 · 16 子任務", kind: accent},
  {text: "跨 3 repo：jedi-flow-engine／BE／FE", kind: accent},
  {text: "🔴 引擎缺口：合流閘道不等兄弟分支（兩落點都修）", kind: crit},
  {text: "範圍排除：流程範本管理頁", kind: warn}
]
footer: "FR-112 · 專案規劃頁新增任務與平行流程 — 設計文件 · 2026-09-19 · 討論稿：[discussion.html](discussion.html)（含被排除方案的完整推演）· 沿革見 LOG 與 git log"
---

> 狀態：**設計定案（D1–D7 全數拍板），待開卡**｜日期：2026-09-19
> 討論稿（含選項與被排除方案的完整推演）：[`discussion.html`](./discussion.html)
> 開發用 worktree：三個 repo 各有 `.claude/worktrees/planning-task-parallel-flow`，branch 一律 `feature/planning-task-parallel-flow`

> 🔴 **這份是設計決策，不是現況。** 內文的行號、筆數與待辦停在 2026-09-19 定稿當時，之後實作改動的結果**不回頭改寫這裡**——現況一律看 `FINAL-SPEC.md`（實作完成後產出）。

## 變更紀錄 {#changelog nav="變更紀錄"}

| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-09-19 | 初版設計定案。D1–D6 拍板（皆採討論稿建議案）；D4 依決策者指示鎖為「屬性面板只留名稱」，編輯器 UI 優化列為延伸項。 | FR-112 母案 |
| 2026-09-19 | 追加 **D7（刪除任務兩入口對稱）**，並據此改寫 D6 ②：XML 少了 `job_execution_uid` 視為刪除意圖而非孤兒，存檔端點改為三向 diff（§5.8）。拆分為 4 子需求 16 子任務。 | 決策者追加拍板 |
| 2026-09-19 | §5.8 三向 diff 加第五列：一個節點對到多筆任務→擋下不靜默刪（DEV 有兩筆舊版編輯器殘留，B-2.3 驗收時發現）。 | 決策者裁示 |
| 2026-09-19 | D6 ① 改為**至少保留一個任務**：規劃頁刪到最後一筆擋下、編輯器存檔會刪到零也擋下。 | 決策者追加拍板 |
| 2026-09-20 | 補兩項裁示：①編輯器刪任務走範本能力守門、不要求專案管理人身分（§5.4）；②輪次凍結反查不到時放行及其理由（§5.8）。另記存檔執行順序「先建後刪」與被刪 uid 回報 route 接 Drive 歸檔（§5.8）。 | 決策者 2026-09-20 裁（arc review CM-1977 → CM-1978） |

---

## 1. 需求背景與端到端流程 {#background nav="背景"}

### 1.1 一句話

**任務的所有資料以專案規劃頁為準存進資料庫，流程圖只留「先後與分支」這一件事；規劃頁補上「新增任務」按鈕，新任務預設掛成平行分支，該查核項目要全部任務做完才算通過。**

### 1.2 給非工程讀者的三個名詞

- **查核項目（AO）**：一條法規控制項底下再細分的檢查點，規劃頁左邊樹狀清單點到的最小單位。每個查核項目底下掛一串任務。
- **流程圖（BPMN）**：用方塊與箭頭畫出來的工作流程。方塊叫 `userTask`（一個方塊＝一個任務），箭頭表示先後順序，菱形的叫「閘道」——**平行閘道**表示「這幾條同時開跑」、**互斥閘道**表示「二選一」。
- **任務資料表（`job_executions`）**：資料庫裡真正存任務的地方——名稱、描述、負責人、狀態、證據、問卷、檢測工具設定都在這裡。

### 1.3 要解的問題

任務的同一份資料存在兩個地方，而且會互相覆蓋：

| 存在哪 | 存了什麼 | 誰在寫 |
|---|---|---|
| 資料庫 `job_executions`（＋ `task_assignees` 等關聯表） | 名稱、描述、指南、類型、狀態、時間、負責人、部門、設備、問卷、檢測工具 | 規劃頁存檔、後端流程引擎 |
| 流程圖 XML 的 camunda 屬性 | `name`、`workDescription`、`guide`、`actionType`、`actionInfo`、`devices`、`departments`、`toolParams`、`completionMode`、`job_execution_uid` | 流程圖編輯器存檔、後端 `_sync_task_to_template_xml` 回寫 |

編輯器存檔時會逐節點打 PUT 更新任務，而它手上只有五個欄位——送出的更新會蓋掉規劃頁設好的東西。程式碼裡的防禦註解就是病徵：「只在 `actionInfo` 非空時帶 surveys⋯避免把規劃頁設的問卷誤刪」「不帶 `approval_required`⋯不誤蓋規劃頁審查旗標」（FE `WorkflowSetupEditor.vue:525-531`）。

::: {.callout .warn}
**每加一個規劃頁專屬欄位，就要在編輯器同步邏輯補一條「這欄不要蓋」的例外**

例外會愈來愈多，而漏掉一條的症狀是**資料靜靜消失、沒有錯誤訊息**。本案的解法不是把例外補齊，是把雙向寫入路徑整條拿掉。
:::

同時，規劃頁**根本不能新增任務**——要多開一個任務，PM 得打開流程圖編輯器、從工具列拖方塊、手動接線、填一堆屬性。流程圖編輯器是工程師與流程設計者的工具，要 PM 為了「多派一件事」去學拖方塊、分辨平行閘道與互斥閘道，等於把工具的複雜度轉嫁給使用者。

### 1.4 端到端流程（做完之後長這樣）

```
PM 在規劃頁點一個查核項目
  → 按「新增任務」，只填名稱
  → 後端在該查核項目的流程圖上插一個方塊、接成平行分支、回寫任務編號（同一個交易內）
  → 任務面板出現、展開待填（負責人／問卷／檢測工具⋯全部在規劃頁填）
  → 專案啟動後，該查核項目底下所有平行任務同時可做
  → 每個負責人各自完成自己那條
  → 全部完成 → 該查核項目才算通過（只完成一部分不會提前放行）

流程圖編輯器（另一條動線，給要調走向的人用）
  → 改線（把平行拉成一條龍、加二選一分支）、拖新方塊（只能填名稱）、刪方塊
  → 存檔 → 後端三向 diff：沒編號的新方塊建成空任務／XML 裡消失的編號刪掉那筆任務／其餘只更新拓樸
  → 回規劃頁重抓，任務清單與圖一致
```

---

## 2. 分工概述（30 秒版） {#overview nav="概述"}

| 階段 | 做什麼 | 產出 | 完成怎麼判定（決策者親手檢查法） |
|---|---|---|---|
| **⓪ 開工前必驗** | 確認輪次推進／輪次回退讀的流程圖，與查核項目底下的收證據流程**不是同一張** | 一頁查證結論（寫進母卡） | 開 `stage_advance_service.py` 與 `stage_rollback_service.py`，看它們讀的 `workflow_template_xml` 來自哪張範本；若與查核項目共用同一張，**停下回報、整案範圍重估** |
| **① 流程圖改圖工具** | 共用套件補三支能力：把流程重排成標準平行、在合流閘道前掛一條新分支、判斷「這個閘道是合流嗎／兄弟分支都完成了嗎」 | jedi-flow-engine 新增／修正的函式 ＋ 單元測試 | 跑套件測試：給一張「兩條平行分支、合流後還有一個任務」的流程圖，完成第一條分支後判定函式要回「還不能放行」；兩條都完成才回「可以」 |
| **② 後端新增／刪除任務** | 新增任務端點不再要求前端先給流程圖節點編號——後端自己在圖上補節點、接線、回寫任務編號；刪除從兩個入口都能做（規劃頁按刪除／編輯器刪方塊存檔，同一支刪除函式）；合流等待判定接上兩個落點 | BE app service ＋ repo 改動 | 用 Swagger 直接打新增任務端點（**不帶** `job_element_uid`）→ 回 200；去資料庫看該查核項目的流程圖 XML，多了一個方塊、且方塊上帶著新任務的編號 |
| **③ 規劃頁按鈕** | 任務區塊補「新增任務」與「刪除任務」；凍結後按鈕停用並顯示原因 | FE `ProjectPlanningView.vue` | 規劃頁點任一查核項目 → 按「新增任務」→ 填名稱存檔 → 重新整理仍在 → 打開流程圖編輯器，看得到這個新方塊，而且是與原任務**並排**（平行）不是串在後面 |
| **④ 流程圖編輯器瘦身** | 屬性面板只留名稱；存檔不再由前端逐一打新增／更新任務端點；**刪方塊的能力保留不鎖**（刪了就是刪任務） | FE `WorkflowSetupEditor.vue` | 打開編輯器點一個方塊 → 右邊只剩名稱欄，沒有任務類型／問卷／設備／部門；拖一個新方塊出來存檔 → 回規劃頁看得到一筆新的空任務；刪掉一個方塊存檔 → 回規劃頁該任務**消失** |
| **⑤ 驗收與回歸** | 專案啟動、輪次推進、批次完成、任務佇列四條既有動線在平行結構下重跑 | 手測清單 ＋ e2e 回歸（測試 repo） | 起一個新專案 → 規劃頁加兩個任務 → 兩個都完成 → 該查核項目變「已完成」；只完成一個時**不可以**變完成 |

依賴：⓪是全案的前置查證；①是②的地基（②要呼叫①補的改圖函式）；③④在②完成後可平行；⑤等③④都驗收過才做。

---

## 3. 決策定案（D1–D7） {#decisions nav="決策定案"}

| # | 決策 | 定案 | 理由 | 被排除的方案 |
|---|---|---|---|---|
| **D1** | 輪次凍結後還能不能新增任務 | **不能。新增／刪除任務只在規劃期開放**；凍結後按鈕停用並在畫面上顯示原因（停用＋留住提示，不彈 toast），後端同步擋下 | 輪次一旦離開規劃期，任務清單就是這一輪的稽核範圍；中途追加等於改變已在跑的稽核範圍，證據完整性與報表口徑都會受影響 | 「凍結後仍可新增」——「稽核到一半發現漏了一項」確實會發生，但那牽涉補件流程與報表口徑，屬另一期的題目，不在本案混做 |
| **D2** | 使用者已把流程排成一條龍時，規劃頁新增的任務掛哪裡 | **掛成合流前的一條新平行分支，不動既有順序**。圖上沒有合流閘道時，在結束事件前補一個 | 同時滿足兩件事：不動使用者刻意排的順序，又維持「新增的任務預設不相依」這個原則 | ①一律重排成標準平行——會擅自推翻使用者排的先後，而且沒有任何提示，是很難解釋的行為；②接在最後一個任務後面繼續串——與「新增預設平行」的方向相反 |
| **D3** | 合流閘道等待修正要不要納入本案 | **納入，且兩個落點都修**——`complete_job()` 與 `complete_main_workflow_job()`（`app/flow_engine/service/workflow_execution_service.py`）。拓樸判定函式放 jedi-flow-engine，兩處呼叫它 | 缺口本身是既有的，但本案讓平行結構從「幾乎不存在」變成「預設樣貌」，並明示保留改線能力——使用者只要在合流後接一個覆核任務就會踩到，而症狀（覆核任務提早跳成進行中）看起來像資料錯亂、不像流程設定問題，會極難查 | ①不修，另開一期——本案正是放大它的原因，留著等於明知會踩；②只修 `complete_job()` 一處——兩支是同一份寫法的兩個副本，只修一處會留下一條靜默錯誤路徑 |
| **D4** | 流程圖編輯器的屬性面板留哪些欄位 | **只留名稱**。工作描述、指南、任務類型、設備、部門全部移除，圖上既有屬性的讀取路徑一併停用 | 決策者原話：**「回歸最簡單就好，不想讓編輯器做太複雜的編輯，目前的 UI 設計不是很好用，之後再來優化」**。工程面理由同向：多留一個欄位就多一條雙向寫入路徑，而「在圖上看得到描述」的好處不值得為它保留第二份真相 | ①連同描述與指南一起留——它們會立刻變成第二份真相，規劃頁改了、圖上沒改，兩邊不一致；②面板全砍只剩唯讀——名稱是圖的可讀性根本，拿掉後圖上只剩節點編號 |
| **D5** | 既有專案圖上的舊 camunda 屬性要不要清掉 | **不清，讀的時候一律忽略**。同時 `_sync_task_to_template_xml()` 收斂成**只寫 `name`** | 清除 migration 要改動所有租戶的範本 XML，改壞了沒有回頭路（XML 沒有欄位層級的版本控制）；而這些屬性留著不造成行為差異——只要沒有任何程式碼讀它，它就只是一段被忽略的文字。收斂寫入端是必要的，否則舊屬性會被持續刷新、永遠不會自然凋零 | 寫 migration 掃全部範本清屬性——唯一好處是「看起來乾淨」，不值得承擔改壞既有專案流程的風險 |
| **D6** | 邊界情形 | ①**至少保留一個任務**——規劃頁刪到最後一筆擋下（`GRC_412` 系列前置條件錯誤，訊息：「查核項目至少要有一個任務」）；編輯器存檔的三向 diff 若會把任務刪到零也整筆擋下、不存；②**XML 少了 `job_execution_uid` ＝刪除意圖，不是錯誤**——後端據此刪對應任務；「孤兒」的定義收窄為「圖上有 `userTask`、但**該節點沒有 uid 屬性**且 DB 查不到對應任務」，那視為**新增**；真正擋下存檔的只剩「uid 重複」與「節點從開始事件走不到」；③**不設硬上限**，但超過 20 筆在規劃頁給提示 | ①零任務會讓該查核項目的流程變成 `開始 → 結束`，一啟動就直接完成、沒有任何人經手就「通過」，稽核上站不住腳；「這項不需要做」應由專案範圍（reviewed-controls）表達，不是把任務刪光；②刪除在兩個入口都要能做（D7），編輯器刪方塊的唯一可觀察訊號就是「XML 裡少了那個 uid」——把它當錯誤擋下，等於宣告「圖上刪方塊無效」，與 D7 相反；③硬上限會在真實需求撞上時變成無解的阻塞，提示足以處理「圖擠成一團」的可讀性問題 | ①允許零任務（圖 `開始 → 結束`）——會產生無人經手即通過的查核項目；②「少了 uid ＝孤兒 → 擋下存檔」——會讓編輯器刪節點這條動線無法成立；③設硬上限（如 20 筆）擋下 |
| **D7** | 刪除任務由哪個入口做 | **兩邊都可以，語意完全一致**。規劃頁按刪除 → 走既有 `DELETE` 端點；編輯器刪掉 `userTask` 節點後存 XML → 後端 diff 出「XML 裡消失的 `job_execution_uid`」，對那些任務呼叫**同一支** `delete_job()`。**硬刪除**（實查 `flow_control_job_repo_impl.py:808` 是 `session.delete(job)`，不是改 `CANCEL`），守門沿用既有的 `_require_manager`（`job_service.py:179`）| 與新增對稱：新增已經是兩個入口同一支端點，刪除若只開一邊，使用者在圖上刪了方塊卻發現任務還在，是無法解釋的行為。**共用同一支 `delete_job()` 是硬要求**——它負責清五張關聯表與 snapshot 問卷，另寫一套必定漏刪，留下永遠查不到任務的孤兒列 | ①只開規劃頁一邊、編輯器刪節點擋下——與 D4「編輯器管走向」的定位矛盾（刪方塊就是改走向）；②編輯器刪節點改成「標 `CANCEL`」——兩入口兩套語意，同一個動作在不同頁面產生不同結果 |

---

## 4. 現況接入點盤點 {#inventory nav="現況盤點"}

::: statgrid
::: stat
[3]{.v}[涉及 repo]{.k}
:::
::: {.stat .warn}
[10]{.v}[寫進流程圖的 camunda 屬性]{.k}
:::
::: {.stat .crit}
[2]{.v}[引擎缺口落點（同一個 bug 兩處）]{.k}
:::
:::

### 4.1 後端（compliance-manager-be）

| 元件 | 現況 | 本案動作 |
|---|---|---|
| `api/flow_control/routes/job_route.py:170` `ProjectJobCreateResource` | 新增任務端點在（`POST /grc/project/<uid>/assessment-object/<ao_uid>/jobs`），註冊於 `api/flow_control/__init__.py:62,117`；只有流程圖編輯器在呼叫 | 端點保留，讓它接受**不帶節點編號**的呼叫 |
| `api/flow_control/serializers/job.py:322` `job_element_uid` | `allow_none=True, load_default=None`——schema 本來就允許不帶 | 不改；改的是下游的 repo |
| `infra/flow_control/repository/flow_control_job_repo_impl.py:673` `create_job()` | 開頭 `if not job_element_uid: return None`（`:698`），路由層轉成 `return_response(False, None)`，前端看不出發生什麼事 | 🔴 **核心改動**：沒帶編號時改成「後端自己在圖上插節點」，插完再往下走原邏輯 |
| 同檔 `create_job()` 冪等防護（`:729`） | 同一流程下同一 `template_job_id` 已有任務 → 更新後回傳、不重建 | 保留。後端自己插節點時編號必定是新的，不衝突 |
| 同檔 `create_job()` 初始狀態判定（`:750`） | 該流程下沒有未完成的任務 → 新任務開 `PROCESSING`；否則 `TODO` | 🔴 判定基準改為看「這個節點的上游是不是分叉閘道／開始事件」，是就開 `PROCESSING`（見 §5.5） |
| 同檔 `_sync_task_to_template_xml()`（`:401`） | 規劃頁存檔後單向回寫圖；定位方式「先找 `job_execution_uid` 屬性、退而求其次比對節點 id」；**同時寫主表與翻譯表** | 保留為**唯一**的圖寫入路徑；寫入欄位收斂成只寫 `name`（D5） |
| 同檔 `delete_job()`（`:779`） | 刪任務時清五張關聯表，再呼叫 `BpmnUtils.remove_user_task_from_xml()` 移節點（註解寫「前端存檔通常已移除，這裡兜底」） | 從兜底升格為**主要路徑**——規劃頁刪除就是走這裡 |
| 同檔 `_compute_can_revert_job()`（`:350-396`） | `len(all_jobs)==1` 直接可退；否則取 `get_next_jobs()` 的**第 0 筆**看是否進行中 | 🔴 改成「所有下一批任務都還沒開始才可以退回」；`len(all_jobs)==1` 那條捷徑一併重檢 |
| `app/flow_control/service/job_service.py:117` `create_job()` | `@transaction`，第一件事 `_require_manager(project_uid)`（`:94`，走 `assert_project_manager`）；接著處理問卷 snapshot、`task_survey` 笛卡爾積、檢測工具綁定與參數加密 | 守門與後續處理沿用不動；加一道規劃期守門（D1） |
| `app/flow_engine/service/workflow_execution_service.py:602` `complete_job()` | 取 `get_next_jobs()`（`:668`）後，`:702` 起的迴圈把每一個下一批任務**直接設 `PROCESSING`**，無兄弟分支檢查。無下一批時（`:677`）才檢查整條流程是否全完成 | 🔴 `:702` 迴圈前插合流等待判定；`:677` 那條分支語意正確，不動 |
| 同檔 `complete_main_workflow_job()`（`:469`） | `:569` 迴圈是與上面一模一樣的寫法；輪次推進在用（`stage_advance_service.py:382` 呼叫） | 🔴 同樣插判定——**兩處都要修**（D3） |
| 同檔 `_assert_round_not_frozen_for_workflow()`（`:155`） | 輪次凍結 gate，目前只擋 `revert_job`（`:794`） | 新增／刪除任務接同一道 gate（D1） |
| `app/flow_control/service/prep_job_generation_service.py:101` `generate()` | 專案啟動時每個查核項目 clone 範本 → 建流程實例 → 建**一筆**任務，節點編號取 `_first_user_task_id()`（`:93`，正規表示式抓第一個 `userTask`，`:54`），再 `inject_job_execution_uid()` 注回圖 | 不改（範本仍是單任務）。在該函式旁留一句註解寫明陷阱：「只建第一個 userTask；多任務範本會靜默漏建；**覆核輪 `launch_reverify` 會 re-clone 第一輪已改過的範本再跑本函式，本案讓 AO 可多任務後此缺口會實際發生**」（延伸項見 §8） |
| `app/flow_control/service/job_batch_complete_service.py:86` | 批次完成：逐筆取 `template_job_id` 呼叫 `complete_job()` | 不改程式，但是風險集中點——批次完成會連續觸發引擎缺口，必測 |
| `app/flow_engine/service/stage_advance_service.py` / `stage_rollback_service.py` | 輪次推進／回退，讀 `workflow_template_xml` | **流程圖不共用**（D-4.1 已查證：輪次圖綁 `project_audit_rounds.workflow_execution_uid`，查核項目圖靠 `workflow_execution_control_mapping`，DEV 全庫零交集）。但 `complete_main_workflow_job()` 是輪次推進實際會跑的程式，B-2.7 動它屬刻意涵蓋；輪次圖只有互斥閘道、無平行閘道，新判定永遠放行、行為不變——**回歸仍要走一次輪次推進**（B-2.7 驗收已含） |
| `infra/readmodel/tasks/vw_user_job_queue.py` | 任務佇列讀模型，`template_job_id` 當一般欄位存（`:77`），無順序假設 | 不受影響 |

### 4.2 共用套件（jedi-flow-engine，現版 1.1.2）

| 元件 | 現況 | 本案動作 |
|---|---|---|
| `bpmn_uilts.py:276` `get_next_jobs()` | 走訪下一批節點：任務直接收；互斥閘道依條件挑一條；**平行閘道（`:326`）把所有出線對應的任務全部收進來** | 展開行為不改。它分不出手上的閘道是「分叉」還是「合流」（BPMN 裡兩者都是 `parallelGateway`，差別只在入線／出線數量）——等待判定另寫一支，不塞進它 |
| `bpmn_uilts.py:213` `get_next_elements()` | 只看一條出線的目標節點類型 | 不改 |
| `bpmn_uilts.py:501` `append_user_task_to_xml()` | 在結束事件之前插一個新方塊，docstring 明寫「接線邏輯（線性流程）」 | 🔴 只會串成一條龍——本案要的平行掛法另新增函式，這支不動（既有 caller 仍在用） |
| `bpmn_uilts.py:574` `inject_job_execution_uid()` | 把任務編號寫進指定節點的 camunda 屬性，已存在就覆蓋 | 直接沿用 |
| `bpmn_uilts.py:613` `remove_user_task_from_xml()` | 移除節點並把線接回去（`前 → 節點 → 後` 變 `前 → 後`） | 擴充：平行分支被刪到只剩一條時，把分叉／合流閘道一併收掉 |
| `bpmn_generator.py:145` `create_bpmn_with_user_tasks()` | **已能產標準平行結構**：開始 → 分叉 → N 個方塊並排 → 合流 → 結束（`:216` 起逐一建方塊與連線） | 直接重用為「重排成標準平行」的實作核心，不另起爐灶 |
| `bpmn_uilts.py:391` `get_job_execution_order()` / `:447` `get_jobs_by_layer()` | 深度優先出順序／廣度優先分層 | 平行結構下「順序」意義不大，但不會壞，不動 |

### 4.3 前端（compliance-manager-fe）

| 元件 | 現況 | 本案動作 |
|---|---|---|
| `ProjectPlanningView.vue:2931` 起 任務區塊 | 逐筆展開面板，欄位齊全；空狀態只顯示「沒有任務」文案（`:2943`），**無新增按鈕** | 🔴 補「新增任務」；空狀態改成可從這裡開第一筆 |
| `ProjectPlanningView.vue:1772` `saveTask()` | 存單筆任務，含名稱必填、檢測工具必選、逐列參數必填、重複分派檢查 | 不動；新增任務走同一支存檔 |
| `ProjectPlanningView.vue:1923` `openBpmnEditor()` | 開新分頁到 `/workflow/workflow-setup`，帶範本編號／流程實例編號／專案編號／查核項目編號 | 保留；按鈕文案改成「編輯流程走向」以符合新定位 |
| `WorkflowSetupEditor.vue:402` `onSaveClick()` | 存 XML → `syncJobExecutions()` → 有新任務就 `patchJobExecutionUids()` → **再存一次 XML** → 重新載入圖 | 🔴 砍掉後三步，只留第一步存 XML；新任務由後端在存檔時建 |
| `WorkflowSetupEditor.vue:489` `syncJobExecutions()` | 逐節點打 PUT／POST | 整支移除 |
| `WorkflowSetupEditor.vue:443` `patchJobExecutionUids()` | 用 DOMParser 直接改 XML 字串寫回任務編號 | 整支移除（改由後端回寫） |
| `WorkflowSetupEditor.vue:635` `onSaveTaskProperties()` ＋屬性面板（`:1032-1045`） | 面板有名稱／工作描述／指南／任務類型四欄，另把設備與部門寫進圖 | 🔴 只留名稱（D4） |
| `TaskSetupView.vue:524` | 也會開 `/workflow/workflow-setup` | 一併確認改動後行為正常 |
| `FlowTemplateEditorView.vue` | 流程範本管理，是另一套系統 | **明確不在範圍** |

---

## 5. 詳細設計 {#design nav="詳細設計"}

### 5.1 原則：一件事只有一個地方說了算

| 這件事 | 真相在哪 | 另一邊是什麼 |
|---|---|---|
| 任務叫什麼、描述、指南、類型、負責人、部門、設備、問卷、工具參數、狀態 | **資料庫 `job_executions` ＋關聯表** | 圖上的 `name` 是顯示副本，由後端單向寫；其餘屬性不再寫也不再讀 |
| 誰先誰後、誰跟誰平行、有沒有二選一分支 | **流程圖 XML** | 資料庫不存拓樸 |
| 圖上這個方塊是哪一筆任務 | **圖上的 `job_execution_uid` 屬性**（後端寫） | — |

::: {.callout .crit}
**🔴 判準一句話：圖上的東西只有「拓樸」和「指回任務的編號」是真的，其他都是印出來給人看的。**

任何一次改動只要讓某個欄位「從圖上讀回來當真值用」，就等於把第二份真相請回來。
:::

### 5.2 新增任務：兩種入口，同一支端點

```{.mermaid cap="圖 1 — 規劃頁新增任務的端到端時序"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
    participant PM as PM（規劃頁）
    participant API as 後端端點<br/>job_route.py:170
    participant SVC as 任務服務<br/>job_service.py:117
    participant REPO as 任務倉儲<br/>repo_impl.py:673
    participant XML as 流程圖改圖<br/>jedi-flow-engine
    participant DB as 資料庫

    PM->>API: 按「新增任務」（只帶名稱，不帶節點編號）
    API->>SVC: create_job(ao_uid, name, job_element_uid=None)
    SVC->>SVC: 檢查專案 manager ＋ 輪次是否仍在規劃期
    SVC->>REPO: create_job(...)
    REPO->>DB: 查這個查核項目的流程範本與流程實例
    REPO->>XML: 依現有拓樸決定插法<br/>（重排成平行 / 掛新分支）
    XML-->>REPO: 新 XML ＋ 新節點編號
    REPO->>DB: 建 job_executions 一筆（狀態依上游判定）
    REPO->>XML: inject_job_execution_uid(新節點, 新任務編號)
    REPO->>DB: 寫回範本 XML（主表＋翻譯表，同一交易）
    REPO-->>SVC: 任務資料
    SVC-->>API: 任務資料
    API-->>PM: 200，任務面板出現、預設展開待填
```

兩種入口走同一支端點，差別只在有沒有帶 `job_element_uid`：

| 入口 | 帶 `job_element_uid` 嗎 | 後端做什麼 |
|---|---|---|
| **規劃頁按「新增任務」** | 不帶 | 自己在圖上插節點、接線、回寫任務編號 |
| **流程圖編輯器拖新方塊、存檔** | 帶（前端存 XML 時圖上已有這個節點） | 直接用這個編號建一筆空任務、把編號回寫圖上 |

::: {.callout .decided}
**保留第二種入口是決策者明示的**

在圖上拖方塊仍然可以開任務——只是屬性面板只能填名稱，其餘細節回規劃頁補。存檔後後端掃圖，發現沒有對應任務的 `userTask` 就建一筆空任務並回寫編號。
:::

### 5.3 插節點規則：看現在的圖長什麼樣決定怎麼插

```{.mermaid cap="圖 2 — 三種情境下新增任務後的拓樸變化"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart TB
    subgraph A["情境一：只有一個任務（專案啟動後的預設樣子）→ 重排成標準平行"]
      direction LR
      A1([開始]) --> A2[任務 1] --> A3([結束])
      A3 -.新增任務 2.-> A4[" "]
      A5([開始]) --> A6{{分叉}}
      A6 --> A7[任務 1]
      A6 --> A8[任務 2 新]
      A7 --> A9{{合流}}
      A8 --> A9
      A9 --> A10([結束])
    end

    subgraph B["情境二：已是標準平行 → 分叉多一條出線、合流多一條入線"]
      direction LR
      B1([開始]) --> B2{{分叉}}
      B2 --> B3[任務 1]
      B2 --> B4[任務 2]
      B3 --> B5{{合流}}
      B4 --> B5
      B5 --> B6([結束])
      B6 -.新增任務 3.-> B7[" "]
      B8{{分叉}} --> B9[任務 1]
      B8 --> B10[任務 2]
      B8 --> B11[任務 3 新]
      B9 --> B12{{合流}}
      B10 --> B12
      B11 --> B12
    end

    subgraph C["情境三：使用者手動排成一條龍 → 掛成合流前的新平行分支，不動既有順序"]
      direction LR
      C1([開始]) --> C2[任務 1] --> C3[任務 2] --> C4{{合流}} --> C5([結束])
      C4 -.新增任務 3.-> C6[" "]
      C7[任務 1] --> C8[任務 2] --> C9{{合流}}
      C10{{分叉}} --> C11[任務 3 新] --> C9
    end
```

規則寫成白話：

1. **圖上只有一個任務** → 重排成標準平行：開始 → 分叉 → 兩個任務並排 → 合流 → 結束。重用 `bpmn_generator.py:145` 的產圖邏輯，**既有任務的 `job_execution_uid` 必須原樣保留**。
2. **圖上已是標準平行結構** → 分叉閘道多一條出線、合流閘道多一條入線，新方塊掛中間。
3. **使用者已排成一條龍或加了二選一分支** → **不動他排好的順序**，把新任務掛成「合流前的一條新平行分支」；圖上沒有合流閘道時，在結束事件前補一個（D2）。

::: {.callout .warn}
**為什麼不是「一律重排成標準平行」**

使用者若刻意把任務排成「先做 A 才能做 B」，一按新增就被重排回全平行，等於系統擅自推翻他的設計，而且沒有任何提示。
:::

### 5.4 刪除任務：兩個入口、同一支 `delete_job()`

| 入口 | 觸發 | 後端做什麼 |
|---|---|---|
| **規劃頁按刪除** | `DELETE /…/jobs/<job_uid>` | `job_service.delete_job()` → 清 `task_surveys` 與 snapshot 問卷 → `repo.delete_job()` 清五張關聯表、移節點、**硬刪** job |
| **編輯器刪方塊後存 XML** | XML 存檔端點 | diff 出「舊 XML 有、新 XML 沒有」的 `job_execution_uid` → 對每一筆呼叫**同一支** `delete_job()`（D7） |

::: {.callout .crit}
**🔴 兩個入口共用同一支 `delete_job()` 是硬要求，不可各寫一套**

`repo.delete_job()`（`flow_control_job_repo_impl.py:779`）負責清 `task_assignees`、`job_execution_org_units`、`job_execution_devices`、`job_execution_surveys`、`job_execution_comments` 五張表——這四張綁定表的外鍵已從 CASCADE 改為軟參照，**清理責任在這支函式裡**，少刪一張就留下永遠查不到任務的孤兒列。另外 `job_service.delete_job()` 上面還有一層 `_cleanup_task_surveys()` 與 snapshot 問卷清理。

繞過去自己寫刪除，症狀是「任務不見了但關聯資料還在」，而且沒有錯誤訊息。
:::

**刪除語意與守門（實查，兩入口一致）**：

- **硬刪除**——`repo_impl.py:808` 是 `session.delete(job)`，不是改狀態成 `CANCEL`。
- **守門只有一道**：`job_service.py:179` 的 `_require_manager(project_uid)`。**現況沒有任何「已有進度不准刪」的檢查**——`PROCESSING`／`COMPLETED`、已有證據、已指派負責人都刪得掉。**本案沿用此規則、不新增限制**（新增限制屬產品行為變更，不在本案；兩入口一致才是本案的要求）。本案另外加的只有 D1 的規劃期守門，兩入口同樣適用。
- **兩個入口的身分判準本來就不同，這是刻意的**：編輯器刪任務走範本能力守門（`module-frame.update`），**不要求專案管理人身分**（決策者 2026-09-20 裁）。存檔端點只拿得到範本 uid，而範本反查專案在現況資料上多半查不到（DEV 上 11,710 個有任務的流程實例只有 910 個查得回專案），沒有 `project_uid` 就問不出「你是不是這個專案的管理人」；租戶隔離由 RLS 負責。
- Drive 資料夾封存是 route 層的 best-effort 副作用（`job_route.py:161-166`）。**編輯器存檔這條路徑也要接上**，否則同一個刪除動作在兩個入口對雲端資料夾的處置不同。

**圖的處理**：

- 一般情形：`前 → 節點 → 後` 接回 `前 → 後`（`remove_user_task_from_xml()` 現有行為，idempotent——編輯器那條路徑節點已經不在 XML 裡，呼叫它不會出錯）。
- 平行分支刪到**只剩一條**：分叉與合流閘道只剩單進單出 → 把兩個閘道一併收掉、接回線性。
- 刪到**只剩最後一個**：擋下，不允許刪到零（D6）。規劃頁的刪除鈕在只剩一筆時停用並顯示原因；編輯器存檔若 diff 結果會刪到零，整筆存檔拒絕、XML 與任務都不動。

### 5.5 初始狀態判定（R3）

平行分支上的任務**應該都可以馬上開始做**，不該排隊。判定基準從「該流程下有沒有未完成任務」改成**看這個節點的上游**：

| 新節點的上游是 | 初始狀態 |
|---|---|
| 開始事件，或分叉閘道（`parallelGateway` 出線數 > 1） | `PROCESSING` |
| 另一個 `userTask`（使用者排的一條龍中段） | `TODO` |

判定要用的拓樸資訊由套件的判定函式提供（§5.7），BE 不自己解析 XML。

::: {.callout .crit}
**🔴 不改這裡的症狀：平行下第二個以後的任務全部變 `TODO` 排隊**

畫面上看起來是「有三個平行任務，但只有一個能做」——與本案要的「誰先做都行」完全相反，而且沒有錯誤訊息。
:::

### 5.6 可否退回判定（R4）

`_compute_can_revert_job()` 現行邏輯取 `get_next_jobs()` 的第 0 筆，平行結構下「下一批」有多筆，只看第一筆會誤判。改成：**所有下一批任務都還沒開始，才可以退回**；`len(all_jobs)==1` 那條捷徑在多任務後失效，一併重檢。

### 5.7 🔴 合流閘道等待判定（D3）

::: {.callout .crit}
**缺口：合流閘道不等兄弟分支——第一條分支完成，合流後面的任務就被啟動**

`complete_job()`（`workflow_execution_service.py:602`）完成一個任務後拿 `get_next_jobs()`（`:668`），接著 `:702` 起的 `for job in next_jobs:` 迴圈**把每一個都直接設成 `PROCESSING`**，中間沒有任何「兄弟分支都完成了嗎」的檢查。

**同一個缺口在兩個地方**：`complete_main_workflow_job()`（同檔 `:469`）的 `:569` 迴圈是一模一樣的寫法，輪次推進在用。
:::

**成立條件**：合流閘道後面**還有** `userTask`。本案要產的標準結構（分叉 → N 任務 → 合流 → 結束）中合流後面是結束事件，`get_next_jobs()` 回空，走的是 `:686` 那條「檢查整條流程是否全部完成」的分支——**那條是對的**。所以預設結構不會立刻踩到，但本案會放大它：

1. 編輯器保留改線能力，使用者只要在合流後接一個「主管覆核」任務，缺口立刻成立。
2. 批次完成（`job_batch_complete_service.py:86`）連續呼叫 `complete_job()`，把時序放到最大。

**修法**：在 jedi-flow-engine 新增拓樸判定函式，回答「這個任務的上游是不是合流閘道／該閘道所有入線分支的任務是否都已完成或取消」；`complete_job()` 與 `complete_main_workflow_job()` 兩處的迴圈在設 `PROCESSING` 之前先問它，答否就跳過不啟動。

::: {.callout .decided}
**判定放套件、呼叫點在主專案，是刻意的分界**

拓樸語意（誰是分叉、誰是合流、兄弟分支有哪些）只有看得到整張圖的那一層能回答，放在套件；「任務算不算完成」是主專案的業務狀態，留在主專案。兩邊各自回答自己知道的事。
:::

### 5.8 流程圖存檔端點：三向 diff

編輯器存 XML 時，後端拿**新 XML 的 `userTask` 節點**對照**該流程實例現有的任務清單**，逐一歸到下面四種處置之一。這張表是存檔端點的完整行為規則：

| 圖上的節點 | DB 有沒有對應任務 | 判定 | 後端做什麼 |
|---|---|---|---|
| 有 `userTask`、**沒有** `job_execution_uid` 屬性 | — | **新增意圖** | 建一筆空任務（名稱取圖上的 `name`）→ `inject_job_execution_uid()` 回寫編號 |
| 有 `userTask`、有 uid，DB 查得到 | ✅ | **保留** | 只更新拓樸（XML 照存），任務資料一個欄位都不動 |
| — | ✅ DB 有、但新 XML 裡**找不到這個 uid** | **刪除意圖**（D6／D7） | 呼叫 `delete_job()` 刪掉它（同一支、同一守門，見 §5.4）；**若刪完會剩零任務 → 整筆存檔擋下** |
| 有 `userTask`、有 uid，但 DB 查不到（uid 失效） | ❌ | **錯誤** | 擋下存檔，指出是哪個節點帶了查不到的編號 |
| 一個 `userTask` 節點在 DB 對到**多筆**任務（同 `template_job_id`，舊版編輯器殘留） | ✅ 多筆 | **錯誤** | 擋下存檔，指出是哪個節點對到哪幾筆任務；不可判成刪除意圖靜默清掉（決策者 2026-09-19 裁） |

::: {.callout .crit}
**🔴 「XML 少了 uid」是刪除，不是孤兒**

把它當錯誤擋下，等於宣告「在圖上刪方塊沒有用」——而編輯器的定位就是管走向，刪方塊正是改走向。孤兒的定義因此收窄成上表最後一列：**帶著一個查不到的編號**。
:::

另外兩項驗證維持，任一不過就退回並**指出哪個節點有問題**：

| 檢查 | 為什麼 | 不擋的症狀 |
|---|---|---|
| `job_execution_uid` 不重複 | 使用者可能複製貼上方塊，連帶複製別人的任務編號 | 兩個方塊指向同一筆任務，完成其中一個另一個跟著變狀態 |
| 每個任務節點從開始事件走得到 | 接線接錯、方塊沒連上 | 完成前一個任務時引擎找不到它，流程卡住 |

**輪次凍結反查不到時放行**（決策者 2026-09-20 裁）：存檔端點只有範本 uid，輪次狀態要從流程實例反查（`workflow_execution_control_mapping.round_id`，或任一任務的指派人所屬專案的最新輪）。兩條都查不到就**放行**，不擋下。理由是查不到的多半是沒有輪次、沒有指派人的舊資料（DEV 上約占一半），那些本來就沒有輪次可凍結；拿不到就擋等於把規劃期的正常存檔一起擋掉，而使用者看到的只是「存不了」。代價是這類舊資料上的凍結守門形同不存在——可接受，因為凍結要保護的稽核範圍在那些資料上並不成立。

**存檔時的執行順序是「先建、再寫圖、最後刪」**，不可改成先刪。刪除的兩道守門（倉儲數任務筆數、套件數圖上節點）都是**逐筆**問「刪完還剩不剩」，只有上層那筆總帳知道新的會補回來。整批換掉（現有 2 筆、刪 2 筆、建 1 筆）先刪的話，刪到第二筆時新的還沒建、圖上也還沒有它，兩道守門都會把這筆合法存檔擋成「至少要留一個任務」。先寫圖還讓刪除走到「節點已不在圖上」的 no-op 分支——編輯器送上來的 XML 本來就沒有那些被刪的方塊。連帶要求：**刪除讀圖一律原生 SQL 現讀**，不可拿 ORM 載過的 `WorkflowTemplate.xml`（寫圖走原生 SQL，ORM 快取那份會過期，拿舊圖改再寫回會把剛建的新方塊整段蓋掉，且沒有錯誤訊息）。

**刪掉哪些任務要回報給 route**：編輯器刪方塊＝刪任務，雲端資料夾的處置必須與規劃頁一致（見 §5.4），存檔服務因此回傳被刪任務的 uid 清單，由 route 比照規劃頁呼叫 Drive 歸檔（best-effort，沒接 Drive 的租戶跳過）。

**整個 diff（建／刪／存 XML／雙寫翻譯表）必須在同一個交易內**——中途失敗時圖與任務要一起回到存檔前的狀態，不可以出現「任務刪了但 XML 沒存」。

### 5.9 🔴 範本 XML 是可翻譯欄位，寫圖必須雙寫（R7）

規劃頁走原生 SQL 讀**主表**、編輯器走倉儲的語系備援讀**翻譯表**。**只更新主表，編輯器會永遠看到舊圖。**

所有寫圖路徑（插節點、移節點、存檔時回寫 uid、**專案建立時把任務編號寫回範本**）都走倉儲的 `write_template_xml()` **同時寫主表與翻譯表**，並且與建任務落在**同一個交易**內。專案建立那條路徑若漏了翻譯表，症狀是新專案一開編輯器直接存檔就被三向 diff 判成「任務全刪」而擋下。

### 5.10 編輯器鎖定範圍

| 還能做 | 不能做 |
|---|---|
| 改線（把平行拉成一條龍、加二選一分支、調整順序） | 填任務類型、問卷、設備、部門、工具參數（回規劃頁） |
| 拖新方塊進來（只填名稱） | 直接改任務的負責人與狀態 |
| **刪方塊（＝刪任務，與規劃頁刪除等效，D7）** | — |

存檔後前端只存一次 XML，取回後端回傳的新 XML 重新載入圖；規劃頁分頁在重新取得焦點時重抓任務清單。

### 5.11 套件開發方式

jedi-flow-engine 的改動**開發期一律走 poetry path dependency**（主專案 `pyproject.toml` 取消 path 形式註解，改動後重啟 BE 即生效）。**path 改動不 commit**；功能整體完成後還原 pin 一起 commit。**發版與推 Nexus 一律等決策者明示**，runner 不自行 bump。

---

## 6. 拆分（4 子需求 × 16 子任務） {#breakdown nav="拆分"}

依賴鏈：**D-4.1（開工前查證）→ A（套件改圖工具）→ B（BE）→ C（FE，B 完成後兩張可並行）→ D-4.2／D-4.3（驗收）**

::: {.callout .warn}
**這是拆分表，不是派工單**

實際開卡、切棒、派工要等決策者發令。跨 3 repo，各 repo 分開 commit；套件的 path dependency 改動不 commit。
:::

### FR-112.A jedi-flow-engine：BPMN 拓樸改寫工具 — 依賴：D-4.1

| # | 子任務 | 驗收條件 | 依賴 | Repo |
|---|---|---|---|---|
| **A-1.1** | 新增兩支插節點函式：①把單任務流程重排成標準平行（重用 `bpmn_generator.py:145` 產圖邏輯）；②在合流閘道前掛一條新平行分支（無合流閘道時於結束事件前補一個） | 單一任務的圖 → 重排 → 產出「開始→分叉→2任務→合流→結束」，且**既有任務的 `job_execution_uid` 原樣保留**；標準平行圖 → 掛新分支 → 分叉出線數與合流入線數都 +1；一條龍圖 → 掛新分支 → **既有先後順序完全不變** | — | jedi-flow-engine |
| **A-1.2** | 擴充 `remove_user_task_from_xml()`：平行分支刪到只剩一條時收掉分叉／合流閘道、接回線性；圖上只剩最後一個 `userTask` 時拒絕移除（拋錯，由呼叫端轉成前置條件錯誤） | 三分支平行圖 → 刪到剩一條 → 閘道消失、接回線性；對只剩一個任務的圖再呼叫 → 拋錯、XML 不變 | — | jedi-flow-engine |
| **A-1.3** | 新增拓樸判定函式：①「這個節點的上游是不是分叉閘道／開始事件」（供初始狀態判定）；②「這個節點的上游是不是合流閘道、該閘道所有入線分支各自的末端任務有哪些」（供合流等待判定）。全部補單元測試 | 給「兩條平行分支、合流後還有一個任務」的圖：查合流後任務 → 回傳兩條分支的末端任務編號；查分叉後任務 → 回傳「上游是分叉」。**不動 `get_next_jobs()` 既有展開行為** | — | jedi-flow-engine |

### FR-112.B compliance-manager-be：新增／刪除、守門與引擎修正 — 依賴：FR-112.A

| # | 子任務 | 驗收條件 | 依賴 | Repo |
|---|---|---|---|---|
| **B-2.1** | `create_job()` 不帶 `job_element_uid` 的入口：依現有拓樸選插法（§5.3）→ 插節點 → 建任務 → `inject_job_execution_uid()` → **主表＋翻譯表雙寫**，全部同一交易 | Swagger 打新增端點**不帶** `job_element_uid` → 200；資料庫查該範本 XML 多一個方塊且帶新任務編號；**打開編輯器看得到新方塊**（證明翻譯表也寫了） | A-1.1、A-1.3 | BE |
| **B-2.2** | 規劃頁刪除入口：`delete_job()` 升為刪除主路徑，清五張關聯表 → 移節點 → 必要時收閘道 → 雙寫。**守門沿用既有 `_require_manager`，不新增「已有進度不准刪」限制**，但新增「至少保留一個」前置條件（§5.4、D6） | 連續新增三筆 → 刪一筆 → 圖剩兩條分支、任務與五張關聯表都清乾淨；再刪到剩一筆 → 閘道收掉接回線性；對最後一筆按刪除 → 412 擋下、任務與圖不變；刪一筆 `PROCESSING` 的任務 → 照現況允許（行為與改動前一致） | A-1.2 | BE |
| **B-2.3** | 流程圖 XML 存檔端點做**三向 diff**（§5.8）：①無 uid 的新節點 → 建空任務並回寫編號；②**新 XML 裡消失的 uid → 呼叫同一支 `delete_job()` 刪任務**（含 Drive 資料夾封存副作用，與規劃頁入口一致）；③其餘只更新拓樸。uid 重複／uid 查不到／節點走不到／**刪到零任務** → 擋下並指出原因。全部同一交易，回傳新 XML | 拖新方塊存檔 → 資料庫多一筆空任務、名稱是圖上打的；**刪掉一個方塊存檔 → 對應任務與五張關聯表全部清乾淨**（與規劃頁刪除結果逐欄相同）；帶查不到的 uid → 被擋下且回應指得出是哪個節點；複製貼上重複 uid → 被擋下；交易中途失敗 → 圖與任務一起回到存檔前狀態 | A-1.3、B-2.2 | BE |
| **B-2.4** | `_sync_task_to_template_xml()` 收斂成只寫 `name`；圖上舊屬性的讀取路徑停用（不清既有 XML） | 規劃頁改任務描述存檔 → 圖上 `name` 更新、其餘 camunda 屬性**維持原值不被刷新**；既有專案流程行為不變 | — | BE |
| **B-2.5** | 初始狀態判定（§5.5）與可否退回判定（§5.6）改依拓樸 | 同一查核項目連續新增三次 → 三筆任務**全部是進行中**（不是兩筆 TODO）；平行下所有下一批任務都未開始才回「可退回」 | A-1.3 | BE |
| **B-2.6** | 規劃期守門：新增／刪除任務接上 `_assert_round_not_frozen_for_workflow()`，凍結後回明確 error code 與訊息 | 輪次凍結後打新增端點 → 被擋、回傳可辨識的 error code（FE 據此顯示停用原因）；規劃期打 → 正常 | — | BE |
| **B-2.7** | 合流閘道等待修正**兩落點**：`complete_job():702` 與 `complete_main_workflow_job():569` 迴圈前插 A-1.3 的判定 | 「兩條平行分支、合流後接一個覆核任務」的圖：完成第一條 → 覆核任務**不得**變進行中；兩條都完成 → 才變進行中。**兩支各驗一次**（`complete_main_workflow_job` 走輪次推進動線） | A-1.3 | BE |
| **B-2.8** | `prep_job_generation_service._first_user_task_id()` 旁補註解寫明陷阱（只建第一個／覆核輪 re-clone 會複製此問題，見 §4.1）；走 `ddd-compliance-reviewer` 掃本子需求全部 diff | reviewer 無阻斷級發現；註解為一行、只寫陷阱不寫施工日誌 | B-2.1～B-2.7 | BE |

### FR-112.C compliance-manager-fe：規劃頁按鈕與編輯器瘦身 — 依賴：FR-112.B

| # | 子任務 | 驗收條件 | 依賴 | Repo |
|---|---|---|---|---|
| **C-3.1** | 規劃頁任務區塊補「新增任務」（空狀態與有任務時都有）與每筆的刪除（帶確認）；新任務建立後自動展開、聚焦名稱欄；凍結時按鈕停用＋**留住原因提示**（照 FR-110 做法，不彈 toast）；超過 20 筆顯示提示 | 點查核項目 → 新增 → 填名稱存檔 → 重新整理仍在；刪除 → 確認後消失、重新整理不回來；凍結狀態按鈕停用且**看得到原因**（不是無聲失效） | B-2.1、B-2.2、B-2.6 | FE |
| **C-3.2** | `WorkflowSetupEditor.vue` 屬性面板只留名稱（移除工作描述／指南／任務類型／設備／部門的編輯與寫入） | 點方塊 → 右邊只剩名稱欄；改名稱存檔 → 規劃頁該任務名稱跟著變 | B-2.4 | FE |
| **C-3.3** | 編輯器存檔瘦身：`onSaveClick()` 只存一次 XML，移除 `syncJobExecutions()` 與 `patchJobExecutionUids()`；**刪節點能力保留不鎖**，但存檔前提示「刪掉的方塊對應的任務會一併刪除」；顯示後端回的驗證訊息（哪個節點有問題）；規劃頁分頁重新取得焦點時重抓任務清單；`TaskSetupView.vue:524` 進來的路徑一併驗 | 拖新方塊存檔 → 回規劃頁（不必手動重新整理）看得到新的空任務；**刪方塊存檔 → 規劃頁對應任務消失**且存檔前有提示；帶查不到 uid 的節點存檔 → 畫面出現明確訊息指出節點 | B-2.3 | FE |

### FR-112.D 驗收與回歸 — D-4.1 最先做，D-4.2／D-4.3 最後做

| # | 子任務 | 驗收條件 | 依賴 | Repo |
|---|---|---|---|---|
| **D-4.1** | 🔴 **開工前查證**：`stage_advance_service.py` 與 `stage_rollback_service.py` 讀的 `workflow_template_xml` 是否與查核項目底下的收證據流程**同一張圖**（唯讀查證，不改任何檔） | 產出一頁結論寫進母卡，明確回答「是／否」與依據（檔名＋行號）。**若為同一張圖 → 停下回報，整案範圍重估**（`get_next_jobs()` 與初始狀態判定的改動會連帶影響輪次主流程） | — | BE（唯讀） |
| **D-4.2** | 端到端手測清單實跑（見 §7.2），批次一輪跑完再統一修 | 清單全項通過；有打折處（環境限制只能模擬）**當場在回報中寫明**，不報成通過 | C-3.1～C-3.3 | 手測 |
| **D-4.3** | e2e 回歸與新情境（母卡收口動作，arc 完成後另開一棒） | 專案啟動建任務、輪次推進、批次完成、任務佇列四條既有動線在平行結構下綠燈；新增「兩個平行任務只完成一個不得放行」情境 | D-4.2 | compliance-manager-test |

**子任務數**：jedi-flow-engine 3、BE 9（B 八張 ＋ D-4.1）、FE 3、test 1，另手測 1 張不綁 repo — 合計 **16**。

---

## 7. 端到端驗收 {#acceptance nav="端到端驗收"}

### 7.1 前置條件（不過就不開工）

**D-4.1 的查證結論必須是「輪次推進／回退讀的不是查核項目那張圖」。** 若共用同一張，本案範圍要重新評估後再開卡。

### 7.2 手測清單（決策者親手可驗）

1. 起一個新專案 → 規劃頁點任一查核項目 → 按「新增任務」→ 填名稱存檔 → **重新整理仍在**。
2. 打開流程圖編輯器 → 新方塊與原任務**並排**（平行），不是串在後面。
3. 同一查核項目連續新增到三筆 → 三筆**全部是進行中**，沒有任何一筆在排隊。
4. 只完成其中一個 → 該查核項目**不可以**變已完成；全部完成 → 才變已完成。
5. 批次完成一次勾兩個平行任務 → 狀態正確，沒有任務提前跳成進行中。
6. 在編輯器把流程改成「兩條平行 → 合流 → 主管覆核」→ 完成第一條 → **覆核任務不得變進行中**；兩條都完成 → 才變。
7. 編輯器點方塊 → 右邊**只剩名稱欄**，沒有任務類型／問卷／設備／部門。
8. 編輯器拖新方塊存檔 → 回規劃頁看得到一筆新的空任務，名稱是圖上打的那個。
9. 編輯器刪掉一個方塊存檔 → 回規劃頁該任務**消失**，且五張關聯表都清乾淨（與規劃頁刪除結果相同）。
10. 手改 XML 讓某節點帶一個查不到的 uid → 存檔被擋下，訊息**指得出是哪個節點**。
11. 規劃頁刪任務 → 圖上方塊消失；刪到剩一筆 → 閘道收掉、刪除鈕停用，且**不必把滑鼠移過去就能在按鈕旁看到**「查核項目至少要有一個任務」；編輯器把最後一個方塊刪掉存檔 → 被擋下、圖與任務不變，訊息顯示中文（不是 error code）。
12. 輪次凍結後 → 新增／刪除按鈕停用，**畫面看得到原因**；直接打後端端點也被擋。
13. 規劃頁新增一個「檢測工具執行」型任務 → 參數**確實加密落庫**（查 DB 欄位不是明文）。
14. 規劃頁改任務描述存檔 → 圖上 `name` 更新、其餘舊屬性未被刷新。
15. 既有專案（圖上帶完整舊屬性）打開規劃頁與編輯器 → 行為正常，無錯誤。

### 7.3 回歸範圍

專案啟動建任務、輪次推進、輪次回退、批次完成、任務佇列——五條既有動線在平行結構下重跑。

---

## 8. 延伸 FR 清單 {#follow-ups nav="延伸項"}

| 項目 | 在做什麼 | 為什麼會長出來 | 狀態 |
|---|---|---|---|
| **編輯器 UI 優化／屬性面板擴充** | 流程圖編輯器的操作體驗重做，並重新評估屬性面板要不要放回類型之類的欄位 | 決策者原話：「目前的 UI 設計不是很好用，之後再來優化」——本案先把面板收到只剩名稱以止住第二份真相，體驗問題另案處理 | 未開案 |
| **稽核中補任務** | 輪次凍結後仍能追加任務的動線 | D1 把新增限制在規劃期，但「稽核到一半發現漏了一項」確實會發生；牽涉補件流程與報表口徑 | 未開案 |
| **多任務流程範本** | 讓流程範本本身就能定義多個任務 | `prep_job_generation_service._first_user_task_id()` 只抓第一個 `userTask`，範本做成多任務會靜默漏建 | 未開案（本案只留註解標註前提） |
| **覆核輪對多任務查核項目的任務生成** | 開覆核輪時，對已有多個平行任務的查核項目，每個方塊各建一筆任務 | 覆核輪 re-clone 第一輪範本後重跑 `generate()`，它只認第一個方塊——本案讓查核項目可多任務後，開覆核輪會出現「圖上三個方塊、資料庫一筆任務」且無錯誤訊息（D-4.1 查證發現）。決策者 2026-09-19 裁留延伸、不擴本案 | 未開案 |
| **圖上舊 camunda 屬性清理** | 掃全部範本清掉不再讀的屬性 | D5 選擇「不清、讀時忽略」，屬性會停在那裡不再增生 | 不排程（風險大於收益，除非有明確需要） |

閱讀順序：先看本頁 → 實作完成後看 `FINAL-SPEC.md` → 要追每一棒做了什麼看 `handoff/FR-112-LOG.md`。

---

## 9. 這份文件的定位 {#positioning nav="定位"}

| 你想知道 | 看哪份 |
|---|---|
| 為什麼要做、決策怎麼裁、被排除了什麼 | **本文件** |
| 完整的選項推演與業界對照 | [`discussion.html`](./discussion.html) |
| 做完之後最後長什麼樣 | `FINAL-SPEC.md`（實作完成後產出） |
| 每一棒做了什麼、踩了什麼坑 | `handoff/FR-112-LOG.md`（待建） |
| 流程圖屬性與 API 欄位的對照 | FE `docs/guides/grc-page-data-models.md:40-47` |
