---
title: 流程疆界重新分析 — flow_engine／flow_control 逐檔歸屬判定
brand: Guidant AI · **FR-104** 流程疆界
eyebrow: FR-104 · CM-1823 · 實查日期 2026-09-16 · HEAD `98d756bb`
h1: 114 個檔，哪些現在就能清
lede: 主專案裡管「流程」的程式分散在兩個地方——`flow_engine`（流程引擎）與 `flow_control`（稽核流程控管）。這份文件把兩包 114 個檔逐支查過，判定每一支該歸誰，每一筆判定都附依賴證據。**不動任何程式碼**；要回答的問題是「哪些現在就能清、哪些要等設計、哪些本來就該留著」。
chips: [{text: 實查基準 98d756bb, kind: accent}, {text: 114 檔／14350 行, kind: accent}, {text: 立即可做 2 項, kind: ok}, {text: 兩項待裁已裁 2026-09-16, kind: ok}]
footer: FR-104 · CM-1823 · 引用前請先重驗數字（見 §1 末「怎麼重跑」）
---

## 30 秒版 {#tldr nav="30 秒版"}

1. **地貌已經跟半個月前完全不同。** FR-069 第四階段把稽核與任務平台的主體抽成兩個套件（`jedi-compliance-audit`／`jedi-task-platform`），主專案這兩包從 226 檔縮到 114 檔。舊設計稿（2026-08-31）的每個數字都已失效，**本文取代它**。
2. **判準是依賴方向，不是檔名。** 每一支看「它 import 了誰、被誰 import」，判給五類之一：引擎／平台／稽核／主程式編排／還看不準。
3. **結果**：引擎 57 支、平台 19 支、稽核 15 支、主程式編排 12 支、還看不準 0 支，另有 11 支空檔（`__init__.py`，零行）。
4. **現在就能清的有兩項**（§5），都是低風險、不牽其他模組的。首腦初判的兩支裡，`flow_template_app_service` **成立**，`workflow_template_snapshot_service` **推翻**——它的唯一消費者是稽核套件，決策者 2026-09-16 裁歸稽核套件（§7）。
5. **要設計才能動的有四項**（§6），最大的一塊是 `workflow_execution_service`（1,321 行）。
6. **端點一條都不能少。** 30 條對外端點（19＋11）四層查過，**沒有一條是死的**。

::: {.callout .warn}
**引用本文的數字前請先重驗。** 舊稿失效的教訓正是「它自己就是為了更正上一份的數字而做的實查，半個月後也過期了」。本文所有數字的產生指令列在 §1 末，重跑一次再引用。

**本文有兩節刻意帶時序敘述**（其餘各節只寫現況）：§3「與舊判定的差異」與 §4「已裁示事項現況」——比對舊判定與標註裁示落點正是本文的核心用途，去掉就無從驗收。§1 有兩處端點註明「已註解退役（卡號）」，那是現況陳述加可查證出處。
:::

---

## ① 現況盤點 {#current nav="現況盤點"}

**這節在講什麼：兩包現在有多大、對外開了哪些門、誰在走那些門。**

### 實查基準 {#baseline nav="-"}

| 項目 | 值 |
|---|---|
| 實查日期 | 2026-09-16 |
| HEAD commit | `98d756bb` |
| branch | `feature/review` |
| 工作目錄狀態 | clean（`git status` 無異動） |

### 八層規模 {#scale nav="-"}

| 層 | 檔數 | 行數 |
|---|---|---|
| `api/flow_engine` | 17 | 1,320 |
| `app/flow_engine` | 16 | 3,041 |
| `domain/flow_engine` | 27 | 852 |
| `infra/flow_engine` | 20 | 1,118 |
| **flow_engine 小計** | **80** | **6,331** |
| `api/flow_control` | 7 | 1,151 |
| `app/flow_control` | 17 | 3,995 |
| `domain/flow_control` | 0 | 0 |
| `infra/flow_control` | 10 | 2,873 |
| **flow_control 小計** | **34** | **8,019** |
| **合計** | **114** | **14,350** |

`domain/flow_control` 是空目錄——該層的實體與領域服務已全數在 `jedi-compliance-audit` 與 `jedi-task-platform` 兩個套件內。

### 對外端點 30 條 {#routes nav="端點清單"}

::: {.callout .crit}
**數端點必須用語法解析，不能用字串比對。** `api.add_resource(` 常寫成跨行，用 `grep -c` 會漏抓。本文的數字來自 `re.finditer(r'^    api\.add_resource\(', s, re.M)` 對兩支 `__init__.py` 的掃描。
:::

**`api/flow_engine`：19 條啟用，1 條已註解退役**

| # | 路徑 | 吃哪支 service | 前端誰在用 |
|---|---|---|---|
| 1 | `GET/PUT /flow-engine/process/comments/<id>` | `workflow_execution_service` | `ProcessDiscussBox.vue` |
| 2 | `POST /flow-engine/task/complete/<id>` | 同上 | `JobExecutionDrawer.vue` |
| 3 | `POST /flow-engine/task/revert/<id>` | 同上 | 同上 |
| 4 | `GET /flow-engine/process-definition/<uid>` | 同上 | `WorkflowSetupEditor.vue` |
| 5 | `GET /flow-engine/task/queue` | `app/readmodel/my_jobs_app_service` | `utils/userUtil.js` |
| 6 | `POST /job-evidences` | `job_evidence_service` | `JobExecutionDrawer.vue`／`RoundAuditReviewView.vue` |
| 7 | `POST /job-evidence` | 同上 | 同上 |
| 8 | `GET/PUT/DELETE /job-evidence/<uid>` | 同上 | 同上 |
| 9 | `GET /flow-engine/stage-objects` | `stage_object_app_service`（16 行薄殼） | `FlowTemplateService.js` |
| 10–15 | `/flow-engine/flow-templates`（list／create／validate／detail／duplicate／publish／unpublish） | `flow_template_app_service` | `FlowTemplateService.js` |
| 16 | `GET /project/<p>/audit-round/<r>/stage/info` | `stage_advance_service` | `StageService.js`／`FlowPhaseBanner.vue` |
| 17 | `POST .../stage/advance` | 同上 | 同上 |
| 18 | `POST .../stage/rollback` | `stage_rollback_service` | 同上 |
| 19 | `GET .../stage/transitions` | 同上 | 同上 |
| — | `PUT /job-execution/task/link` | — | **已註解退役**（CM-1498，兩張 mapping 表已 DROP） |

**`api/flow_control`：11 條啟用，1 條已註解退役**

| # | 路徑 | 吃哪支 service | 前端誰在用 |
|---|---|---|---|
| 1 | `GET /grc/jobs/my/projects/menu` | `project_service` | `MyTasksView.vue` |
| 2 | `POST /grc/projects/list` | 同上 | `ProjectListView.vue`／`DashboardView.vue`／`ProjectSummaryReportManage.vue` |
| 3 | `POST /grc/projects/batch-delete` | 同上 | `ProjectListView.vue` |
| 4 | `GET/PUT/DELETE /grc/project/<uid>` | 同上 | 9 個頁面／元件 |
| 5 | `GET /grc/project/<p>/assessment-plans/menu` | `assessment_plan_app_service` | `ImportDocxPage.vue`／`ProjectAuditorOverview.vue` |
| 6 | `GET/PUT /grc/project/<p>/ap/<ap>` | 同上 | `ProjectAuditorOverview.vue` |
| 7 | `GET /grc/project/<p>/ap/<ap>/dashboard` | 同上 | 同上 |
| 8 | `POST .../control-group/<g>/control/<c>/assessment-object/<ao>/jobs/list` | `job_service` | `TaskSetupView.vue` |
| 9 | `GET/PUT/DELETE /grc/project/<p>/job/<j>` | 同上 | `JobExecutionDrawer.vue`／`TaskSetupView.vue`／`RoundAuditReviewView.vue` |
| 10 | `POST /grc/project/<p>/assessment-object/<ao>/jobs` | 同上 | `TaskSetupView.vue` |
| 11 | `POST /grc/jobs/my/list` | 同上 | `MyTasksView.vue`／`DashboardView.vue` |
| — | `GET /grc/projects/menu` | — | **已註解退役**（FR-038 dark stub） |

::: {.callout .ok}
**30 條全部是活的**，四層查法逐條核過：

1. **前端常數**（`src/config/api/api.js`）：8 個 flow-engine 常數＋5 個 GRC 常數，每個都至少一個 `.vue`／`.js` 檔在用。
2. **前端路由**（`src/config/router/index.js`）：對應頁面皆掛在路由上。
3. **資料庫選單**（`public.ui_routes`，DEV 唯讀查詢）：`flow-template-manage`／`project-list`／`my-tasks`／`project-dashboard`／`audit-manage` 皆 `enable=1`。
4. **非瀏覽器呼叫者**：① `common/middleware/license_readonly_mw.py:116` 的唯讀租戶白名單收了 `/flow-engine/flow-templates/validate`——**拔掉這條端點會讓唯讀租戶的流程範本驗證靜默失效**；② E2E（`compliance-manager-test/site-regression/`）有 23 條路徑在打，含 `stage/advance`／`stage/rollback`／`stage/transitions`／`flow-templates` 全鏈。
:::

### 怎麼重跑 {#howto nav="-"}

```bash
# 八層檔數與行數
for d in api/flow_engine app/flow_engine domain/flow_engine infra/flow_engine \
         api/flow_control app/flow_control domain/flow_control infra/flow_control; do
  n=$(find $d -name '*.py' -not -path '*__pycache__*' 2>/dev/null | wc -l)
  l=$(find $d -name '*.py' -not -path '*__pycache__*' -print0 2>/dev/null | xargs -0 cat | wc -l)
  echo "$d	$n 檔	$l 行"
done

# 端點數（必須用語法解析，不可用 grep -c）
python3 -c "
import re
for f in ['api/flow_engine/__init__.py','api/flow_control/__init__.py']:
    s=open(f).read()
    print(f, 'active:', len(re.findall(r'^    api\.add_resource\(', s, re.M)),
             'disabled:', len(re.findall(r'^\s*#\s*api\.add_resource\(', s, re.M)))
"

# 某支檔的依賴（正向）
grep -n '^from \|^import ' app/flow_engine/service/stage_advance_service.py

# 某支檔被誰引用（反向；排除自己那兩包與快取）
grep -rn 'app.flow_engine.service.stage_advance_service' \
  --exclude-dir=.venv --exclude-dir=__pycache__ --exclude-dir=.git .

# 資料庫選單（DEV，唯讀）
psql -h localhost -p 5432 -U cmmgr -d guidant_ai_dev -At -F' | ' \
  -c "SELECT name, url, enable FROM public.ui_routes WHERE url ILIKE '%flow%' OR url ILIKE '%project%' OR url ILIKE '%task%' ORDER BY url;"

# 軟參照掃描（import 掃不到的稽核關聯；見 §2「domain 層」）
for d in api/flow_engine app/flow_engine domain/flow_engine infra/flow_engine \
         api/flow_control app/flow_control infra/flow_control; do
  find $d -name '*.py' -not -path '*__pycache__*'
done | while read f; do
  n=$(grep -vE "^\s*(from|import) " "$f" | grep -ciE "audit_round|project_audit_rounds|poam|assessment_plan|round_id|ap_task")
  [ "$n" -gt 0 ] && echo "$n	$f"
done | sort -rn
```

---

## ② 逐檔歸屬判定 {#verdicts nav="逐檔判定"}

**這節在講什麼：114 支逐一判給五類之一，每一筆附依賴證據。**

### 判準 {#criteria nav="-"}

看**依賴方向**，不看檔名也不看直覺：

| 類別 | 什麼算這一類 |
|---|---|
| 🟦 **引擎** | 只依賴 `jedi-flow-engine` 與自家 `*/flow_engine/`，不碰業務語意 |
| 🟩 **平台** | 依賴 `jedi-task-platform`（任務／專案／指派），與稽核無關 |
| 🔴 **稽核** | 依賴 `jedi-compliance-audit`（輪次／AP／POA&M），或實作稽核專屬規則 |
| 🟨 **主程式編排** | 同時依賴三個以上模組、負責把它們接起來——這種本來就該留在主程式 |
| 🟡 **還看不準** | 依賴方向與語意方向不一致，兩種判法都說得通 |

### 結果總覽 {#summary nav="-"}

::: statgrid
::: {.stat .ok}
[57]{.v}[🟦 引擎／2,619 行]{.k}
:::
::: stat
[19]{.v}[🟩 平台／4,643 行]{.k}
:::
::: {.stat .crit}
[15]{.v}[🔴 稽核／2,903 行]{.k}
:::
::: {.stat .warn}
[12]{.v}[🟨 主程式編排／4,188 行]{.k}
:::
::: {.stat .warn}
[0]{.v}[🟡 還看不準／0 行]{.k}
:::
:::

另有 11 支 `__init__.py` 空檔（合計 7 行，皆為 `api/`／`app/`／`infra/` 各層的套件宣告檔），不參與判定。五類相加 103 ＋ 空檔 11 ＝ 114。

::: {.callout .warn}
**行數分布與檔數分布方向相反，這是重點。** 引擎檔數最多（57 支）但行數最少（2,619 行）——那 57 支多半是 `domain/`／`infra/` 的小檔（entity、mapper、repo impl，平均 46 行）。**行數集中在平台與主程式編排**（合計 8,831 行，佔 62%），而那正是 §6「要設計才能動」的所在。換句話說：**好判的都是小檔，大檔都卡著。**
:::

### app/flow_engine 六支 service {#app-fe nav="app/flow_engine"}

這六支是整份判定的核心，逐支列出完整證據：

| 檔 | 行 | 判定 | import 了誰 | 被誰 import |
|---|---|---|---|---|
| `workflow_execution_service.py` | 1,321 | 🟨 **主程式編排** | `jedi_flow_engine`（17 處）／`jedi_task_platform`（participant 4 支＋project）／`jedi_survey`／`jedi_iam`／`app.associations`／`app.notification`／`domain.module_frame`／`common.authz.workflow` | `di_containers/flow_engine/workflow_excution_containers.py`、`app/module_frame/service/module_frame_service.py`、1 支測試 |
| `stage_advance_service.py` | 702 | 🔴 **稽核** | `jedi_compliance_audit.RoundStageTransitionEntity`（直接 import）／`jedi_flow_engine`／`jedi_task_platform`／`domain.flow_engine.stage_completion_registry` | `di_containers/flow_control/flow_control_containers.py:667`、1 支測試 |
| `stage_rollback_service.py` | 189 | 🔴 **稽核** | `jedi_flow_engine`／`jedi_task_platform`／`common.enum.participant_enum` | `di_containers/flow_control/flow_control_containers.py:367`、1 支測試 |
| `flow_template_app_service.py` | 265 | 🟦 **引擎** | 只有 `jedi_common`／`jedi_flow_engine`／`common.authz`／`common.util.audit_nickname`／自家 `domain.flow_engine` | `di_containers/flow_engine/flow_template_containers.py`、1 支測試 |
| `job_evidence_service.py` | 181 | 🟩 **平台** | `jedi_file_upload`／`jedi_flow_engine`／`jedi_survey`／自家 `domain.flow_engine` | `di_containers/flow_engine/job_evidence_containers.py` |
| `workflow_template_snapshot_service.py` | 94 | 🔴 **稽核**（決策者 2026-09-16 裁） | 只有 `jedi_flow_engine`（兩支） | `di_containers/flow_engine/workflow_excution_containers.py:124`；**唯一的業務消費者是 `jedi_compliance_audit.audit_round_app_service`**（`:306`／`:308`／`:321`） |

::: {.callout .crit}
**`stage_rollback_service` 的稽核依賴沒有消失，只是換了形式。**

派工卡上寫「實查其依賴只剩 jedi-common／jedi-flow-engine／jedi-task-platform，原判證據失效」——那是只看 `import` 行得到的結論。逐處追下去，稽核依賴還在，只是改由兩條路走：

1. **DI 注入**：`flow_control_containers.py:370` 把 `audit_round_domain_service=project_audit_round_domain_service` 注進去，service 內用 `self._audit_round_domain_service.get_by_uid(round_uid)` 取輪次（`stage_rollback_service.py:90`）。
2. **route 層 import**：`api/flow_engine/routes/stage_rollback_route.py:19` 直接 `from jedi_compliance_audit.app.service.audit_round_app_service import AuditRoundAppService`。

**原判（歸稽核）成立**，只是證據要換成上面兩條。這正是「看 import 行不夠」的實例——依賴注入會讓靜態 import 看起來很乾淨。
:::

### app/flow_control 17 支 {#app-fc nav="app/flow_control"}

| 檔 | 行 | 判定 | 證據 |
|---|---|---|---|
| `project_service.py` | 789 | 🟨 **主程式編排** | 同時依賴 `jedi_compliance_audit`／`jedi_task_platform`／`jedi_iam`／`app.notification`／`domain.associations`／`domain.oscal`——六個來源；`get_project()` 拼 participants／SSP inventory／owner 暱稱 |
| `assessment_plan_app_service.py` | 742 | 🔴 **稽核** | `jedi_oscal_v2`／`jedi_task_platform`／`app.module_frame`；處理的是評估計畫（AP）＝稽核概念 |
| `job_import_service.py` | 470 | 🟩 **平台** | `jedi_task_platform`／`jedi_iam`／自家 infra 查詢；做的是任務批次匯入 |
| `job_service.py` | 403 | 🟩 **平台** | `jedi_task_platform`／`jedi_detection`／`jedi_survey`／`jedi_iam`／`app.detection_tools`；型別專屬邏輯已切成 handler mixin |
| `prep_job_generation_service.py` | 217 | 🔴 **稽核** | `jedi_compliance_audit.ao_derivation.AO_PART_NAME`／`jedi_oscal_v2`（兩支 catalog repo）；做的是 per-AO 準備期任務生成 |
| `job_handlers/survey_handler.py` | 196 | 🟩 **平台** | `jedi_survey`／`jedi_common`；問卷型別的任務處理 |
| `job_batch_complete_service.py` | 193 | 🟩 **平台** | `jedi_task_platform`／`infra.readmodel`／`app.flow_engine.workflow_execution_service` |
| `job_binding_orphan_cleanup_service.py` | 174 | 🟩 **平台** | 只有 `jedi_common`＋自家 `infra.flow_control.job_binding_orphan_query` |
| `oscal_stage_handlers.py` | 142 | 🔴 **稽核** | 只 import `domain.flow_engine.stage_completion_registry`（實作引擎定義的介面）；內容是稽核輪次各階段的完成處理 |
| `oscal_stage_preconditions.py` | 129 | 🔴 **稽核** | 同上；內容是「POA&M 要全部關閉才能推進」這類稽核前置條件 |
| `oscal_stage_rollback_handlers.py` | 71 | 🔴 **稽核** | 同上（`IStageRollbackHandler`） |
| `reverify_inheritance_service.py` | 73 | 🔴 **稽核** | `infra.flow_control.reverify_clone_query`；覆核輪承襲＝稽核概念 |
| `dto/job_dto.py` | 360 | 🟩 **平台** | `jedi_task_platform`／`jedi_detection`；稽核殘留只剩 3 行（`:351`–`:353` 的 `ao_code`／`ao_name`／`ao_id` 讀 `row.ap_task_*`） |
| `job_handlers/__init__.py` | 36 | 🟩 **平台** | 兩支 mixin 的匯出點 |
| `__init__.py`／`dto/__init__.py`／`service/__init__.py` | 0 | — | 空檔 |

::: {.callout .ok}
**三支 `oscal_stage_*` 是「介面在引擎、實作在稽核」的正確形狀。**

它們只 import `domain/flow_engine/service/stage_completion_registry.py` 的抽象介面，反過來由 DI container 在啟動時註冊進 registry（`flow_control_containers.py:684`–`:689`）。引擎不需要認識稽核，稽核來實作引擎的介面——這是舊稿說的「引擎只留通用的階段推進機制抽象」已經兌現的部分。
:::

### api 層 24 支 {#api nav="api 層"}

route 與 serializer 跟著它們呼叫的 service 走：

| 檔群 | 檔數 | 行 | 判定 |
|---|---|---|---|
| `api/flow_engine/routes/flow_template_route.py`＋`stage_object_route.py`＋對應 serializer 4 支 | 6 | 274 | 🟦 引擎 |
| `api/flow_engine/routes/job_evidence_route.py`＋serializer | 2 | 197 | 🟩 平台 |
| `api/flow_engine/routes/flow_engine_route.py`＋serializer 3 支 | 4 | 499 | 🟨 主程式編排（吃 `workflow_execution_service`） |
| `api/flow_engine/routes/stage_advance_route.py`＋`stage_rollback_route.py`＋serializer 2 支 | 4 | 251 | 🔴 稽核（`stage_rollback_route.py:19` 直接 import `AuditRoundAppService`） |
| `api/flow_engine/__init__.py` | 1 | 102 | 🟨 主程式編排（blueprint 組裝） |
| `api/flow_control/routes/project_route.py`＋`job_route.py`＋`serializers/job.py` | 3 | 912 | 🟩 平台 |
| `api/flow_control/routes/assessment_plan_route.py` | 1 | 117 | 🔴 稽核（import `jedi_compliance_audit`） |
| `api/flow_control/__init__.py` | 1 | 122 | 🟨 主程式編排（blueprint 組裝＋兩支套件 `attach`） |

`api/flow_control/__init__.py` 值得單獨說明：它建 blueprint、定 URL 前綴，然後讓 `jedi-task-platform` 與 `jedi-compliance-audit` 各自 `attach` 自己那半的 route。**完整的 URL 表要看三份**（本檔＋兩支套件的 routing），這是刻意的設計，不是遺漏。

### domain/flow_engine 27 支 {#domain nav="domain 層"}

**全部 🟦 引擎。** 這一層乾淨到不需要逐支列表——27 支的 import 只有三種來源：`jedi_common`（基礎設施）、`jedi_flow_engine`／`jedi_task_platform`（套件型別）、自家 `domain/flow_engine`。**零稽核 import。**

兩處要點名：

1. **`stage_completion_registry.py`（177 行）** 是引擎定義給稽核實作的介面（見上方 callout），被 6 處引用，其中 `flow_template_containers.py:24` 註冊成 Singleton。
2. **兩族共 11 支帶稽核軟參照**（見下方 callout）。

::: {.callout .crit}
**軟參照是 import 掃描的盲區——判歸屬時看不到，搬遷時是真的資料關聯。**

對 114 支做過一次「排除 import 行之後 grep 稽核詞」的全面掃描（指令見 §1 末），判引擎的檔裡命中 11 支，逐支開檔確認全部是同一形狀：**指向稽核表的整數欄位，無外鍵**。

| 欄位 | 出現在 | 指向 |
|---|---|---|
| `round_id` | `workflow_execution_control_mapping` 一族 4 支（entity／query entity／model／mapper／repo impl） | `compliance.project_audit_rounds.id`（model `:44` 註解明寫「soft ref，無 FK」） |
| `inherited_from_round_id` | `job_evidence` 一族 6 支（entity／dto／model／mapper／serializer） | 同上（model `:66` 註解同樣明寫無 FK；用途是覆核輪自母輪 clone 證據時記來源） |

DB 層實查確認無外鍵約束。這是 D17 律②（跨疆界只存識別碼、不建外鍵）的正確用法，**不構成程式層依賴**——引擎不必認識稽核輪次是什麼，只存一個整數，判引擎成立。

**但搬遷時必須寫進套件 README**：這兩張表若隨引擎套件走，那兩個欄位就成了跨套件軟參照。第 11 支是 `infra/flow_engine/models/stage_object.py`，命中的是階段 code 字串（`planning`／`task_execution`／`audit`／`poam`）——那是碼表值不是參照，無須處理。
:::

### infra 層 30 支 {#infra nav="infra 層"}

**`infra/flow_engine` 20 支全部 🟦 引擎**（mapper／model／repo impl 各 5–6 支，跟著自家 entity 走）。兩支被外部引用：`models/ext_workflow_execution.py` 與 `models/job_evidence.py` 被 `infra/readmodel/tasks/job_batch_complete_query.py` 引用。

**`infra/flow_control` 10 支**——這裡是舊稿 D-4「讀寫混合」的所在，實查結果與舊稿差距最大：

| 檔 | 行 | 寫入語句 | 表數 | 跨 schema | 判定 |
|---|---|---|---|---|---|
| `flow_control_job_repo_impl.py` | 1,084 | **4** | 24 | compliance／oscal／survey／config／iam | 🟩 平台（讀寫混合） |
| `flow_control_project_repo_impl.py` | 716 | **0** | 23 | compliance／oscal／public／iam | 🟨 主程式編排（純讀跨模組） |
| `task_execution_query.py` | 292 | **0** | 19 | compliance／oscal／public／config | 🟨 主程式編排（純讀跨模組） |
| `job_export_query.py` | 251 | 0 | 13 | compliance／oscal／iam | 🟨 主程式編排（純讀跨模組） |
| `reverify_clone_query.py` | 176 | **6** | 10 | compliance／public／survey | 🔴 稽核（跨模組搬運：複製任務＋證據＋問卷作答） |
| `job_import_lookup_query.py` | 149 | 0 | 6 | iam | 🟩 平台（純讀） |
| `job_binding_orphan_query.py` | 129 | **1** | 2 | compliance | 🟩 平台 |
| `task_existence_query.py` | 76 | 0 | 2 | — | 🟩 平台（純讀） |

::: {.callout .warn}
**舊稿 D-4 的四支，現在只剩兩支是讀寫混合。**

`flow_control_project_repo_impl`（舊稿記 1 處寫入）與 `task_execution_query`（舊稿記 1 處寫入）現在**都是 0 處寫入的純讀查詢**。`flow_control_job_repo_impl` 的寫入從 21 處降到 4 處。

這代表 D-4 的建議（「逐支切成兩半：純讀的抽進 readmodel、會寫的留原地」）**大部分已經做完了**——三支純讀跨模組查詢（`flow_control_project_repo_impl`／`task_execution_query`／`job_export_query`，共 1,259 行）現在符合 `infra/readmodel/` 的收錄條件，但還留在原地。這是 §6 的第一項。
:::

---

## ③ 與舊判定的差異 {#diff nav="與舊判定的差異"}

**這節在講什麼：舊稿判 X、現在判 Y、為什麼改。這是本文的核心產出。**

::: {.callout .crit}
**舊稿的判定有一半已經無從比較**——它判的對象（`camunda_service` 2,852 行、`bpmn_generator` 2,428 行、`audit_round_app_service` 925 行……）現在不在主專案了。下表只列**還有對象可比**的條目。
:::

### 判定改變的 {#diff-changed nav="-"}

| 對象 | 舊判 | 現判 | 為什麼改 |
|---|---|---|---|
| `workflow_execution_service` | 🟡 還看不準（1,679 行，D-9 說「真正跨模組只有 4 條線，改 port 就歸平台」） | 🟨 **主程式編排**（1,321 行） | D-9 說的四條線裡，三條已經隨套件化自然解決（`TaskSurveyService`→`jedi_survey`、兩支 participant service→`jedi_task_platform`、`UserService`→`jedi_iam`），**但解法不是「改成 port」而是「那些模組本身變成套件了」**。剩下的跨模組依賴是 `app.notification`／`app.associations`／`domain.module_frame`——三個仍在主專案的模組。它同時牽三個主專案模組，這正是主程式編排的形狀 |
| `workflow_template_snapshot_service` | 🔴 稽核（理由：snapshot 屬輪次凍結語意） | 🔴 **稽核**（決策者 2026-09-16 裁，見 §7 待裁①） | 依賴面只有 `jedi_flow_engine` 兩支 import，乾淨到不能再乾淨；但唯一的業務消費者是 `jedi_compliance_audit.audit_round_app_service`。依賴說引擎、用途說稽核 |
| `flow_control_project_repo_impl` | 🟡（720 行／1 處寫入，D-4 待切） | 🟨 **主程式編排**（716 行／**0 處寫入**） | 寫入已經清掉了，現在是純讀跨模組查詢，符合 readmodel 收錄條件 |
| `task_execution_query` | 🟡（277 行／1 處寫入，D-4 待切） | 🟨 **主程式編排**（292 行／**0 處寫入**） | 同上 |
| `flow_control_job_repo_impl` | 🔴 讀寫混合（1,015 行／**21** 處寫入） | 🟩 平台（1,084 行／**4** 處寫入） | 寫入從 21 降到 4；跨的表從 13 增到 24（查詢面變廣，寫入面變窄） |
| `job_dto.py` | 🟩 平台，但「含 6 處稽核欄位待拆」（D-6） | 🟩 平台（稽核殘留 **3 行**） | 6 處剩 3 行（`:351`–`:353` 讀 `row.ap_task_*` 補 AO 欄位） |
| `job_service.py` | 🟡 還看不準（1,144 行，三種型別混在一支，D-1 建議切三份） | 🟩 **平台**（403 行） | D-1 已執行——檢測那份進了 `jedi_detection`，問卷那份切成 `job_handlers/survey_handler.py`，剩下的是乾淨的任務 CRUD |
| `project_service.py` | 🔴 稽核（812 行，但註明「其實看不準」，D-2） | 🟨 **主程式編排**（789 行） | 依賴六個來源（兩支套件＋`jedi_iam`＋通知＋關聯＋OSCAL），是把它們接起來的那一層 |

### 原判成立、但證據要換的 {#diff-evidence nav="-"}

| 對象 | 舊判 | 現判 | 證據變化 |
|---|---|---|---|
| `stage_rollback_service` | 🔴 稽核（證據：直接 `import AuditRoundAppService`） | 🔴 **稽核，原判成立** | 直接 import 確實不見了，但稽核依賴改走 DI 注入（`flow_control_containers.py:370`）＋route 層 import（`stage_rollback_route.py:19`）。**卡上「原判證據失效」這句只對一半**——證據變了，結論沒變 |
| `stage_advance_service` | 🔴 稽核（證據：吃 `domain.flow_control.RoundStageTransitionEntity`） | 🔴 **稽核，原判成立** | 該 entity 隨套件化搬家，現在是 `jedi_compliance_audit.domain.entities.round_stage_transition_entity`（`stage_advance_service.py:43` 直接 import）。**證據更強了**——從「吃同專案的 domain 物件」變成「直接依賴稽核套件」 |
| `flow_template_app_service` | 🟦 引擎 | 🟦 **引擎，原判成立** | 依賴仍然只有 `jedi_flow_engine`＋自家 domain＋共用工具 |
| `job_evidence_service` | 🟩 平台 | 🟩 **平台，原判成立** | 依賴 `jedi_file_upload`／`jedi_flow_engine`／`jedi_survey` |
| 四條 stage route | 🔴 稽核 | 🔴 **稽核，原判成立** | `stage_rollback_route.py:19` 的 `AuditRoundAppService` import 仍在（只是來源換成套件） |
| `reverify_clone_query` | 🔴（D-4 例外：「整支在做跨模組搬運，留主程式」） | 🔴 **稽核** | 6 處寫入仍在；它服務的 `reverify_inheritance_service` 是覆核輪承襲，稽核專屬 |

### 舊稿判過但對象已不存在的 {#diff-gone nav="-"}

| 對象 | 舊判 | 現況 |
|---|---|---|
| `camunda_service.py`（2,852 行） | 🟦 引擎 | **已刪**（CM-1486 退役，`561922d4`） |
| `bpmn_generator.py`（2,428 行） | 🟦 引擎 | **已移入套件** `jedi_flow_engine/common/utils/`（CM-1492） |
| `bpmn_topology_validator.py`（330 行） | 🟦 引擎 | 同上 |
| `audit_round_app_service.py`（925 行） | 🔴 稽核 | **已移入** `jedi_compliance_audit` |
| `assessment_result_app_service`／`ar_import_app_service`／`poam_*`／`ap_docx_import_*`／各 parser | 🔴 稽核 | **已移入** `jedi_compliance_audit` |
| `control_service`／`control_group_service`／`assessment_object_service`（D-8① 三支 dark stub） | 待退役 | **已刪**（CM-1486） |
| `app/project/oscal_audit_service.py`（409 行，D-8②） | 「不動，需另派一棒查證是不是殭屍」 | **已刪**（CM-1494 查證後定讞為殭屍） |
| `auditor_dashboard_query`／`job_batch_complete_query`（D-5 兩支） | 待補進 readmodel | **已補**（`infra/readmodel/audit/`／`infra/readmodel/tasks/`） |
| `review_service.py`（D-7） | 🟢 通用，建議帶走 | **已移入** `jedi_compliance_audit/app/service/review_service.py`——D-7 的建議（歸通用）**被後續執行推翻**，實際歸了稽核 |
| `jedi-project`（399 行，D-2 建議當平台包種子） | 待處理 | **已被吸收**進 `jedi_task_platform`，套件本身不存在了 |
| `domain/flow_control`（舊稿列為要整包搬遷的四層之一） | 待搬 | **空目錄**，該層已全數在兩支套件內 |

---

## ④ 已裁示事項現況 {#rulings nav="已裁示事項"}

**這節在講什麼：決策者 2026-08-31 逐項裁過 D-1～D-9，這裡標註每一項現在還成不成立。**

裁決結果是「全數照建議採納」（另有 D-7 補閥「port 超過一張即退回稽核半」、D-8② 另立查證棒）。

| 項 | 當時裁的 | 現況 | 結論 |
|---|---|---|---|
| **D-1** | `job_service.py`（1,144 行）切三份：任務 CRUD 歸平台／檢測那份歸檢測插件／問卷那份歸問卷插件 | **已執行**。`job_service.py` 現 403 行；檢測那份在 `jedi_detection.app.service.detection_job_binding_handler`；問卷那份是 `app/flow_control/service/job_handlers/survey_handler.py`（196 行）。兩支以 mixin 形式由 `JobService` 繼承回來 | ✅ **已完成，不必再裁** |
| **D-2** | `project_service.py`（812 行）拆兩支：CRUD 併入平台包吸收 jedi-project 當種子／聚合查詢進 readmodel | **半成**。`jedi-project` 已被 `jedi_task_platform` 吸收（套件不存在了），但 `project_service.py`（789 行）**沒有拆**——`get_project()` 的聚合仍在原地，仍依賴六個來源 | 🟡 **未完成部分見 §6②**，裁示方向仍適用 |
| **D-3** | `job_import_service.py`（464 行）算通用，欄位定義改申報制；若判太早就整支帶走留稽核、第 3 步再議 | **已執行**（走「一行未改吃到申報制」那條路，CM-1490）。現 470 行，依賴 `jedi_task_platform`／`jedi_iam`，無稽核依賴 | ✅ **已完成** |
| **D-4** | 四支讀寫混合 query 逐支切兩半：純讀進 readmodel、會寫的留原地；`reverify_clone_query` 例外整支留主程式 | **大部分已完成**。四支中兩支（`flow_control_project_repo_impl`／`task_execution_query`）已變純讀，`flow_control_job_repo_impl` 寫入從 21 降到 4，`reverify_clone_query` 照裁示留著。**但那兩支變純讀的沒有搬進 readmodel** | 🟡 **剩搬遷動作，見 §5②** |
| **D-5** | `auditor_dashboard_query`（106 行）與 `job_batch_complete_query`（271 行）補進 `infra/readmodel/` | **已執行**。兩支現在在 `infra/readmodel/audit/auditor_dashboard_query.py` 與 `infra/readmodel/tasks/job_batch_complete_query.py` | ✅ **已完成** |
| **D-6** | `job_dto.py` 的 6 處稽核欄位：整支當通用帶走，欄位改由插件擴充 | **多數已消化**。稽核殘留從 6 處降到 3 行（`:351`–`:353` 的 AO 三欄讀 `row.ap_task_*`），未改成插件擴充形式 | 🟡 **殘留 3 行，規模已不值得單獨處理**；併入 §6② |
| **D-7** | `review_service.py`（123 行）與 `review_marks` 表算通用，標的用軟參照；補閥「port 超過一張即退回稽核半」 | **裁示被執行推翻**。`review_service` 現在在 `jedi_compliance_audit/app/service/review_service.py`，`review_mark` model 也在該套件（`infra/model/review_mark.py`），實際歸了稽核。主專案側只剩 `infra/readmodel/oscal/ssp_control_implementation_query.py:204` 的唯讀查詢 | ⚠️ **原裁示已不適用**（結果與裁示相反但合理，補閥應已觸發），見 §7 待裁② |
| **D-8①** | 三支 dark stub（`control_service`／`control_group_service`／`assessment_object_service`，225 行）搬遷前先退役；退役前先驗拔掉 DI 不會炸 | **已執行**（CM-1486，三支檔案已不存在） | ✅ **已完成** |
| **D-8②** | `oscal_audit_service.py`（409 行）不動，跟著稽核走；到底是不是殭屍另派一棒查證 | **已執行**。查證棒 CM-1494 定讞為殭屍，檔案已刪，整個 `app/project/service/` 現在只剩 `project_start_app_service.py`（481 行） | ✅ **已完成** |
| **D-9** | `workflow_execution_service.py`（1,679 行）算通用歸平台層，四條跨模組線改 port：`TaskSurveyService`→型別 handler 申報／`NotificationService`→`INotifier` port／兩支 participant service→平台指派能力／`UserService`→身分名冊 port | **四條線裡三條已解，但解法不同**。`TaskSurveyService` 現在是 `jedi_survey.app.service.task_survey_service`、兩支 participant service 在 `jedi_task_platform.participant.app.service.*`、`UserService` 在 `jedi_iam.app.service.user_service`——**不是改成 port，而是那些模組整個變成套件了**。第四條 `NotificationService` 仍是主專案的 `app.notification.service.notification_service`，未改 port。另外新增兩條主專案依賴：`app.associations.ProjectAssessmentPlanMappingService` 與 `domain.module_frame.ActionType` | 🟡 **「真正跨模組只有 4 條線」這個前提已失效**——現在是 3 條（notification／associations／module_frame），且都指向主專案模組，見 §7 判定與 §6③ |

::: {.callout .warn}
**D-7 是九項裡唯一「裁示與執行結果相反」的一項。** 裁的是「算通用」，做出來是歸稽核套件。當時補的閥（「port 超過一張即退回稽核半」）應該就是在執行時觸發了。這不算違規——補閥本來就是為這種情況設的——但**裁示文字與現況不一致**，需要決策者確認是否正式撤銷原裁示（見 §7 待裁②）。
:::

---

## ⑤ 可以立刻做的乾淨項 {#quickwins nav="立即可做"}

**這節在講什麼：依賴單純、搬動不牽其他模組的，逐項列出搬去哪、動幾個檔、怎麼驗。**

::: {.callout .ok}
**只有兩項合格。** 判準是「說得出依賴證據」——首腦初判的兩支裡一支成立、一支推翻，另外找到一項舊稿沒列的。
:::

### ① `flow_template_app_service` 歸引擎 — 成立 {#qw1 nav="-"}

**判定**：🟦 引擎，首腦初判成立。

**依賴證據**（`app/flow_engine/service/flow_template_app_service.py:1`–`:24`）：

```
jedi_common（例外類別／transaction／auth context）
jedi_flow_engine（bpmn_topology_validator、BpmnUtils）
common.authz（assert_scope_writable、viewer_is_platform_admin）
common.code.flow_control_error_code
common.util.audit_nickname（enrich_audit_nickname_objs）
common.exception_error.common_exception（BpmnTopologyError）
app.flow_engine.dto.flow_template_dto（自家）
domain.flow_engine.*（自家：entity 2 支、domain service 2 支）
```

**零稽核依賴、零跨模組依賴**。被引用處只有兩個：`di_containers/flow_engine/flow_template_containers.py` 與 `test/test_flow_template_app_service.py`。

**搬去哪**：`jedi-flow-engine` 套件。

**要動幾個檔**：這支 service（265 行）＋ 它依賴的自家檔案：`app/flow_engine/dto/flow_template_dto.py`（49）、`domain/flow_engine/entity/flow_template_entity.py`（21）、`flow_template_query_entity.py`（12）、`repository/flow_template_repo.py`（28）、`service/flow_template_domain_service.py`（33）、`infra/flow_engine/mapper/flow_template_mapper.py`（44）、`models/flow_template.py`（29）、`repository/flow_template_repo_impl.py`（108）＋ route 與 serializer（`flow_template_route.py` 144／`serializers/flow_template.py` 68）＋ DI container 改接線。**11 支、801 行。**

⚠️ **兩處要注意**：① `stage_object_*` 一族（同樣 11 支、301 行，從 app service 到 route 一條完整的鏈）與 flow template 共用同一個 DI container（`di_containers/flow_engine/flow_template_containers.py`），是否一起走要先確認——兩族都判引擎，一起搬比分兩次省事——但 flow_template 與 stage_object 共容器，一起搬是 22 支進套件並需發版（首腦估核心程式約 570 行；含 route／serializer／DI 接線則逾 1,100 行），屬套件異動卡不是小工；② `common.util.audit_nickname` 與 `common.authz` 是主專案的共用工具，套件化時要走 port 或由套件自帶對應能力。

**怎麼驗**：① 搬前搬後 `flask routes` 輸出逐字相同（7 條 flow-templates 端點＋1 條 stage-objects）；② 守衛測試 `python -m pytest test/test_module_boundaries.py` 全綠；③ `test/test_flow_template_app_service.py` 與 `test_flow_template_domain_service.py` 照跑；④ 手測：流程範本的新增／複製／發布／取消發布／驗證五個動作（`FlowTemplateService.js` 打的那五條）。⑤ **唯讀租戶要單獨測一次範本驗證**——`license_readonly_mw.py:116` 白名單收了 `/flow-engine/flow-templates/validate`，搬遷若改動路徑，唯讀租戶會靜默失效。

### ② 三支純讀跨模組查詢搬進 `infra/readmodel/` {#qw2 nav="-"}

**判定**：🟨 主程式編排（跨模組唯讀查詢），舊稿 D-4 未完成的部分。

**依賴證據**（寫入語句數以 `grep -ciE "INSERT INTO|UPDATE [a-z_.]+ SET|DELETE FROM"` 計）：

| 檔 | 行 | 寫入 | 跨表 | 跨 schema |
|---|---|---|---|---|
| `infra/flow_control/repository/flow_control_project_repo_impl.py` | 716 | **0** | 23 | compliance／oscal／public／iam |
| `infra/flow_control/repository/task_execution_query.py` | 292 | **0** | 19 | compliance／oscal／public／config |
| `infra/flow_control/repository/job_export_query.py` | 251 | **0** | 13 | compliance／oscal／iam |

三支都是**純讀、跨三到四個 schema**，形狀與 `infra/readmodel/` 裡已有的九支完全一致（`audit/auditor_dashboard_query.py`、`tasks/job_batch_complete_query.py` 等）。

::: {.callout .crit}
**更正（首腦驗收實查 2026-09-16）：三支裡只有 `job_export_query.py` 真純讀可搬。** 上表的「寫入 0」是用 SQL 關鍵字 grep 得出，漏了 ORM 寫入——`flow_control_project_repo_impl.py:698-715` 的 `soft_delete_project` 用 ORM `session.flush()` 寫入；`task_execution_query.py:136` 有 `UPDATE compliance.workflow_executions`。這兩支不符 readmodel 的只讀鐵則，不可搬進 `infra/readmodel/`。判讀寫要查 `session.add`／`merge`／`flush`，不能只 grep `INSERT`／`UPDATE`／`DELETE`。
:::

**搬去哪**：`infra/readmodel/`——依該目錄現行的功能分組，`flow_control_project_repo_impl` 與 `task_execution_query` 進 `tasks/` 或新開 `projects/`，`job_export_query` 進 `tasks/`。**落點需依該目錄 README 的四條規格確認**（只讀鐵則／層次紀律／出口必 DTO／port 化注入）。

**要動幾個檔**：三支檔案本身＋`di_containers/flow_control/flow_control_containers.py` 的註冊改接線＋`infra/readmodel/*/__init__.py` 成員表＋兩支測試的 import（`test/test_grc_project_repo.py`、`test/test_task_execution_service.py`）。**約 8 個檔。**

⚠️ **`flow_control_project_repo_impl` 有個前提要先驗**：它的名字叫 `repo_impl`，意味著它實作某個 repository 介面。搬進 readmodel 前要確認它真的只被當查詢用——若有 domain repository 介面指著它，就不是單純搬檔，要先處理介面歸屬。**另兩支名字就叫 `query`，沒有這個疑慮。**

**怎麼驗**：① 守衛測試全綠（`test_module_boundaries.py` 有針對 readmodel 的規則）；② 那兩支測試照跑；③ 端點回應比對——`/grc/projects/list`、`/grc/project/<uid>`、`/grc/project/<p>/ap/<ap>/jobs/export` 搬前搬後同一參數回同一結果；④ 出口型別檢查：readmodel 規格要求「出口必 DTO 不泄 ORM row」，搬進去前要逐支核。

### ③ `workflow_template_snapshot_service` — **推翻首腦初判** {#qw3 nav="-"}

::: {.callout .crit}
**首腦初判「屬立即可做的乾淨項」，實查推翻。**

依賴面確實乾淨到不能再乾淨——全檔只有兩行 import，都是 `jedi_flow_engine`：

```python
from jedi_flow_engine.app.dto.workflow_template_dto import WorkflowTemplateDTO
from jedi_flow_engine.app.service.workflow_template_service import WorkflowTemplateService
```

**但它的唯一業務消費者是稽核套件**。`jedi_compliance_audit/app/service/audit_round_app_service.py` 三處用它（`:306` 判 None、`:308` 呼叫 `get_source_template_uid`、`:321` 再判一次），DI 由 `flow_control_containers.py:349` 注入。主專案側除了 DI 註冊與兩支測試，沒有別的呼叫點。

**搬去哪取決於怎麼判**，而怎麼判有兩種合理答案（見 §7 待裁①，決策者 2026-09-16 已裁歸稽核套件）。在裁決前搬動它，等於用搬遷動作把判定鎖死——正是舊稿「先分家後打包」原則要避免的。**列為待裁項（已裁），不列為立即可做。**
:::

---

## ⑥ 要設計才能動的 {#blocked nav="要設計才能動"}

**這節在講什麼：卡在哪、需要什麼前置。不給完整方案。**

### ① `workflow_execution_service`（1,321 行）— 最大的一塊 {#b1 nav="-"}

**卡在哪**：它同時依賴三個仍在主專案的模組：

| 依賴 | 用在哪 | 為什麼不能直接搬 |
|---|---|---|
| `app.notification.service.NotificationService` | 任務完成／退回時通知相關人 | D-9 裁的是「改走 `INotifier` port」，port 尚未建 |
| `app.associations.service.ProjectAssessmentPlanMappingService` | 專案與 AP 的對應查詢 | `associations` 模組本身未套件化；且它的三組 mapping service 中兩組的呼叫者只有 DI 註冊（FR-102 已記） |
| `domain.module_frame.code.ActionType` | 模組框架動作型別 | `module_frame`（10,951 行）是主專案第二大模組，未套件化 |

**需要什麼前置**：三條線各自要有歸宿。最小的一步是 `NotificationService`→`INotifier` port（D-9 已裁定方向，只是沒做）；另兩條要等 `associations` 與 `module_frame` 的疆界先定。

**附帶**：`__init__` 有四個參數已無呼叫點但刻意保留（`project_service`／`assessment_plan_service`／`assessment_plan_task_domain_service`／`drive_sync_orchestration_service`），檔內註解說明「拆掉是 DI 簽名異動，2B 重接時大機率還要用」。動這支時要一併決定這些留不留。

### ② `project_service`（789 行）— D-2 未完成的那一半 {#b2 nav="-"}

**卡在哪**：D-2 裁的是「拆兩支：CRUD 歸平台、聚合進 readmodel」。CRUD 那半的歸宿已經有了（`jedi_task_platform` 吸收了 jedi-project），但**拆的動作沒做**。`get_project()`（`:238`–`:334`）仍在拼六個來源的資料：participants、SSP inventory 分類、devices、owner 暱稱、雲端同步狀態、稽核輪次。

**需要什麼前置**：要先決定聚合那半進 readmodel 之後，剩下的 CRUD 半是「搬進套件」還是「留主專案當接線」。這牽動 §5② 的 `flow_control_project_repo_impl` 搬遷落點——兩件事要一起想。

**附帶**：`job_dto.py:351`–`:353` 那 3 行稽核殘留（D-6）規模已小到不值得單獨動，併進這一項一起處理。

### ③ 四條 stage route ＋兩支 stage service 歸稽核 {#b3 nav="-"}

**判定明確**（🔴 稽核，原判成立、證據更強），**但動不了**。

**卡在哪**：`stage_advance_service`（702 行）與 `stage_rollback_service`（189 行）目前在 `app/flow_engine/`，判定歸稽核，理論上該進 `jedi-compliance-audit`。但：

1. 它們實作的是 `domain/flow_engine/service/stage_completion_registry.py` 定義的介面——那個介面在引擎側，**如果兩支 service 進稽核套件，介面也要跟著上移到引擎套件**，否則變成「稽核套件 import 主專案的 domain」，方向反了。
2. 三支 `oscal_stage_*` handler（342 行）也實作同一組介面，同樣的問題。
3. URL 路徑 `/project/<p>/audit-round/<r>/stage/*` 掛在 `api/flow_engine/` 的 blueprint 上，路徑**一字不能改**（前端已在用、E2E 在打）。搬動時要用 `attach` 模式（`api/flow_control/__init__.py` 已有先例）。

**需要什麼前置**：`stage_completion_registry` 先上移到 `jedi-flow-engine`，這一步做完上面三件才有得談。

### ④ `api/flow_engine/__init__.py` 的 blueprint 組裝 {#b4 nav="-"}

**卡在哪**：`api/flow_control/__init__.py` 已經改成「主專案建 blueprint、套件 `attach` 自己那半」的形狀，但 `api/flow_engine/__init__.py` 還是傳統的全部自己掛。上面三項任何一項動起來，這支都要跟著改成 attach 模式。

**需要什麼前置**：無技術障礙，但沒有理由單獨做——它是上面三項的配套動作。

---

## ⑦ 決策者已裁的 {#pending nav="已裁項"}

### 待裁① `workflow_template_snapshot_service`（94 行）歸引擎還是歸稽核？ {#p1 nav="-"}

**事實**：

- 依賴面：全檔只有兩行 import，都是 `jedi_flow_engine`。零稽核依賴。
- 用途面：唯一的業務消費者是 `jedi_compliance_audit.audit_round_app_service`（三處），做的事是「稽核輪次啟動時，把流程範本凍結一份副本」。
- 檔內文件自述：「本 service 不依賴 spec1 domain entity，**接 raw fields，跨 caller 都能用**」「Engine 不知道 master 來源在哪張表……caller 自己詮釋（compliance／module／其他業務皆可）」——**它是刻意設計成通用的**。
- 舊稿判稽核（理由：snapshot 屬輪次凍結的語意），本次實查推翻該理由的依賴基礎。

**兩案與各自後果**：

| | 甲案：歸引擎（進 `jedi-flow-engine`） | 乙案：歸稽核（進 `jedi-compliance-audit`） |
|---|---|---|
| 依據 | 依賴方向；且檔內明文寫著設計成跨 caller 通用 | 實際唯一用途是稽核輪次凍結 |
| 後果 | 引擎套件多一個「凍結範本副本」的通用能力，未來問卷／檢測若要凍結範本可直接用 | 稽核套件自足，引擎不必為只有一個用戶的功能負責 |
| 風險 | 若三年內沒有第二個 caller，就是替假想需求付維護成本 | 若之後問卷／檢測要凍結範本，會複製第二份——正是模組化要避免的 |
| 動的檔數 | 1 支＋DI 改接線＋1 支測試 | 同左 |

**我的觀察（不是建議）**：檔內那段「跨 caller 都能用」是 2026-05 寫的設計意圖，四個月過去仍只有一個 caller。這個事實對兩案都是證據——甲案說「能力已備好等著用」，乙案說「假想需求沒有兌現」。

::: {.callout .ok}
**決策者 2026-09-16 裁：歸稽核套件（乙案）。** 理由：四個月只有一個用戶且用途是稽核輪次凍結；將來有第二個 caller 再抽上引擎。
:::

### 待裁② D-7 原裁示（`review_service` 歸通用）是否正式撤銷？ {#p2 nav="-"}

**事實**：

- 2026-08-31 裁：`review_service.py`（123 行）與 `review_marks` 表**算通用**，標的用軟參照，由插件決定標的是什麼。補閥：「port 超過一張即退回稽核半」。
- 現況：`review_service.py` 在 `jedi_compliance_audit/app/service/review_service.py`，`review_mark` model 在同套件 `infra/model/review_mark.py`。**實際歸了稽核，與裁示相反。**
- 主專案側只剩 `infra/readmodel/oscal/ssp_control_implementation_query.py:204` 的 `get_review_marks_by_control_ids()`（唯讀查詢，由 `app/oscal/service/ssp_control_implementation_service.py:707` 呼叫）。

**這一項不是要重新選邊**——執行結果合理（補閥應已觸發），只是裁示文字與現況不一致，留著會讓下一個讀裁決紀錄的人誤判。需要的是一句確認：**原裁示撤銷、以「歸稽核」為準**，或說明當時補閥觸發的實際理由以便入帳。

::: {.callout .ok}
**決策者 2026-09-16 裁：原裁示撤銷，以歸稽核為準。**
:::

---

## ⑧ 邊界 {#limits nav="邊界"}

::: {.callout .crit}
**這些東西一個字都不能動**

1. **對外 API 的 URL 路徑**——30 條端點前端全部在用，E2E 有 23 條在打。判歸屬時路徑維持原樣，只換實作位置。
2. **error code 的字串值**——前端 `error-code.json` 靠字串比對做多語系。改類別名稱可以，改值不行。
3. **資料庫內容**。
:::

**本文刻意不碰的範圍**：

- **`app/project`**（481 行，只剩 `project_start_app_service.py`）：不在本次範圍。它是 OSCAL／專案／流程引擎三者的接縫，若要判歸屬需連同 OSCAL 疆界一起看。
- **`associations`／`module_frame`／`notification`** 三個模組：只在 §6① 記為 `workflow_execution_service` 的前置，不判它們自己的歸屬。
- **兩支套件內部**（`jedi-compliance-audit`／`jedi-task-platform`）：本文只看主專案這 114 支。
- **資料庫 view 歸屬**：舊稿 §8 列過四張 view，本次未重驗（不在卡片範圍）。若要引用那份清單，請先重查。
