FR-080 · 設計定案 · 2026-09-09(承接 FR-069 終局)

jedi-* 套件整併與服務化路線:25 支收成 21 支,每支可分開部署、預設兩支分開跑

承接 FR-069 收官後的 25 支套件。決策者提出三個要求——朝微服務模式、套件可在其他專案隨插即用、現況粒度太細。本檔給出定案分類(25→21,以內容級盤點為準:讀每支套件的 README 疆界、表欄位、port、消費者用途,不用行數與 grep 計數)、今天與目標的差距、以及從插件走到服務要補的七階。推理過程同步歸檔於 docs/analysis/2026-09-09-jedi-consolidation-25-to-20.md

🔴 第 1 棒分析,決策者不採用,待重新分析 附錄 A/B 證據可重用 今天 0 支是服務 七階補法・第 3 階為評估點 Notion 未開卡
§1

摘要

25 → 21套件數:合併 4 組(任務平台/系統字典/日誌/資產)

0今天以服務身分在跑的套件

16七階補完後可分開部署的套件(21 − 5 支函式庫)

2預設分開部署的套件(detection、evidence)

🔴 先看這個:每支套件有兩個互不相干的決定,本檔用兩個不同的詞

問的問題 本檔用詞 絕不用的詞
套件邊界 這支要不要跟別支合成一支 pip 套件? 合併/不動/拆環
部署方式 這支跑在主進程裡,還是自己一個容器? 進程內/分開部署 「獨立」(語意含糊,本檔不再單獨使用)

兩軸互不影響:detection「不動」同時「分開部署」(自己一個容器);participant「合併」(進 task-platform)同時「進程內」。

兩軸總覽(25 → 21)

單位 套件邊界 部署方式(預設) 部署方式(觸發條件成立後)
jedi-common 瘦身(system_logs 表與 DBLogHandler 搬去日誌套件) 函式庫
jedi-system-dict(暫名) 合併:system-config+system-menu 進程內
jedi-log 合併:api_logs+common 的 system_logs;log-forwarding 為 sink 子模組 進程內;轉發由同 image 的 worker 進程跑 轉發面可升獨立容器
jedi-notification 不動 進程內;投遞第 5 階後走 worker 可分開部署(投遞量大時)
jedi-iam 不動 進程內 可分開部署(claims 模式)
jedi-license-runtime 不動 進程內
jedi-integrity 不動 函式庫
jedi-file-upload 不動 進程內
jedi-task-platform 合併:吞 participant(決策者選 B:flow-engine 不併) 進程內
jedi-flow-engine 不動(決策者 2026-09-09 裁:分開是對的) 函式庫
jedi-oscal-v2 不動 函式庫
jedi-compliance-audit 不動 進程內
jedi-survey 不動 進程內 可分開部署(outbox 模式)
jedi-detection 不動 分開部署
jedi-remote-agent 不動 進程內(agent ingress 隨 detection 容器走)
jedi-evidence-classification 不動 分開部署
jedi-ai-dashboard 不動 進程內
jedi-ai-bot 不動 進程內 可分開部署(對話量大時)
jedi-issue 不動 進程內
jedi-asset 合併:device+information-system(決策者 2026-09-09 裁定:同性質資產清冊) 進程內
jedi-bulletin 不動 進程內

五個結論

  1. 分類要用兩把尺,且證據要看內容不看形狀。第一把是子領域型態(地基/通用/核心),第二把是部署剖面(函式庫/進程內/分開部署)。前六輪推演用行數、表數、grep 計數、pyproject 宣告判合併,四組合併案(system-infra 五合一、asset、detection 吞 remote-agent、ai 三合一)全部在讀了 README 疆界、表欄位、port、消費者用途後被推翻。手冊 D17 律①「想建外鍵=該住一起」才是合併的唯一判準:程式碼耦合可用 port 解,資料耦合不行。
  2. 25 支收成 21 支,四組合併:前三組以「表是否互掛外鍵、是否天天連表」為判準:① 任務平台——task-platform 吞 participant(六張參與者表全掛在任務表上,ORM 直接 relationship,兩包雙向 import);② 系統字典——system-config+system-menu(兩張 group/key/value 表、同一群人維護、零 port 使用);③ 日誌——jedi-log 收下 common 的 system_logs,log-forwarding 為 sink 子模組(兩張表都是產品自產紀錄,LogTypeCode 早預留 SYSTEM_LOG=2);④ 資產——device+information-system 合為 jedi-asset(決策者裁定:兩支同為資產盤點清冊、同一群人維護、都是半體,合併一次升完全體;欄位不重疊不構成拆開的理由,一支資產套件內含兩種資產型別是自然的)。flow-engine 不併(決策者 2026-09-09 拍板選 B,維持 FR-069 D-10)。其餘 15 支各自疆界站得住,不動。
  3. 今天零支是服務。FR-069 做的是模組化不是服務化:全部在同一進程、同一 DB、同一 transaction;零 broker、零 outbox;10 支有真獨立 harness 但沒有進 CI。
  4. 「可分開部署」是設計要求,「預設分開部署」是部署選擇。七階補完後,除 5 支函式庫外的 16 支都要能用 import 模式或 API gateway 模式跑,套件碼一行不改;但今天只有 detection 與 evidence-classification 有分開跑的收益。iam、survey 技術上都能分開部署,只是要等觸發條件。
  5. 前三階(harness 進 CI、契約層、資料解耦)約 20 棒完成插件模式,達成「其他專案隨插即用」;第 3 階收完停下評估第二個產品是否出現,再決定後四階(身分可攜、outbox worker、雙 adapter、detection 切出)。

下一步:第 0 階套件合併,順序 資產(最小、零 migration、決策者指定首棒)→ 系統字典 → 日誌 → 任務平台吞 participant。每組比照 FR-069 退役慣例(死名守衛、移出 EXTRACTED_PACKAGES、pin 改名、Nexus 發版等令)。

§2

判準

兩把尺

問的問題 取值 為什麼不能少
子領域型態 它是什麼?換第二個產品還會要嗎? 地基/系統基礎建設/安全與部署/平台核心/領域積木 決定依賴方向(只能往下)與「誰會裝它」
部署剖面 它跑在哪? 函式庫(0 route,被 import)/進程內/分開部署 決定要不要為它付網路稅;函式庫沒有部署身分

合併的三個條件(全部成立才合)

  1. 另一個產品會整包要它:切到「一個業務能力」為止,不切到「一張表」。1 千行、1 張表的套件單獨存在是 nanoservice。
  2. 資料自己擁有:有 FK、天天 JOIN 的表必須同單位;跨單位只留軟參照。
  3. 沒有任何人受害:同操作者、同生命週期、彼此零 import 或單向 import。

「可分開部署」與「預設分開部署」是兩件事

微服務的定義是「可分開部署」,關鍵字是「可」

業界定義(Newman):微服務是可獨立部署(independently deployable)的服務,不是「必須分開部署」。因此本檔對每支套件回答兩個問題:能不能包成服務(套件品質要求,七階補完後 15 支全部要能)與該不該今天就分開跑(部署選擇,看有沒有收益)。前面版本把「不該切」講成「不能切」,是錯的表述,本版已修正。

§3

定案分類

%%{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 L5["⑤ 領域積木(GRC 專屬,挑用或換掉)"]
    OSCAL["jedi-oscal-v2<br/>函式庫"]
    AUDIT["jedi-compliance-audit"]
    SURVEY["jedi-survey"]
    DET["jedi-detection"]
    EVID["jedi-evidence-classification"]
    DASH["jedi-ai-dashboard"]
    BOT["jedi-ai-bot"]
    ISSUE["jedi-issue"]
    ASSET["jedi-asset<br/>★ device+information-system"]
    BULL["jedi-bulletin"]
  end
  subgraph L4["④ 平台核心(插座)"]
    TASK["jedi-task-platform<br/>★ 吞 participant"]
    FLOW["jedi-flow-engine<br/>函式庫(不併)"]
  end
  subgraph L3["③ 安全與部署"]
    IAM["jedi-iam"]
    LIC["jedi-license-runtime"]
    INTEG["jedi-integrity<br/>函式庫"]
    FILE["jedi-file-upload"]
    RA["jedi-remote-agent"]
  end
  subgraph L2["② 系統基礎建設"]
    DICT["jedi-system-dict<br/>★ config+menu"]
    LOG["jedi-log<br/>★ api_logs+system_logs+forwarding"]
    NOTIF["jedi-notification"]
  end
  L1["① 地基 jedi-common(★ system_logs 搬走)"]
  L5 --> L4 --> L3 --> L2 --> L1
圖 1 — 定案後 21 個單位的五層;依賴只能往下,同層互不認識。★ 為本次異動

總表

單位 收哪些 異動 部署剖面 內容證據(一句)
jedi-common system_logs 表+DBLogHandler 搬去 jedi-log 瘦身 函式庫 它是根套件裡唯一的 ORM 表;留著讓每個裝 common 的產品被迫長這張表;jedi_system_log 曾因撞它表名被刪(CM-1472)
jedi-system-dict(暫名) system-config+system-menu 合併 進程內 兩張都是 group/key/value 表、同一群平台管理員維護、都零 port 使用、都登記簿型;差別只有 value 型別(JSONB vs 字串)
jedi-log api_logs+system_logs(含 DBLogHandler);log-forwarding 為 sink 子模組 合併 進程內;轉發走 worker 兩張表都是「產品自產紀錄」、同一群人看;LogTypeCode.SYSTEM_LOG=2 早預留;forwarding 是 logging 管線的 sink 與 system_logs 同層。條件:DBLogHandler 保留「app 建立前」掛載方式
jedi-notification 不變 不動 進程內;第 5 階後 worker 投遞 三個出站 adapter 會持續長;被 iam、survey 兩個 INotifier port 各自宣告一次——是 port 收斂問題不是合併問題
jedi-iam 不變 不動 進程內;觸發後可分開(claims 模式) 被 3 支套件+主專案依賴的中心,不宜併入任何東西;與 system-menu 的 ui_routes vs system_menus 只是長得像(權限樹 vs 字典)
jedi-license-runtime 不變 不動 進程內 與 integrity 長得像(都 Ed25519、都落地版)但一個管租戶權益(tenant 為 key、有 UI)、一個管 binary 完整性(machine 為 key、app 建立前跑);宿主用 callable 膠合正是刻意零耦合
jedi-integrity 不變 不動 函式庫 無 DB session、無 Flask app 也要能跑;併進 license 會把 DDD 依賴拖進啟動閘門
jedi-file-upload 不變 不動 進程內(SeaweedFS SDK) issue/detection/主專案多模組共用的附件基礎設施;四種 adapter+宿主第五種
jedi-remote-agent 不變 不動 進程內;ingress 隨 detection 容器 remote_agents.capabilities 已有 detection_scanfile_storage 兩種消費者;detection 只用它的 mTLS/JWT 工具函式,是上下層
jedi-task-platform 吞 participant 合併 進程內 participant 六張表全是 (資源 id, user_id) 關聯表,task_assignees.task_idjob_executions.idprocess_participants.process_idworkflow_executions.id,ORM 直接 relationship,主專案天天連表;兩包雙向 import。想建外鍵=該住一起(D17 律①)
jedi-flow-engine 不變 不動 函式庫 表面證據說它與 task-platform 是同一件事(四張綁定表全掛 job_executions);決策者 2026-09-09 裁「分開是對的」——BPMN 引擎維持可單獨使用,FR-069 D-10 不推翻
jedi-oscal-v2 不變 不動 函式庫 45 表全在 oscal schema、零 tenant_id;與 iam 的 roles 同名不同 schema
jedi-compliance-audit 不變 不動 進程內 對 oscal-v2 是重度使用(22 支 repo_impl 直 import)但是流程 vs 文件模型,不是同一件事;越層 import 是債不是合併理由
jedi-survey 不變 不動 進程內;觸發後可分開(outbox 模式) 14 表自成 survey schema;91 commits 最活躍
jedi-detection 不變 不動 分開部署 長任務、資源型態不同;任務型別申報書在宿主
jedi-evidence-classification 不變 不動 分開部署 LLM 長任務、起 docker 容器;與 ai-dashboard/ai-bot 無 import、無共表、操作者不同
jedi-ai-dashboard 不變 不動 進程內 無表、純 LLM 編排;能力增長在宿主 registry
jedi-ai-bot 不變 不動 進程內 無表、純聊天;與 ai-dashboard 唯一共通是「都打 anthropic」
jedi-issue 不變 不動 進程內 members.project 是 GitLab 語意字串非 FK;決策點是砍死碼不是併
jedi-asset device+information-system 合併 進程內 決策者裁定:兩支同為資產盤點清冊(D11 已把 device 歸資產盤點語境、information-system README 自稱「資產盤點的主體之一」)、同一群人維護、都是半體。內容盤點指出欄位不重疊(硬體屬性 vs FIPS-199 安全分級),但那是同一套件內兩種資產型別的差異,不是不同疆界;SSP inventory 的 picker 兩支都要,合併後一個入口
jedi-bulletin 不變 不動 進程內 薄到幾乎只剩 model,值得問的是「要不要收回主專案」而非「併去哪」;D14 已裁保留

四組合併的證據與被推翻的三組

🔴 前六輪的三組合併案被內容級盤點推翻;asset 由決策者裁定維持合併

曾判合併 形狀證據(誤導) 內容證據(推翻)
system-infra 四/五合一 都是平台管理員操作、各 1 表 1k 行 log-forwarding 跑在 logging dictConfig 與 gunicorn fork 期、有自己繞 RLS 的 session;config/menu 是 request 期 CRUD。生命週期不同不能綁一支。「同一個 UI 分頁」是巧合不是領域
detection 吞 remote-agent 套件本體 23 處 detection 字樣 那 23 處幾乎全在 agent_tasks.detection_tool_id 一個欄位;capabilities 已有 file_storage 第二消費者;detection 只用它的 mTLS/JWT 工具函式
ai 三合一 都打 LLM 無 import、無共表、操作者不同(稽核員 vs 任一使用者);該抽的是 LLM client 層,不是合併三支

教訓:形狀相似的東西內容可以完全不同。 合併的唯一判準是 D17 律①「想建外鍵=該住一起」,看表不看 code。

四組合併,各自的證據:

合併 想建外鍵? 天天連表? 同一群人? 同生共死? 決策者裁定
task-platform 吞 participant task_assignees.task_id → job_executions.idprocess_participants.process_id → workflow_executions.id ORM relationship ✓ 主專案 infra/flow_controlinfra/flow_engine 直接 JOIN TaskAssignee ✓ 兩包雙向 import 選 B:併 participant、不併 flow-engine
system-config+system-menu ✓ 平台管理員 ✓ 都是登記簿型、零 port 使用 直接做
jedi-log 收 system_logs+forwarding ✓ 同一個「日誌」頁面 LogTypeCode 預留、撞表名炸過 直接做
device+information-system → jedi-asset ✓ 資產管理員 ✓ 都是半體、都歸資產盤點語境(D11) 決策者裁定合併;分析曾以「欄位不重疊」撤回,被駁回:同一套件內兩種資產型別是自然的,欄位不重疊不是拆開的理由

flow-engine 不併:決策者 2026-09-09 拍板

內容證據說 task-platform 與 flow-engine 是同一件事(四張綁定表全掛 job_executionsFlowControlJobTypejob_executions.type 落庫值、ITaskExistenceQuery 只為跨包查 id)。決策者裁「flow-engine 分開是對的」——BPMN 引擎維持可單獨使用,FR-069 D-10 不推翻。代價:兩包之間繼續靠 ITaskExistenceQuery port 跨包查 id。反悔條件見反悔表。

命名:系統字典套件暫名 jedi-system-dict,替代 jedi-system-config(沿用其中一支的名字,menu 併入)。日誌套件沿用 jedi-log(dist 名)但 import 名 jedi_api_log 要改回 jedi_log,因為它不再只是 api log。

§4

現況與差距

今天實際的部署形狀

🔴 25 支套件裡,以服務身分在跑的是零支

全部在同一個 Flask 進程、同一個 DB、同一個 transaction。三條死因:零非同步邊界(無 broker/outbox/domain event);單一 DB 加 30 張 RLS 表,session 變數由 iam 中介層每 request 注入;跨套件直接 import 是函式庫耦合不是 API 契約。

真正以獨立進程跑的東西都不是 jedi-* 套件:RUN_MODE=socketio(同 image 的傳輸分身)、License Center(獨立 repo,license-runtime 是它的客戶端)、客戶主機上的 agent 執行檔(remote-agent 是伺服端)、SeaweedFS/Postgres/Redis(基礎設施)。

脫離宿主啟動的能力:harness 三級

harness 是每支套件自帶的最小獨立宿主(harness/dev_app.pydocker-compose.yml):起一個空 Flask app、連自己的空 DB、register() 掛上、宿主該給的 port 用 Fake 頂替、test_client 實打 route。它證明零隱藏耦合——比 grep 守衛強一級,因為對空庫跑,SQL 字串裡偷 JOIN 宿主表會直接炸。FR-069 D7 列為標配。

等級 內容 支數 哪幾支
A 真脫離 compose 起自己的 Postgres、create_all、實打 route 10 iam(空庫 seed 後 POST /login 拿真 JWT)、file-upload、flow-engine、bulletin、device、information-system、license-runtime、log-forwarding、remote-agent、ai-bot
B 吃假的 有 dev_app,port 全用 Fake,沒有自己的 DB 7 participant、compliance-audit、evidence-classification、notification、integrity、system-config、ai-dashboard
C 沒有 8 common(合理)、oscal-v2、detection、issue、log、survey、system-menu、task-platform

兩個未兌現的承諾

harness 沒進 CI。D7 寫「由 CI 獨立執行」,monorepo 零 CI 檔。手動跑的證明三個月內壞掉沒人知道。 ② 最該服務化的 detection 是 C 級。它 147 檔、依賴四支兄弟套件,最難寫 harness,也最沒證明自己能獨立活著。服務化第一步不是切,是先補 harness,那一步會逼出它的 port 化。

有 Flask 殼就能做事嗎:差四樣

harness 只證明「活著」,離「有用」差四樣,每樣對應七階中的一階:

缺的東西 宿主裡誰供應 單獨跑時 補法
DB 加 schema 宿主庫、migration 已套、RLS 已建 create_all 空庫——沒 RLS、沒 seed、沒 migration 歷史 第 3 階
身分脈絡 iam 中介層每 request 查 users 表建 context 沒有 users 表建不出 context,harness 用 _FakeGuard 放行 第 4 階
port 實作 宿主接線盤接上真服務 只有一種實作:進程內直呼;接線盤 requests.httpx. 為零 第 2、6 階
呼叫者 宿主 route 或其他 service 直接 call 沒有東西打它;FE 只認宿主一個 base URL 第 6 階
§5

從插件到服務:七階

決策者要的效果拆成三個目標,需要的深度不同。A、B 是插件模式,C 是微服務模式;A 不需要等 C。

目標 一句話 補到第幾階
A 其他專案裝上就有 API pip install 幾支、寫一支 app_factory、起得來 1~3
B 插件可拔可換 拔一支宿主照跑;換同 port 實作宿主不改 2~3
C 套件可搬到另一進程跑 同一份碼,宿主改一個設定就從 import 變 HTTP 4~7
%%{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
  S0["第 0 階 套件合併 25→21"]
  subgraph PLUGIN["插件模式(目標 A、B)"]
    S1["第 1 階 harness 全面 A 級 + 進 CI"]
    S2["第 2 階 port 收斂成契約層 jedi-contracts"]
    S3["第 3 階 資料層解耦 + 宿主範本"]
  end
  EVAL{"評估點:第二個產品<br/>來了嗎?要哪幾支?"}
  subgraph MICRO["微服務模式(目標 C)"]
    S4["第 4 階 身分可攜(claims 進 token)"]
    S5["第 5 階 Postgres outbox + RUN_MODE=worker"]
    S6["第 6 階 雙 adapter + 宿主薄連接器"]
    S7["第 7 階 detection 第一刀"]
  end
  S0 --> S1 --> S2 --> S3 --> EVAL
  EVAL -->|要| S4 --> S5 --> S6 --> S7
  EVAL -->|不要/未定| STOP["停在插件模式<br/>已達成隨插即用"]
圖 2 — 七階補法:前三階完成插件模式,第 3 階收完是評估點,後四階完成微服務模式
現況 補什麼 買到什麼 棒數
0 套件合併 25 支 資產(device+information-system)→ 系統字典(config+menu)→ 日誌(log 收 system_logs+forwarding)→ 任務平台吞 participant 25→21 4
1 harness + CI A 10/B 7/C 8;零 CI C 級 8 支補 compose+dev_app;B 級 7 支補自己的 DB;monorepo 加 .gitlab-ci.yml 每支一 job 每支天天證明自己活著 8~10
2 契約層 port 散在 6 支套件共 27 支、語意重疊;接線盤散四處 開極薄套件 jedi-contracts(只放 Protocol 與 dataclass,零實作零依賴),收斂成約十支;跨單位 import 全改走契約;AST 守衛改按五層分組 換實作不改套件 3~4
3 資料解耦 + 宿主範本 四條跨單位 FK;task-platform 擁有兩張認識插頭的表;只有 2 個 schema;migration 隨包只 7 支 FK 改軟參照;插座兩張表改申報制(用既有 register_job_type(),只存 (job_execution_id, plugin_key, ref_uid) 通用表);每單位一 schema;migration 全隨包;抽 jedi-host-template(最小 app_factory 掛 system-dict+log+notification+iam)與 meta-package jedi-platform-suite 插件模式完成,其他專案可裝 4~5
4 身分可攜 JWT 只帶 uid;每 request 查 users 表 jwt_mw.py 既有 additional_claims_loader 把 tenant/org paths/capabilities 塞進 token;改 RS256 公鑰驗簽;契約層加 claims-only 實作;撤銷走 Redis 或短 TTL 第二進程不打 iam 就能建 context 套 RLS 2
5 非同步邊界 零 outbox;RUN_MODE 只有 api/socketio;notification 全同步;detection 長任務用 Thread jedi-common 加 outbox 表與 publish()main.pyRUN_MODE=worker 輪詢 outbox(FOR UPDATE SKIP LOCKED不引 broker);先搬寄信、log 轉發、detection 長任務 第一個業務 worker,同 image 3
6 雙 adapter + 連接器 port 只有進程內實作 為 detection、evidence 加 adapters/http/(OpenAPI 已由 flask-apispec 產出);宿主設定 `UNIT_MODE_=inproc remote;宿主薄連接器 jedi--connector` 做申報與轉呼叫 同一份碼兩種跑法
7 detection 切出 自己的 schema、容器、worker;agent ingress 走自己的 port;宿主用 connector 接。evidence 照抄 微服務模式完成 2~3

工作量:第 0~3 階約 20 棒達成「隨插即用」;第 4~7 階約 10 棒。第 3 階收完停下評估——那時才知道第二個產品來不來、要哪幾支。

🔴 三個會讓整件事白做的風險

① harness 沒進 CI——第 2~7 階建立的一切半年內靜默腐化。 ② jedi-contracts 長成第二個 jedi-common——必須零實作零依賴,AST 守衛擋它 import 任何非 typing 的東西。 ③ 為了 C 提前引 broker——這個規模 Kafka/RabbitMQ 是純負債,落地版多一個容器。Postgres outbox 撐到第二個產品出現都夠。

§6

終局形狀

每支套件的部署地位

地位 意思
函式庫,不適用 common、contracts、oscal-v2、flow-engine、integrity 0 route,被 import,沒有部署身分
預設分開部署 detection、evidence-classification 長任務、資源型態不同,今天就有收益。remote-agent 的 mTLS ingress 隨 detection 容器走
有觸發條件時分開 iam、survey、ai-bot、notification、jedi-log 的轉發面 機制備好,觸發條件成立就切,見下表
可以但沒場景 task-platform(含 participant)、compliance-audit、asset、issue、bulletin、ai-dashboard、license-runtime、remote-agent、file-upload、system-dict 七階補完技術上都能切;與宿主同 transaction 寫入,切出去換分散式一致性問題、零收益

iam 與 survey 怎麼分開部署

iam:改 claims 模式,是 21 支裡最有資格分開部署的

今天「每 request 拿 uid 查 users 表」綁死進程,那是實作不是本質。改業界標準兩段式:發 token時把 tenant/org paths/capabilities 塞進 claims;驗 token由每個消費者自己用公鑰驗簽、從 claims 建 context、注入 RLS——不打 iam;撤銷走共享 Redis;只有名冊查詢(非 request path)走 HTTP 且可快取。這就是 Keycloak 模式,改完 API 進程每 request 只做本地驗簽,延遲比查 DB 還低。

survey:資料早已自成疆界,缺的是跨界寫入

自有 schema、14 表自成疆界。綁進程的只有兩件事:向插座申報任務型別——第 6 階的 jedi-survey-connector 在宿主進程內申報;作答完成更新任務狀態——今天同 transaction 寫兩邊,改成 survey 寫自己的表加 outbox 事件,task-platform 消費事件更新 job。唯一代價是最終一致的幾百毫秒延遲,問卷場景可接受。

兩種模式的實作形式

契約層 jedi-contracts
  IProjectDirectory(Protocol)
       ↑ 實作 A                       ↑ 實作 B
  InProcProjectDirectory           HttpProjectDirectory
  (直接 call task-platform)      (打 gateway /api/projects/{uid})

宿主接線:UNIT_MODE_survey=inproc | remote
  • import 模式pip install jedi-surveyregister() 掛 route,port 注入實作 A。今天的樣子。
  • gateway 模式:survey 跑自己的容器,gateway(落地版用已在 stack 裡的 Traefik/nginx)把 /api/survey/* 路由過去;宿主只裝 connector 申報,port 注入實作 B。
  • 套件碼一行不改,它只認 Protocol。

預設部署的容器

容器 內容 性質
guidant-api 單體 API:進程內套件加宿主骨幹 模組化單體
guidant-socketio 同 image,傳輸進程 傳輸分身
guidant-worker 同 image,outbox 消費者 單體的第二進程
guidant-detection detection 服務加自己的 worker 真微服務 ①
guidant-evidence evidence-classification 服務 真微服務 ②
license-center 獨立 repo 外部真服務
db/redis/seaweedfs 基礎設施 現成品

從三個容器變七個。一個模組化單體帶三個分身,加兩個真服務,加一個外部簽發系統,是這個團隊規模與產品形態下的合理上限。

觸發條件

套件 什麼情況升格為分開部署
iam 第二個產品要共用同一套帳號。屆時先評估換 Keycloak,自建服務是次選
survey 出現「只要問卷、不要 GRC」的第二個產品且要分開部署
ai-bot 對話量大到拖垮 API 進程延遲;它 0 表純出站,技術上最容易切
notification 投遞量大到 worker 要獨立擴縮,或客戶要求通知網路出口與主系統隔離
jedi-log 轉發面 客戶要求 SIEM 轉發與主系統隔離部署;升為獨立容器但仍不是業務服務
§7

反悔條件

第一欄是本檔已定案的事(現在就是這樣),第二欄是什麼情況要重議,第三欄是重議後會變成什麼。這張表不是待辦清單。

已定案 什麼情況要重議 重議後變成
flow-engine 不併進 task-platform(決策者選 B) 出現第三個要跨包查 job_executions 的 port,或有人為了繞 ITaskExistenceQuery 直接 import 對方 ORM 兩包合一
participant 進 task-platform 出現「只要指派、不要任務」的消費者(例如組織級責任矩陣) participant 拆回獨立,改用軟參照
system-config+system-menu 合併 任一方長出 port 實際使用者或 adapter 名冊 拆回兩支
system_logs 搬進 jedi-log DBLogHandler「app 建立前」掛載方式在合併後做不到 改為獨立 jedi-system-log
remote-agent 不併進 detection file_storage 能力被移除,且三年內無第三種 agent 能力 併進 detection
device+information-system 合併為 asset 出現「只要設備清冊、不要資訊系統」的消費者 拆回兩支
notification 不併 不設反悔條件:它是被三處 port 宣告的供應者,合併方向只會更錯
只有 detection、evidence 預設分開部署 某支進程內套件出現實測資源競爭 該支升為分開部署
不引 broker,用 Postgres outbox outbox 輪詢延遲或吞吐在實測中不夠用 換 Redis Streams 或 RabbitMQ

§8

附錄 A:實查數據

規模與跨套件 import

套件 行數 route 對兄弟套件 import(非 common)
jedi-detection 17,013 11 35 remote-agent 6/flow-engine 5/iam 2/file-upload 1
jedi-oscal-v2 15,837 45 0
jedi-iam 15,147 17 41
jedi-compliance-audit 13,604 9 5 oscal-v2 36/task-platform 13/flow-engine 10/participant 8/iam 5
jedi-survey 11,360 14 32 participant 7/iam 7/flow-engine 2/device 1/task-platform 1
jedi-flow-engine 6,791 5 0
jedi-participant 5,310 6 20 flow-engine 3/task-platform 2
jedi-evidence-classification 3,850 2 10
jedi-issue 3,634 6 1 file-upload 9
jedi-license-runtime 3,608 2 1 raw SQL 直打 iam v_user_capabilities
jedi-common 3,476 1 0
jedi-task-platform 3,450 5 10 participant 2
jedi-remote-agent 2,824 3 1
jedi-integrity 2,768 0 0
jedi-file-upload 2,273 1 7
jedi-ai-dashboard 2,097 0 2
jedi-log-forwarding 2,065 1 1
jedi-log 1,315 1 2
jedi-system-menu 1,203 1 5
jedi-information-system 1,146 1 0
jedi-system-config 988 1 0
jedi-notification 800 0 1
jedi-device 780 1 0
jedi-bulletin 656 1 0
jedi-ai-bot 516 0 1

跨套件 FK 與 pyproject 相依

來源 目標 意義
common → tenantsorg_units(iam) FK 地基反向依賴上層
task-platform → users×2、org_units×2(iam)、devices(device) FK 第 3 階改軟參照
compliance-audit → upload_files(file-upload) FK 第 3 階改軟參照
task-platform → survey、device、information-system pyproject 插座反向依賴插頭
iam → notification pyproject notification 可坐 iam 下面
file-upload → iam pyproject file-upload 不能坐 iam 下面
participant → task-platform(2 處 import) 程式碼 循環相依,見附錄 B

執行期事實

  • jedi_iam/middleware/jwt_mw.py 每 request 呼叫 user_service 查 DB 建 UserContextDTO;JWT 只帶 identity=user_uidlogin_service.py:132
  • api_logs 寫入點在主專案 common/middleware/app_mw.py,同步 session.add
  • 零 broker/outbox/domain event;DB 只有 configsurvey 兩個 schema;30 張表有 RLS policy
  • port 分兩處:jedi-common 3 支身分名冊 resolver;六支套件各自 27 支(participant 6、detection 5、compliance-audit 5、task-platform 5、survey 4、license-runtime 2);宿主接線盤 requests.httpx. 為零
  • migration 隨包只 7 支:detection、evidence-classification、file-upload、license-runtime、log-forwarding、remote-agent、participant
  • remote-agent 套件本體:detection 23 次、tool_id 13 次、scan 7 次;消費者 detection 5 處、主專案 app/remote_agent 3 處
  • notification 實際內容:SMTP/Discord/Telegram 三個出站 adapter 加工廠
  • 主專案 readmodel 有 22 個 JOIN 的跨疆界查詢(ssp_control_implementation_query.py),屬骨幹不隨套件走

衛生問題

  • iam、file-upload、flow-engine 的 pyproject 仍宣告已退役的 jedi-auth/login/mfa/captcha
  • survey、flow-engine 宣告了叫 jedi-python-package 的依賴,疑似誤填
  • 主專案 infra/ 仍有 __tablename__ = "workflow_executions" 的 model,與 flow-engine 同名
  • poams 在 oscal-v2 與 compliance-audit 各定義一次、roles 在 iam 與 oscal-v2 各定義一次,需確認是同表兩 ORM 還是不同 schema
§9

附錄 B:推演過程與分歧點

四輪推演

判準 結果 為什麼被否決
09-07 提案 換產品原封裝 25→20 判準是插件粒度,會留六支千行級獨立單位;integrity/license-runtime/remote-agent「刻意不合」的理由框架被本檔推翻
第 1 輪 部署單位(FK/同步密度/擴縮型態) 25→14 iam 吞 config/menu 讓所有插件被迫依賴 iam,方向反了;flow-engine、oscal-v2 是函式庫,綁進插件就沒人能單獨用
第 2 輪 雙模式可行性 25→12 同上且更嚴重,核心與服務混成一欄
第 3 輪 型態+剖面兩把尺 25→19 大方向成立,三處判錯:file-upload 判微服務(實為函式庫耦合)、notification 不併 system-infra(iam pyproject 證據推翻)、participant 併 task-platform(過重)
第 4 輪 對照外部 AI 表逐項實查 25→20 仍用形狀證據;決策者質疑「內容你有了解過嗎」
第 5 輪 手讀 remote-agent/detection/device/information-system 內容 25→22 撤回 asset 與 detection 吞 remote-agent;system-infra 仍判四合一
第 6 輪 兩個 subagent 對 25 支做內容級盤點(7 題/支) 25→22 system-infra 拆成字典+日誌兩組;participant 由拆環改為併入 task-platform;flow-engine 三合一提案由決策者否決(選 B)
第 7 輪(定案) 決策者裁定 25→21 asset 合併恢復——分析以「欄位不重疊」撤回被駁回:同性質資產清冊該住一起,套件內兩種型別是自然的。教訓:分析把「內容證據」推到極端,連決策者明確要的合併都撤,是矯枉過正

外部 AI 分類表逐項對照

決策者提供一份外部 AI 的表並明言「只信一半」。裁決依據是實查證據,不是誰說的:

項目 外部 AI 定案 裁決 依據
system-infra 五合一 五合一 拆成字典+日誌兩組,notification 獨立 第 4 輪採納、第 6 輪推翻 log-forwarding 生命週期不同;notification 是被多處 port 宣告的供應者
asset 二合一 二合一 合併 第 4 輪採納、第 5 輪撤回、第 7 輪決策者恢復 他對:同性質資產清冊
ai 三支 各自不動(無理由) 各自不動 採納結論、補理由 同類型≠同疆界
remote-agent 獨立,「mTLS 派工基礎設施」 不合 第 4 輪否決、第 5 輪改採納 他對:capabilities 已有 file_storage 第二消費者
bulletin 「獨立或併進 infra」 獨立 否決模糊 過不了開機必要;D14 已裁
file-upload 獨立,「issue 會被迫依賴整支 infra」 獨立 採納結論、否決理由 issue 本來就依賴 jedi-log;真正理由是 file-upload 依賴 iam
participant 不動 併入 task-platform 否決 他沒看到循環相依與六張表全掛任務表
common 不動 瘦身 否決 system_logs FK 到 tenants

外部表完全沒碰「能不能雙模式」的門檻(port 化、軟參照、outbox、插座反向依賴)。回頭看,它的四項合併建議兩對兩錯,本檔前四輪也是——雙方都是用形狀判的,錯法一樣。

內容級盤點(第 5~6 輪,2026-09-09)

前四輪用形狀證據判斷後,決策者質疑「兩者的內容你有了解過再分類嗎」。第 5 輪派兩個 subagent 讀完 25 支的 README 疆界宣告、表欄位語意、port 設計、消費者用途、依賴方向、變動節奏,每支回答固定 7 題。結論寫進正文定案表「內容證據」欄;本節只記盤點抓到的債。

port 重複宣告(第 2 階契約層要收)

  • ILicenseGuardIProjectRoleGuardIIdentityGuard 在 task-platform 與 compliance-audit 各一份逐字相同
  • INotifier 在 iam、survey 各宣告一次,供應者都是 notification
  • ISettingsReaderINotifier 樣板在 flow-engine、device、information-system 三份零使用
  • IUserNameResolver 在 jedi-common 與 log-forwarding 各宣告一份同形狀

越層與反向依賴(第 3 階要解)

  • compliance-audit 直接 import 22 支 oscal-v2 的 infra *_repo_impl,繞過 domain 層
  • survey 的 task_surveys 對 flow-engine 與 device 做 ORM relationship,與自家 README「存 id 不建外鍵」相違
  • participant → flow-engine ORM(infra/model/project_participant.py:6);併入 task-platform 後仍是跨包 relationship

通用套件綁死產品的洩漏(各套件下次動時順手修)

套件 洩漏 修法
remote-agent agent_tasks.detection_tool_id NOT NULLIAgentTaskLifecycleListener.on_scan_* 命名 payload_ref JSON、hook 改 on_task_*
task-platform projects.living_ssp_id(守衛只掃 task/ 子目錄漏掉主檔) 移到 compliance-audit 的擴充表
survey task_surveys.inherited_from_round_id(稽核輪次詞彙住進問卷表) 改軟參照+通用名
evidence-classification framework_id server_default cmmc-l1 拿掉 default
file-upload storage_scope=SYSTEM 語意寫死「最高層 tenant 的 STORAGE_CONFIG」;storage_type 被宿主擴成 REMOTE_AGENT 且 recon adapter 直接 query 套件 ORM scope 語意改由宿主 port 決定
license-runtime licensed_job_types 契約形狀來自 jedi_task_platform...flow_control_job_type_enum 契約改字串集合
iam security_policy.py 明文寫「值存 system_configs ROOT/RUNTIME_CONFIG」 隨 system-dict 合併一併改 port
oscal-v2 oscal_parser_factory.TYPE 只硬編 CMMC 改 registry
issue GitLab attachment adapter 在主專案 di_containers/feedback 仍被註冊,與 README「沒在用」矛盾 砍死碼前先核實 feedback 走哪條路

participant 為什麼當初獨立、循環相依怎麼來的

層次 理由 今天還成立
圖論 FR-069 開案盤點的六模組核心環(760 檔)中,participant 是最大 hub(被 60+ 處 import)但 out-degree 僅 8。能不能抽看 out-degree——import 名不變,消費端零改動。抽掉它核心環最粗兩條邊自動消失,後續拆解才有路 成立,已兌現
疆界 不是身分(管「你是誰」)、不是流程(管「下一步」),它管「這一步歸誰」;換產品規則不變 成立
時序 participant 抽出是 2026-08-31(CM-1477),當時只有 399 行掏空的舊 jedi-project;task-platform 隔天 2026-09-01 才合成(CM-1492)。抽出那刻沒有任務平台可併 歷史事實

循環相依不是設計,是搬家收尾的漏網之魚

抽出當天 project_participant_service.py 直接 import 舊 jedi_projectProjectDomainService同一時刻 domain/ports.py:84 已定義 IProjectDirectory port,README 也寫「專案在哪:走 port」。設計者知道該走 port、port 也建好了,但 _resolve_project_id_from_uid() 沒接。隔天 CM-1492 改名的字串替換把這句變成對 jedi_task_platform 的直接依賴;同棒 task-platform 反向 import participant 2 處。兩邊各自合理的一步,合起來就是環。這是「先搬家、後裝修」的已知代價,CM-1484 已用同樣方式償還過 iam 那條債。

§10

附錄 C:與既有文件的關係

  • 推翻architecture-handbook/jedi-module-map-proposal.html(09-07,未 commit)的 remote-agent 獨立與「刻意不合」理由框架
  • 修正architecture-handbook/dependency-map.md 寫「功能套件彼此零直接依賴」——第四階段後已不成立,實況見附錄 A
  • 補充architecture-handbook/package-taxonomy.md 的「同族≠同疆界」仍有效,本檔加第三個概念「同單位」
  • 不推翻architecture-handbook/project-platform-decision.md(插座 vs 插頭),是第 3 階申報制的依據
  • D7 未兌現:harness 進 CI,第 1 階補
  • D16 需改寫:「只准依賴 common」→「單位內自由、跨單位走契約層」,隨第 2 階改 design.md
  • D6 未落實:migration 隨包只 7 支,第 3 階補齊
  • D-10 維持:flow-engine 與 task-platform 兩包不合,決策者 2026-09-09 重申;本檔附錄 B 記錄了推翻它的內容證據,供日後反悔時免走回頭路
  • D11 維持:device 不併檢測;device 與 information-system 合為 jedi-asset(同歸資產盤點語境,與 D11 精神一致)