FR-069 第四階段 · CM-1478 設計稿 · 2026-08-31
現在的 flow_control 把兩種東西混在一起——一種是任何產品都用得到的流程通則(任務有狀態、狀態怎麼推進、要通知誰),一種是只有稽核產品才有的規則(例如 POA&M 要全部關閉才能結案)。這份稿子逐塊盤點,畫出兩者的分界線,並把三件懸而未決的歸屬問題(flow-engine 的 route 該歸誰、app/project 該歸誰、資料庫 view 該歸誰)一次定案。拿不準的做成 D-1~D-9 待決項,請決策者逐項裁。設計沒拍板前,一支檔案都不搬。
FlowControlJobType(general/survey/detection_tool)加上 JOB_TYPE_REQUIRED_MODULES,就是一張手寫死的任務型別登記表。要改成「功能自己申報」是換個形態,不是從零造新東西。| 步 | 做什麼 | 完成怎麼判定(決策者檢查法) |
|---|---|---|
| 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 步各自也是。停在任何一步都不會留下半成品。 這是刻意的安排——「哪些算通用」的判斷,會隨著第二個真實流程(問卷審核)落地而修正,留餘地比一次押死好。
這節在講什麼:不分家的代價,以及分家換來什麼。
問卷想要「送審→核可/退回」,就得重寫一整套狀態機——因為現在這套是稽核專用的,推進條件裡直接寫著「POA&M 全部 closed」。每多一種流程,就多一套客製程式。
問卷申報自己的狀態機,走同一套推進引擎。客戶 A 要審核、客戶 B 不要,變成一個設定開關(D18 設定 schema 申報制)。流程差異的交付成本,從「開發案」降為「勾選項」。
分家不是限制重複使用,分家正是重複使用的前提。 整包 24,600 行拔去別的產品,會夾帶 POA&M 檢查、稽核輪次、覆核 clone——用不到又拆不掉。(決策全文見 專案平台化決策)
這節在講什麼:現在 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 要看的三個病灶
stage_rollback_route.py:19 直接 import AuditRoundAppService——引擎的 route 認識了稽核業務。而 oscal_stage_handlers.py 反過來實作引擎的 registry 介面。這兩條線是同一個死結的兩端。api/project/,但實際做事的 service 在 app/flow_control/——路徑放哪裡跟程式屬於誰,對不上。jedi_project/app/dto/*.py 有三支 import 主專案的 app.common.dto.base_dto(這是本棒實查才發現的,boundary-map 沒記載)——套件依賴主專案,方向完全反了。這節在講什麼:最終要長成的樣子——所有功能都指向平台,平台反過來不認識任何功能。
%%{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
跟現況只差兩處,但兩處都是方向性的:① 平台層不再 import 任何業務程式(現在 app/flow_engine/stage_rollback_service 認識 AuditRoundAppService);② 任務型別從寫死的清單,改成由插件自己申報。
這節在講什麼:把 flow_control 的每一支檔案,逐支判給通用或稽核,附上判定證據。
判準:「換一個產品,這段還會一樣嗎?」判定用三類證據——① 檔案內稽核專屬詞的密度(poam/audit_round/assessment_plan/ssp/oscal 這幾個字的 grep 命中數);② 對外依賴的方向;③ 這支檔案實際在做什麼事。
12通用(可重複使用)
22稽核專屬(不可重複使用)
12還看不準(本稿不硬判)
24,047總行數/226 檔
🟢 通用——換個產品還會一樣
| 檔 | 行 | 判定證據 | 最後要放哪 |
|---|---|---|---|
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 層比 app 層好分:
| 分類 | 檔 | 證據 |
|---|---|---|
| 🟢 通用 | flow_control_job_*(entity/domain service/repo 介面)、flow_control_review_*、flow_control_task_setup_*、flow_control_dashboard_entity、code/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_query、ap_docx_parse_job_*/ar_xlsx_parse_job_*、flow_control_control* |
稽核詞密集 |
| 🟡 還看不準 | flow_control_project_*、project_extension_* |
專案主檔歸誰,會牽動 jedi-project(見 D-2) |
這一層最危險——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 要一起看) |
這節在講什麼: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 AuditRoundAppService(stage_rollback_route.py:19)、還吃 domain.flow_control.RoundStageTransitionEntity(stage_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 | 跟著使用它的人走 | — |
這節在講什麼:app/project 只有 3 支 service,逐支判完;另外 api/project 底下 28 條 route 其實是稽核的。
卡片寫大約 1,300 行,實查是 974 行/3 支 service(api/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 等)留在主程式。
這節在講什麼:資料庫裡的 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-runtime 的 tenant_admin_notify_query.py 用 raw SQL 直接打 public.v_user_capabilities——這是套件穿越身分模組的 view 版變體。FR-062 當時還沒有名冊 port 可用,屬於合理的權宜做法。正確做法是改走主程式接線的 port,等下次動到 license-runtime 時順手還掉(已記在 STATE backlog)。
這節在講什麼:拿問卷當第一個案例,證明切完之後,第二種流程不必再寫客製程式。
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
現況離目標比想像中近: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。
交付成本:從「兩套客製程式」降為「一個勾選項」。
這節在講什麼: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_service/control_group_service/assessment_object_service(225 行)的 route 都已經在 api/flow_control/__init__.py 註解停用(2A dark stub 回空、前端沒有人在呼叫),只剩 DI 註冊還活著。app/project/oscal_audit_service.py(409 行)是舊的 AP-scoped 生命週期,新模型走 /audit-round/*——但它仍然被 AuditService 和 stage_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 律③已經有樣板)、ProcessParticipantService/ProjectControlParticipantService→改用平台自己的指派能力(P12 完成後 participant 已經是套件)、UserService→改走身分名冊 port(D8 標準件)。這支是第 2 步的主戰場,第 1 步不動它。
這節在講什麼:四步怎麼走、每步做什麼、怎麼驗收。
這是草案,四步的實際範圍以拍板後的 D 裁決為準。 每一步各自都能交付、都能停下。
做什麼: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 步要搬的面積縮小。
做什麼:把 §5 判為通用的那 12 支(加上拍板後從「看不準」轉進來的)下沉(往下移到共用的底層)成 task 平台層;workflow_execution_service 那四條真正跨模組的線改成 port(D-9);stage_advance/stage_rollback 從 flow_engine 移到稽核那邊。
驗證:手測同上;另外加一條——請 Claude 證明「平台層 grep 不到 poam/audit_round/assessment_plan/ssp」。這條是分家成功與否的機械判準。
做什麼:FlowControlJobType 從寫死的枚舉改成申報制;job_service 切成三個 handler(D-1);問卷插件申報審核狀態機和設定規格(D18)。
驗證:問卷任務的「送審/核可/退回」走得通;把設定開關關掉之後審核步驟消失——這一條是「設定不是客製」的驗收核心。
做什麼:task 平台包(吸收 jedi-project 種子)和稽核插件各自上 Nexus,照 extraction-sop 的 D6/D7 標準(register 簽名/standalone harness/接入 README/拔掉測試)。
驗證:poetry show 看得到兩個版號;拔掉稽核插件之後系統仍然起得來(只是沒有稽核功能)。
順序刻意安排成「先分家、後打包」——第三階段已經證明:邊界切乾淨之後再補外殼,是機械工。反過來先打包,會把「哪些算通用」的判斷錯誤鎖進套件版號裡,之後要改,貴十倍。
這節在講什麼:哪些東西一個字都不能動,以及這份稿子刻意不碰的範圍。
這些東西一個字都不能動(重申 D5)
GRC_404001 等)——這是對外契約,前端 error-code.json 靠字串比對做多語系。改類別名稱可以(GrcErrorCode→FlowControlErrorCode 就做過),改值不行。/project/<p>/audit-round/<r>/stage/* 維持原樣;§7 那 28 條 api/project 路徑也一樣。其他邊界:
這節在講什麼:這份稿子跟先前七項決策各是什麼關係。
| 既有決策 | 本稿的關係 |
|---|---|
| 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 歸屬併入第四階段流程疆界三方設計」,本稿交付 |