FR-069 第四階段 · CM-1478 設計稿 · 2026-08-31

24,600 行要切在哪裡

現在的 flow_control 把兩種東西混在一起——一種是任何產品都用得到的流程通則(任務有狀態、狀態怎麼推進、要通知誰),一種是只有稽核產品才有的規則(例如 POA&M 要全部關閉才能結案)。這份稿子逐塊盤點,畫出兩者的分界線,並把三件懸而未決的歸屬問題(flow-engine 的 route 該歸誰、app/project 該歸誰、資料庫 view 該歸誰)一次定案。拿不準的做成 D-1~D-9 待決項,請決策者逐項裁。設計沒拍板前,一支檔案都不搬。

設計稿待拍板 通用 12/稽核 22/待判 12 待決 9 項
§1

30 秒版

  1. 要做的事:flow_control 這包程式(24,047 行/226 個檔)現在把「流程通則」和「稽核規則」焊在一起。這份稿子把每一塊逐支判給其中一邊,讓下一步搬遷有圖可照。
  2. 判準只有一句話「換一個產品,這段還會一樣嗎?」——會一樣的,是通用流程部分(最後併入 task 平台包,給別的產品重複使用);不一樣的,是稽核專屬部分(自己獨立成一個功能套件)。
  3. 盤點結果:app 層可判的 46 個物件裡,通用 12 支/稽核 22 支/還看不準的 12 支。看不準的這稿不硬判,列成待決項請決策者裁。
  4. 有個好消息:讓功能自己申報任務型別的機制,雛形已經存在。FlowControlJobType(general/survey/detection_tool)加上 JOB_TYPE_REQUIRED_MODULES,就是一張手寫死的任務型別登記表。要改成「功能自己申報」是換個形態,不是從零造新東西
  5. 動工分四步(見 §9):搬出來 → 分家 → 改成申報制 → 打包。每一步都能單獨驗收,也能停在任何一步。
  6. 這份稿子不動任何程式碼(唯一例外是 jedi-project 的虛掛依賴清理,純粹的依賴整理,已經 commit)。

各階段完成判定

做什麼 完成怎麼判定(決策者檢查法)
0 本稿拍板(9 項 D 待決逐項裁) 每項 D 有裁決結論,寫入 design.md §3 決策表
1 拆出 flow_control 整包搬成套件(import 名不變、業務邏輯原樣) 請 Claude 跑 URL 集合比對(搬前搬後 route 表逐字相同)+守衛測試綠;稽核主流程(建專案→建輪次→啟動稽核→結案)手測一遍
2 蒸餾(分家) 通用部分下沉(=往下移到共用的底層)成 task 平台層,稽核部分留在插件 手測同上;另外請 Claude 證明「平台層 grep 不到 poam/audit_round/assessment_plan」
3 平台化 任務型別改申報制、問卷申報審核狀態機 問卷任務的「送審/核可/退回」走得通;把設定關掉後審核步驟消失(=設定不是客製)
4 打包 task 平台包上 Nexus、稽核插件上 Nexus poetry show 看得到兩個版號;拔掉稽核插件後系統仍起得來(只是沒有稽核功能)

這四步不必一次做完,也不該一次做完。 第 1 步做完,系統就是可以交付的狀態;第 2、3 步各自也是。停在任何一步都不會留下半成品。 這是刻意的安排——「哪些算通用」的判斷,會隨著第二個真實流程(問卷審核)落地而修正,留餘地比一次押死好。


§2

為什麼現在要切

這節在講什麼:不分家的代價,以及分家換來什麼。

不切會怎樣

問卷想要「送審→核可/退回」,就得重寫一整套狀態機——因為現在這套是稽核專用的,推進條件裡直接寫著「POA&M 全部 closed」。每多一種流程,就多一套客製程式。

切完會怎樣

問卷申報自己的狀態機,走同一套推進引擎。客戶 A 要審核、客戶 B 不要,變成一個設定開關(D18 設定 schema 申報制)。流程差異的交付成本,從「開發案」降為「勾選項」。

分家不是限制重複使用,分家正是重複使用的前提。 整包 24,600 行拔去別的產品,會夾帶 POA&M 檢查、稽核輪次、覆核 clone——用不到又拆不掉。(決策全文見 專案平台化決策


§3

現況:三方的實際依賴

這節在講什麼:現在 flow-engine、flow_control、project 三邊互相亂穿,圖 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'}}}%%
flowchart TD
  subgraph MAIN["主專案"]
    FC["flow_control<br/>24,047 行/226 檔<br/>稽核業務層"]
    FE_APP["app/flow_engine<br/>9,183 行<br/>(BPMN 編排/階段推進)"]
    PROJ_APP["app/project<br/>974 行<br/>(專案成立編排)"]
    RM["infra/readmodel<br/>1,149 行<br/>(跨模組唯讀查詢集中區,4.A 已正名)"]
  end
  subgraph PKG["既有套件"]
    JFE["jedi-flow-engine<br/>5,465 行(BPMN 引擎)"]
    JP["jedi-project<br/>399 行(被掏空的 CRUD)"]
  end
  FC -->|"150 domain 自引<br/>+ 25 jedi_flow_engine"| JFE
  FC -->|"9 infra + 9 domain<br/>(穿透 infra)"| JP
  FE_APP -->|"stage_rollback_route<br/>直接 import AuditRoundAppService"| FC
  FC -->|"oscal_stage_handlers<br/>實作 flow_engine 的 registry 介面"| FE_APP
  PROJ_APP -->|"6 條 route 走 api/project<br/>但 service 在 flow_control"| FC
  RM -.->|"純讀,不隨任何套件走"| FC
圖 1 — 現況:三方互相穿透,方向混亂(實線=import,數字=出現次數)

圖 1 要看的三個病灶

  1. flow_engine 和 flow_control 互相依賴stage_rollback_route.py:19 直接 import AuditRoundAppService——引擎的 route 認識了稽核業務。而 oscal_stage_handlers.py 反過來實作引擎的 registry 介面。這兩條線是同一個死結的兩端。
  2. api/project 只是資料夾路徑,不代表歸屬:AP docx 匯入、AR xlsx 匯入、稽核輪次的 route 全掛在 api/project/,但實際做事的 service 在 app/flow_control/——路徑放哪裡跟程式屬於誰,對不上。
  3. jedi-project 被掏空了,卻還反過來依賴主專案:只剩 399 行,卻被引用 75 次;而且 jedi_project/app/dto/*.py 有三支 import 主專案的 app.common.dto.base_dto(這是本棒實查才發現的,boundary-map 沒記載)——套件依賴主專案,方向完全反了。

§4

目標:三層

這節在講什麼:最終要長成的樣子——所有功能都指向平台,平台反過來不認識任何功能。

%%{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 TD
  AUDIT["稽核插件<br/>(flow_control 的稽核專屬部分)<br/>申報 task type: general/detection<br/>+輪次狀態機+POA&M 規則"]
  SURVEY["問卷插件<br/>(jedi-survey + task_survey)<br/>申報 task type: survey<br/>+審核狀態機(作答→送審→核可/退回)"]
  DETECT["檢測插件<br/>(jedi-detection,P9)<br/>申報 task type: detection_tool"]
  PLATFORM["🔌 task 平台包<br/>(jedi-project 種子 + flow_control 通用部分)<br/>專案/任務/狀態機/指派/綁人綁附件綁頁面<br/><b>不認識任何業務</b>"]
  ENGINE["jedi-flow-engine<br/>BPMN 執行引擎(純技術)"]
  HOST["主程式 Guidant AI<br/>接線盤+組裝設定+跨模組唯讀查詢櫃檯"]
  AUDIT -->|register| PLATFORM
  SURVEY -->|register| PLATFORM
  DETECT -->|register| PLATFORM
  PLATFORM --> ENGINE
  HOST -.->|"組裝:裝哪些插件、設定值存哪"| PLATFORM
  HOST -.->|"跨模組唯讀查詢"| AUDIT
圖 2 — 目標:所有箭頭指向平台,平台不認識任何業務

跟現況只差兩處,但兩處都是方向性的:① 平台層不再 import 任何業務程式(現在 app/flow_engine/stage_rollback_service 認識 AuditRoundAppService);② 任務型別從寫死的清單,改成由插件自己申報。


§5

切線盤點:flow_control 逐塊判

這節在講什麼:把 flow_control 的每一支檔案,逐支判給通用或稽核,附上判定證據。

判準:「換一個產品,這段還會一樣嗎?」判定用三類證據——① 檔案內稽核專屬詞的密度(poam/audit_round/assessment_plan/ssp/oscal 這幾個字的 grep 命中數);② 對外依賴的方向;③ 這支檔案實際在做什麼事。

12通用(可重複使用)

22稽核專屬(不可重複使用)

12還看不準(本稿不硬判)

24,047總行數/226 檔

app 層 service(41 支+dto 15 支)

🟢 通用——換個產品還會一樣

判定證據 最後要放哪
service/job_service.py 1,144 稽核詞 poam/round/ap/ssp/oscal 全 0;做的是任務 CRUD+指派+通知 ⚠️ 有分歧,見下方 G-1
service/job_comment_service.py 79 稽核詞全 0;純留言 CRUD task 平台層
service/job_batch_complete_service.py 193 稽核詞全 0;批次完成任務 task 平台層
service/task_execution_service.py 139 稽核詞全 0;啟動任務執行+進度 task 平台層(但吃跨模組唯讀查詢,見 D-4)
service/task_setup_service.py 18 純轉發 domain service task 平台層
service/dashboard_service.py 16 薄殼(16 行);聚合在 readmodel 主程式(readmodel 已在 4.A 正名)
service/auditor_dashboard_service.py 14 薄殼;聚合在 auditor_dashboard_query 主程式(該 query 尚未搬 readmodel,見 D-5)
dto/job_dto.py 361 稽核詞 6(ap/ar),其餘是任務欄位 task 平台層(6 處稽核欄位需拆,見 D-6)
dto/job_comment_dto.py 32 稽核詞全 0 task 平台層
dto/dashboard_dto.py 74 稽核詞全 0 主程式
dto/review_dto.py 32 稽核詞全 0 task 平台層
dto/control_group_dto.py 162 稽核詞全 0(但 control group 是 OSCAL 概念,見下方待判區) 待判→稽核專屬(見 D-7)

🔴 稽核專屬——別的產品沒有這些東西

判定證據
service/audit_round_app_service.py 925 round 76/ssp 49/poam 17/ap-ar 34——輪次狀態機本體
service/assessment_result_app_service.py 829 ap-ar 88/control 46/oscal 31/poam 12
service/assessment_plan_app_service.py 742 ap-ar 69/oscal 33/control 32/round 21
service/ar_import_app_service.py 610 control 20/ap-ar 10;亞航 CMMC L1 xlsx 匯入
service/poam_app_service.py 441 poam 31
service/ap_docx_import_app_service.py 375 ssp 9/control 7/ap-ar 6
service/poam_service.py 172 poam 22/ap-ar 18
service/prep_job_generation_service.py 217 round 9/control 10;per-AO 收證據 job 生成(AO=OSCAL 概念)
service/oscal_stage_preconditions.py 129 ap-ar 20/poam 10——「POA&M 全 closed 才能推進」正是稽核專屬前置條件
service/oscal_stage_handlers.py 142 round 16/poam 4
service/oscal_stage_rollback_handlers.py 71 round 7/ssp 3
service/reverify_inheritance_service.py 73 round 10——覆核輪承襲
service/planning_readiness_checker.py 128 round 2+flow_engine 7;規劃階段推進前置條件
service/project_current_ssp_service.py 69 ssp 23/oscal 4
service/audit_service.py 146 ap-ar 25
service/ap_report_parser/*(4 檔) 474 亞航 CMMC L1 報告格式 adapter
service/ar_report_parser/*(4 檔) 283 同上 AR 版
service/ar_import/*(4 檔) 157 AO 對齊/佐證分級配對
service/ar_framework_profile/cmmc_l1.py 37 CMMC L1 → NIST 控制對照
service/ao_derivation.py 42 control 10;AO 推導
dto/poam_dto.py / audit_dto.py / control_detail_dto.py / control_dto.py / project_dto.py / assessment_plan_dto.py / assessment_object_dto.py / task_setup_dto.py 998 稽核詞密集
service/project_service.py 812 ⚠️ 列在這裡但其實看不準,見 G-2

🟡 還看不準——本稿不硬判

為什麼看不準 對應待決
service/job_service.py 1,144 稽核詞 0,但檢測工具詞 88/問卷詞 73/指派詞 33——它其實把三種任務型別的處理全塞在同一支。算通用?還是該切三份? G-1/D-1
service/project_service.py 812 專案 CRUD 是通用動作,但 get_project() 會去拼 SSP inventory 分類、participant 出現 55 處——它是個跨模組的聚合窗口(盤點 #5) G-2/D-2
service/job_import_service.py 464 任務批次匯入是通用動作,但欄位含 AO/控制項對應(participant 34/device 29) D-3
service/review_service.py 123 「審閱標記」看起來通用,但被標記的東西是 control/AO(稽核概念) D-7
service/control_service.py / control_group_service.py / assessment_object_service.py 225 route 已經停用(2A dark stub 回空),只剩 DI 註冊還活著 D-8(要不要退役?)

domain 層(3,008 行/58 檔)

同一把尺量下來,domain 層比 app 層好分

分類 證據
🟢 通用 flow_control_job_*(entity/domain service/repo 介面)、flow_control_review_*flow_control_task_setup_*flow_control_dashboard_entitycode/flow_control_job_type_enum.py 稽核詞 0–2
🔴 稽核專屬 flow_control_audit_*(entity 17/domain service 25/repo 21)、poam_*(17/9/5)、project_audit_round_*round_rollback_supersession_*round_stage_transition_*ssp_reference_document_*i_ssp_document_pool_queryap_docx_parse_job_*ar_xlsx_parse_job_*flow_control_control* 稽核詞密集
🟡 還看不準 flow_control_project_*project_extension_* 專案主檔歸誰,會牽動 jedi-project(見 D-2)

infra 層(6,293 行/52 檔)

這一層最危險——SQL 字串裡的跨表依賴,程式碼掃描工具看不到(4.A 建 infra/readmodel/ 正是為了這件事)。實查 6 支重量級 query 的讀寫混合狀況:

寫入語句 表數 跨 schema 判定
flow_control_job_repo_impl.py 1,015 21 13 compliance/oscal/survey 🔴 讀寫混合——不能整支放進 readmodel(那裡只收純讀的)。見 D-4
flow_control_project_repo_impl.py 720 1 13 compliance/oscal/public 🟡 幾乎純讀,但有一處寫。見 D-4
task_execution_query.py 277 1 12 compliance/oscal/public 🟡 同上
reverify_clone_query.py 175 6 8 compliance/public/survey 🔴 讀寫混合,而且是跨模組搬運(複製任務+證據+問卷作答)
job_batch_complete_query.py 271 0 3 compliance/oscal 🟢 純讀跨模組 → 該進 readmodel
auditor_dashboard_query.py 106 0 4 compliance/oscal 🟢 純讀跨模組 → 該進 readmodel(4.A 漏收,見 D-5)

其餘 infra 檔(mapper/單表 repo/model)跟著它的 entity 歸屬走,不需要獨立判斷。

infra/flow_control/model 的 14 張表(表歸誰,決定 migration 跟著哪個套件走):

schema 判定
job_execution_comments / job_execution_devices / job_execution_org_units / job_execution_surveys compliance 🟢 通用(任務綁留言/設備/組織/問卷=「任務可以綁東西」)
review_marks compliance 🟡 還看不準(被標記的是 control/AO)
poams / project_audit_rounds / round_rollback_supersessions / round_stage_transitions compliance 🔴 稽核專屬
ap_docx_parse_jobs / ar_xlsx_parse_jobs / ssp_reference_documents / ssp_reference_document_mappings oscal 🔴 稽核專屬(而且已經在 oscal schema,P13 要一起看)

§6

flow-engine 19 條 route 歸屬(3.4 懸置案)

這節在講什麼:3.4 當初煞車沒解決的 route 歸屬,這裡逐條判完;其中 4 條是整個死結的實體。

3.4(CM-1470)當初煞車的原因:19 條 route 有 17 條吃主專案的 service,route 搬進套件會把主專案業務一起拖走。實查後的歸屬判定(還在用的 16 條+已停用 6 條,卡片寫「19 條」是含已註解掉的):

# Path Route class 的 service 在哪 歸屬判定
1 GET/POST /flow-engine/flow-templates app/flow_engine/flow_template_app_service 🟦 引擎(範本管理是純 BPMN)
2 /flow-engine/flow-templates/validate 同上 🟦 引擎
3 /flow-engine/flow-templates/<uid> 同上 🟦 引擎
4 .../duplicate 同上 🟦 引擎
5 .../publish 同上 🟦 引擎
6 .../unpublish 同上 🟦 引擎
7 GET /flow-engine/stage-objects app/flow_engine/stage_object_app_service(16 行薄殼) 🟦 引擎
8 /job-evidences(POST list) app/flow_engine/job_evidence_service 🟩 平台(證據=任務可綁附件)
9 /job-evidence(POST 建立) 同上 🟩 平台
10 /job-evidence/<uid> 同上 🟩 平台
11 /flow-engine/task/complete/<id> app/flow_engine/workflow_execution_service(1,679 行,吃 participant/task_survey/notification/associations) 🟩 平台(但該 service 要先拆,見 D-9)
12 /flow-engine/task/revert/<id> 同上 🟩 平台(同 D-9)
13 /flow-engine/process/comments/<id> 同上 🟩 平台(同 D-9)
14 /flow-engine/process-definition/<uid> 同上 🟩 平台(同 D-9)
15 /job-execution/task/link 同上+jedi_flow_engine.JobExecutionService 🟩 平台
16 /flow-engine/task/queue app/readmodel/my_jobs_app_service(4.A 已正名) 🟨 主程式(跨模組唯讀)
17 /project/<p>/audit-round/<r>/stage/info app/flow_engine/stage_advance_service(702 行,吃 domain.flow_control.RoundStageTransitionEntity 🔴 稽核——路徑就寫著 audit-round
18 .../stage/advance 同上 🔴 稽核
19 .../stage/rollback app/flow_engine/stage_rollback_service +直接 import AuditRoundAppService 🔴 稽核
20 .../stage/transitions 同上 🔴 稽核

最關鍵的一條線:#17–20 這 4 條 stage route

它們掛在 api/flow_engine/、service 也在 app/flow_engine/,看起來像引擎的東西。但實際上:URL 路徑寫著 audit-round、程式碼直接 import AuditRoundAppServicestage_rollback_route.py:19)、還吃 domain.flow_control.RoundStageTransitionEntitystage_advance_service.py:43。這 4 條做的是「稽核輪次的階段推進」,卻被放錯在引擎目錄下——前面說的那個「同一個死結」,實體就是它們。

判定:這 4 條 route,加上 stage_advance_service(702 行)和 stage_rollback_service(189 行),歸稽核專屬,跟著稽核插件走。引擎只留下通用的「階段推進機制」抽象(stage_completion_registry 已經在 domain/flow_engine/,形狀是對的)。

API 路徑不動(D5)——路徑是對外契約,前端已經在用。

app/flow_engine 這 9,183 行歸誰(卡片寫 6,375 行,實查是 9,183——多的是 util/bpmn_generator.py 2,428 行):

歸屬 證據
service/camunda_service.py 2,852 🟦 引擎 純 BPMN/Camunda
util/bpmn_generator.py 2,428 🟦 引擎 純 BPMN XML 生成
util/bpmn_topology_validator.py 330 🟦 引擎 同上
service/workflow_execution_service.py 1,679 🟡 還看不準 吃 participant/task_survey/notification/associations/module_frame 五個模組,D-9
service/stage_advance_service.py 702 🔴 稽核 domain.flow_control
service/stage_rollback_service.py 189 🔴 稽核 import AuditRoundAppService
service/flow_template_app_service.py 264 🟦 引擎 純範本
service/job_evidence_service.py 181 🟩 平台 任務綁附件
service/workflow_template_snapshot_service.py 85 🔴 稽核 snapshot 是輪次凍結的語意
其餘(dto/小 service) ~473 跟著使用它的人走

§7

app/project 974 行歸屬

這節在講什麼:app/project 只有 3 支 service,逐支判完;另外 api/project 底下 28 條 route 其實是稽核的。

卡片寫大約 1,300 行,實查是 974 行/3 支 serviceapi/project 另有 1,658 行的 route 和 serializer):

做什麼 歸屬
project_start_app_service.py 476 專案成立:clone 資源庫三件組(catalog/profile/SSP)+建 projects 主檔+participants+per-AO 準備期 job 🟨 主程式編排——它是 OSCAL、專案、flow_engine 三者的接縫(盤點 #16:直接 import 6 支 jedi_oscal_v2 RepoImpl,穿透了 infra 層)
oscal_audit_service.py 409 舊的 AP-scoped 稽核生命週期(activate/submit_for_review/launch_audit/confirm_audit/close_round) 🔴 稽核專屬,但很可能已經是退役品——見 D-8
project_system_info_service.py 35 專案系統資訊 🟢 通用(很薄)

api/project 底下還在用的 34 條 route:其中 28 條是稽核輪次/AP/AR/POA&M/audit-round/*/ap/*/ar-import/*/ap-docx-import/*),service 全都在 app/flow_control/——路徑掛在 project,程式卻在 flow_control。 判定:這 28 條跟著稽核插件走(路徑不改,D5)。剩下 6 條(/oscal-project/start 等)留在主程式。


§8

資產盤點:DB view 歸屬

這節在講什麼:資料庫裡的 view 也是跨模組查詢,但程式掃描掃不到,所以必須人工列冊。

全庫只有 4 張自建的 view。view 就是存在資料庫裡的跨模組查詢,同樣受 D17 三律管;但守衛測試只掃 Python,掃不到 view,所以必須人工列出來。(CM-1474 有實證:.12 那次的雙寫守衛就漏掉了 vw_user_job_queue

view JOIN 範圍 歸屬 本稿動作 程式面誰在用(實查)
vw_user_job_queue 8 表/3 schema/六個模組 主專案的 跨模組唯讀查詢櫃檯 標註歸屬,不動 infra/readmodel/my_grc_jobs_query.py:33(4.A 已搬);infra/flow_control/repository/flow_control_job_repo_impl.py:307 只在註解裡提到
v_role_routes 身分模組 身分模組的資料庫面資產(建在 jedi-iam 的表上,由主專案 migration 建立) 標「iam 相關表異動時必查」;不搬不改 Python 零命中
v_user_capabilities 身分模組 同上 同上 ⚠️ jedi-license-runtime/infra/tenant_admin_notify_query.py:32 用 raw SQL 直接打(見下方已知債)
v_user_routes 身分模組 同上 同上 Python 零命中

已知債(這一棒不還)jedi-license-runtimetenant_admin_notify_query.py 用 raw SQL 直接打 public.v_user_capabilities——這是套件穿越身分模組的 view 版變體。FR-062 當時還沒有名冊 port 可用,屬於合理的權宜做法。正確做法是改走主程式接線的 port,等下次動到 license-runtime 時順手還掉(已記在 STATE backlog)。


§9

問卷審核走查:「客製」變「設定」

這節在講什麼:拿問卷當第一個案例,證明切完之後,第二種流程不必再寫客製程式。

好消息:申報制的雛形已經存在

domain/flow_control/code/flow_control_job_type_enum.py 現在長這樣:

class FlowControlJobType(StrEnum):
    GENERAL = "general"
    SURVEY = "survey"
    DETECTION_TOOL = "detection_tool"

JOB_TYPE_REQUIRED_MODULES = {          # 任務型別 → 所需授權模組
    "survey": ("survey",),
    "detection_tool": ("remote-agent-manage", "detection-profile", "plugin"),
}

這已經是一張任務型別登記表了,只是被寫死在稽核模組裡。改成讓插件自己申報,是換個形態,不是從零造新東西——每加一種任務型別就得回頭改這支枚舉,正是「平台去認識功能」那個病的小樣本(跟 AI 儀表板那份 525 行手工登記簿是同一種病,CM-1476 已經拆掉了)。

目標形態

%%{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 HOST as 主程式啟動
  participant SURVEY as 問卷插件
  participant PLAT as task 平台層
  participant USER as 使用者
  HOST->>SURVEY: register(platform)
  SURVEY->>PLAT: 申報 task type "survey"<br/>+狀態機(作答→送審→核可/退回)<br/>+設定規格(審核開關,D18)<br/>+頁面路由
  PLAT-->>SURVEY: ok(平台不認識「問卷」是什麼)
  Note over PLAT: 平台只存:型別代號/狀態圖/設定規格
  USER->>PLAT: 完成一個 survey 任務
  PLAT->>PLAT: 查設定:這個租戶要不要審核?
  alt 審核開啟
    PLAT->>PLAT: 狀態 作答完成 → 待審核
    PLAT->>SURVEY: 通知審核者(走平台的指派與通知)
  else 審核關閉
    PLAT->>PLAT: 狀態 作答完成 → 完成
  end
圖 3 — 問卷插件申報自己的任務型別與狀態機

現況離目標比想像中近app/task_survey/common/task_survey_code.py 已經有 TaskSurveyStatusCode(UNFILLED/EDITING/UNDER_REVIEW/ADD_NOTES/COMPLETED)——審核狀態本來就在。差別只在於,推進這些狀態的邏輯目前散在 workflow_execution_service(1,679 行)裡,而不是由問卷插件自己申報。

「客戶 A 要審核、B 不要」怎麼走

走 D18 設定 schema 申報制:問卷插件申報一個租戶層級的開關(survey.review_required,預設值寫在程式裡),值存在主程式的 system_configs。這是現成的表、現成的 (tenant_id, group, key) 唯一鍵,不需要新開表。平台推進狀態時查這個開關,決定要不要走 UNDER_REVIEW。

交付成本:從「兩套客製程式」降為「一個勾選項」。


§10

待決項(D 編號,請逐項拍板)

這節在講什麼:9 個看不準的地方,每個附建議與理由,請決策者逐項裁。

D-1|job_service.py 這支 1,144 行的大檔案要怎麼分家?

現況:稽核詞全 0(看起來是乾淨的通用候選),但檢測工具詞 88/問卷詞 73/指派詞 33——它其實把三種任務型別的處理全塞在同一支:_replace_detection_tool_binding(檢測)、_reconcile_task_surveys_replace_survey_snapshots(問卷)、_resolve_job_assignees(指派)。

建議:切成三份,因為這三種型別的程式碼已經物理上相鄰,切開就直接變成三個 handler。 ① 任務 CRUD+指派+通知 → task 平台層;② _replace_detection_tool_*(約 400 行)→ 檢測插件申報的 handler;③ _reconcile_task_surveys_replace_survey_snapshots(約 150 行)→ 問卷插件申報的 handler。這支正是「任務型別 handler 申報制」最好的第一個示範。但這是第 3 步(平台化)的工,不是第 1 步(搬出來)的工:第 1 步整支帶走,第 3 步再切三份。

D-2|project_service.py(812 行)跟 jedi-project 之間該怎麼理?

現況:專案 CRUD 是通用動作,但 get_project() 會去拼 participants、living SSP 的 inventory 分類、devices/audit_systems、owner nickname(盤點 #5:教科書級的套餐窗口,DTO 的四個欄位來自四個不同模組);participant 這個字出現 55 處。同時 jedi-project(399 行)已經被掏空,而且反過來 import 主專案jedi_project/app/dto/*.py 有三支 import app.common.dto.base_dto,本棒實查才發現)。

建議:拆成兩支,因為 CRUD 和聚合查詢本來就是兩件事,混在一起才讓歸屬看不準。 ① 專案 CRUD/狀態 → 併入 task 平台包(吸收 jedi-project 當種子,同時把反向 import 修掉);② get_project() 的聚合部分 → infra/readmodel/app/readmodel/。這樣也順勢解掉 D15「jedi-project 疆界待決」——路 B 確立後,它不再是孤兒,而是平台包的種子。

D-3|job_import_service.py(464 行)該歸通用還是稽核?

現況:任務批次匯入/匯出是通用動作,但 Excel 欄位裡含 AO 和控制項對應(participant 34/device 29/ap-ar 7)。

建議:算通用,但欄位定義改成申報制,因為機制和規格是可以分開的。 平台提供匯入匯出的機制(讀 xlsx、驗證、批次建立),欄位規格由各任務型別的插件自己申報。如果覺得現在判斷太早,第 1 步就整支帶走留在稽核那邊,第 3 步再議。

D-4|4 支讀寫混合的 infra query 該怎麼處置?

現況(實查):flow_control_job_repo_impl(1,015 行/21 處寫入/13 表/3 schema)、flow_control_project_repo_impl(720/1/13)、task_execution_query(277/1/12)、reverify_clone_query(175/6/8)。readmodel 的規則寫死「會寫入的一律不收」,所以這 4 支進不去;但它們的 SQL 跨 3–4 個 schema,誤搬進套件就會靜默出錯(4.A 當初建 readmodel 就是為了防這個)。

建議:逐支切成兩半,因為讀和寫的搬遷風險完全不同。 純讀的跨模組查詢方法抽出來放進 infra/readmodel/,會寫入的方法留在原模組。reverify_clone_query 是例外:它整支在做「跨模組搬運」(複製任務+證據+問卷作答+設備關聯),本質是主程式的編排工作,整支留在主程式。逐支評估切法是第 2 步的工,第 1 步先原地不動,並在套件的 README 誠實寫明「這 4 支不隨包走」

D-5|auditor_dashboard_query.py(106 行)要不要補進 readmodel?

現況:純讀,跨 compliance/oscal 兩個 schema 四張表,形狀跟 4.A 已經搬過去的那五支完全一樣,只是當時漏收,還留在 infra/flow_control/repository/。同型的還有 job_batch_complete_query.py(271 行/純讀/compliance+oscal)。

建議:兩支都補進 infra/readmodel/,因為它們跟已搬的五支同型,留在原地只是不一致。 同時更新該目錄 __init__.py 的成員表。這是低風險的機械工作,可以在第 1 步之前先單獨做掉(純搬檔+改 import,守衛測試就能證明)。

D-6|job_dto.py(361 行)裡那 6 處稽核欄位怎麼辦?

現況:整體是任務 DTO(通用),但裡面含 6 處 ap/ar 欄位。

建議:整支當通用帶走,6 處稽核欄位改由插件擴充,因為為了 6 個欄位把整支判成稽核並不划算。 做法比照 D8 的 JSONB 擴充容器/project_extensions 模式。第 1 步不動,第 2 步再處理。

D-7|review_service.py(123 行)和 review_marks 這張表該歸誰?

現況:「審閱標記」這個機制本身是通用的(標記某個東西已審閱、記下誰在什麼時候標的),但被標記的東西是 control/AO,而且權限走 ParticipantRole.MANAGER/REVIEWER

建議:算通用,因為「東西可以被標記為已審閱」是任何流程產品都會有的能力。 被標記的對象用軟參照(D17 律②:只存識別碼、不建外鍵),由插件決定標的是什麼。如果反對意見成立(覺得現在就通用化太早),退回稽核那邊也不影響第 1 步。

D-8|已停用的東西怎麼處理:3 支 dark stub service,加上 oscal_audit_service(409 行)?

現況control_servicecontrol_group_serviceassessment_object_service(225 行)的 route 都已經在 api/flow_control/__init__.py 註解停用(2A dark stub 回空、前端沒有人在呼叫),只剩 DI 註冊還活著。app/project/oscal_audit_service.py(409 行)是舊的 AP-scoped 生命週期,新模型走 /audit-round/*——但它仍然被 AuditServicestage_advance_service 引用(實查有活的引用鏈),不是純粹的死程式。

建議:分兩類處理,因為「route 停了」和「程式沒人接線」是兩回事。 ① 3 支 dark stub:搬遷前先退役(route 已停、只剩 DI),省下 225 行的搬家成本——但退役前必須先驗證拔掉 DI 註冊不會炸flow_control_containers.py 有 713 行的交叉引用)。② oscal_audit_service不動,跟著稽核那邊走;它到底是不是殭屍需要另派一棒查證——route 面看起來停了,程式面卻還接著線,屬於「上了膛的槍」那一型。

D-9|workflow_execution_service.py(1,679 行)要怎麼切?

現況:它是 flow-engine 這邊最大的一塊未定案——26 個依賴橫跨 participant/task_survey/notification/associations/module_frame 五個模組(盤點的「假聚合清單」#4 判定其中 12 個是 jedi_flow_engine 自家的,真正跨模組的只有 4 條線)。而 #11–14 這四條還在用的 route 全都吃它。

建議:算通用(歸 task 平台層),因為真正跨模組的只有 4 條線,改成 port 注入就乾淨了。 四條線的改法:TaskSurveyService→改由任務型別 handler 申報、NotificationService→改走 INotifier port(D17 律③已經有樣板)、ProcessParticipantServiceProjectControlParticipantService→改用平台自己的指派能力(P12 完成後 participant 已經是套件)、UserService→改走身分名冊 port(D8 標準件)。這支是第 2 步的主戰場,第 1 步不動它。


§11

動工分步計畫(草案)

這節在講什麼:四步怎麼走、每步做什麼、怎麼驗收。

這是草案,四步的實際範圍以拍板後的 D 裁決為準。 每一步各自都能交付、都能停下。

第 1 步:搬出來(機械搬家,不做任何分家)

做什麼app/flow_control/domain/flow_control/infra/flow_control/api/flow_control/ 整包搬成套件(照 jedi-iam「先搬家、後裝修」的先例),import 名稱不變、業務邏輯原樣帶走。D-4 那 4 支讀寫混合的 query 原地不動,並在套件 README 誠實寫明不隨包走。

驗證(照 3.1 全額):① URL 集合比對——搬前搬後 flask routes 的輸出逐字相同;② 守衛測試 python -m pytest test/test_module_boundaries.py 全綠(目前基準 64 綠);③ 拔掉測試(D6 驗收核心);④ 稽核主流程手測。

風險:這是整個案子最大的一次搬遷(226 個檔)。建議先做 D-5(兩支純讀 query 補進 readmodel)和 D-8①(3 支 dark stub 退役)當熱身——兩件都是低風險的機械工作,而且能把第 1 步要搬的面積縮小。

第 2 步:分家(通用部分下沉)

做什麼:把 §5 判為通用的那 12 支(加上拍板後從「看不準」轉進來的)下沉(往下移到共用的底層)成 task 平台層;workflow_execution_service 那四條真正跨模組的線改成 port(D-9);stage_advance/stage_rollback 從 flow_engine 移到稽核那邊。

驗證:手測同上;另外加一條——請 Claude 證明「平台層 grep 不到 poam/audit_round/assessment_plan/ssp」。這條是分家成功與否的機械判準。

第 3 步:改成申報制

做什麼FlowControlJobType 從寫死的枚舉改成申報制;job_service 切成三個 handler(D-1);問卷插件申報審核狀態機和設定規格(D18)。

驗證:問卷任務的「送審/核可/退回」走得通;把設定開關關掉之後審核步驟消失——這一條是「設定不是客製」的驗收核心。

第 4 步:打包

做什麼:task 平台包(吸收 jedi-project 種子)和稽核插件各自上 Nexus,照 extraction-sop 的 D6/D7 標準(register 簽名/standalone harness/接入 README/拔掉測試)。

驗證poetry show 看得到兩個版號;拔掉稽核插件之後系統仍然起得來(只是沒有稽核功能)。

順序刻意安排成「先分家、後打包」——第三階段已經證明:邊界切乾淨之後再補外殼,是機械工。反過來先打包,會把「哪些算通用」的判斷錯誤鎖進套件版號裡,之後要改,貴十倍。


§12

邊界與凍結事項

這節在講什麼:哪些東西一個字都不能動,以及這份稿子刻意不碰的範圍。

這些東西一個字都不能動(重申 D5)

  1. error code 的字串值GRC_404001 等)——這是對外契約,前端 error-code.json 靠字串比對做多語系。改類別名稱可以(GrcErrorCodeFlowControlErrorCode 就做過),改值不行
  2. 對外 API 的 URL 路徑——前端已經在用。所以 §6 判定「4 條 stage route 歸稽核」時,路徑 /project/<p>/audit-round/<r>/stage/* 維持原樣;§7 那 28 條 api/project 路徑也一樣。
  3. 資料庫內容

其他邊界

  • project 本體不動(那是最終要長成的樣子,不是這個階段的事)。
  • 既有的跨模組外鍵不回頭全面拆除(D17)——拆到誰才判誰。
  • 這份稿子只做盤點與設計,除了 jedi-project 虛掛依賴清理(純粹的依賴整理,已 commit)之外,不動任何程式碼。
  • 看不準的一律寧可多列、不武斷:這個 arc 已經有五次「卡片斷言被實查推翻」的紀錄,所以本稿把拿不準的全部列進待決,不自行拍板。

§13

本稿與既有決策的關係

這節在講什麼:這份稿子跟先前七項決策各是什麼關係。

既有決策 本稿的關係
D5(改名 flow_control,error code/API path 凍結) 全篇遵守,§10 重申
D12(oscal → flow_control 單向依賴) 本稿把 flow_control 的形狀定下來,4.3(CM-1479)才有對象可驗
D15(jedi-project 疆界待決) 路 B 確立:D-2 建議它成為 task 平台包的種子
D17(資料關聯三律+view 條款) §8 的 view 歸屬表,就是該條款要求的人工列冊
D18(設定 schema 申報制) §9 的問卷審核開關,是該制的第二個實例(第一個是 security_policy.py
4.A 跨模組聚合正名infra/readmodel/ D-4/D-5 是它的延伸——本稿實查發現兩支同型的漏收、四支讀寫混合待切
3.4 煞車裁決(CM-1470 (a)+(c)) §6 就是該裁決承諾的「route 歸屬併入第四階段流程疆界三方設計」,本稿交付
FR-069.4.2 · CM-1478 · 盤點以 2026-08-31 codebase 為準