FR-080 · 設計定案 · 2026-09-09(承接 FR-069 終局)
承接 FR-069 收官後的 25 支套件。決策者提出三個要求——朝微服務模式、套件可在其他專案隨插即用、現況粒度太細。本檔給出定案分類(25→21,以內容級盤點為準:讀每支套件的 README 疆界、表欄位、port、消費者用途,不用行數與 grep 計數)、今天與目標的差距、以及從插件走到服務要補的七階。推理過程同步歸檔於 docs/analysis/2026-09-09-jedi-consolidation-25-to-20.md。
25 → 21套件數:合併 4 組(任務平台/系統字典/日誌/資產)
0今天以服務身分在跑的套件
16七階補完後可分開部署的套件(21 − 5 支函式庫)
2預設分開部署的套件(detection、evidence)
🔴 先看這個:每支套件有兩個互不相干的決定,本檔用兩個不同的詞
| 軸 | 問的問題 | 本檔用詞 | 絕不用的詞 |
|---|---|---|---|
| 套件邊界 | 這支要不要跟別支合成一支 pip 套件? | 合併/不動/拆環 | — |
| 部署方式 | 這支跑在主進程裡,還是自己一個容器? | 進程內/分開部署 | 「獨立」(語意含糊,本檔不再單獨使用) |
兩軸互不影響:detection「不動」同時「分開部署」(自己一個容器);participant「合併」(進 task-platform)同時「進程內」。
| 單位 | 套件邊界 | 部署方式(預設) | 部署方式(觸發條件成立後) |
|---|---|---|---|
| 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 | 不動 | 進程內 | — |
五個結論
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 支各自疆界站得住,不動。下一步:第 0 階套件合併,順序 資產(最小、零 migration、決策者指定首棒)→ 系統字典 → 日誌 → 任務平台吞 participant。每組比照 FR-069 退役慣例(死名守衛、移出 EXTRACTED_PACKAGES、pin 改名、Nexus 發版等令)。
| 尺 | 問的問題 | 取值 | 為什麼不能少 |
|---|---|---|---|
| 子領域型態 | 它是什麼?換第二個產品還會要嗎? | 地基/系統基礎建設/安全與部署/平台核心/領域積木 | 決定依賴方向(只能往下)與「誰會裝它」 |
| 部署剖面 | 它跑在哪? | 函式庫(0 route,被 import)/進程內/分開部署 | 決定要不要為它付網路稅;函式庫沒有部署身分 |
微服務的定義是「可分開部署」,關鍵字是「可」
業界定義(Newman):微服務是可獨立部署(independently deployable)的服務,不是「必須分開部署」。因此本檔對每支套件回答兩個問題:能不能包成服務(套件品質要求,七階補完後 15 支全部要能)與該不該今天就分開跑(部署選擇,看有沒有收益)。前面版本把「不該切」講成「不能切」,是錯的表述,本版已修正。
%%{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
| 層 | 單位 | 收哪些 | 異動 | 部署剖面 | 內容證據(一句) |
|---|---|---|---|---|---|
| ① | 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_scan、file_storage 兩種消費者;detection 只用它的 mTLS/JWT 工具函式,是上下層 |
| ④ | jedi-task-platform | 吞 participant | 合併 | 進程內 | participant 六張表全是 (資源 id, user_id) 關聯表,task_assignees.task_id 對 job_executions.id、process_participants.process_id 對 workflow_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.id、process_participants.process_id → workflow_executions.id ORM relationship |
✓ 主專案 infra/flow_control、infra/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_executions、FlowControlJobType 是 job_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。
🔴 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/dev_app.py + docker-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 化。
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 階 |
決策者要的效果拆成三個目標,需要的深度不同。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/>已達成隨插即用"]
| 階 | 現況 | 補什麼 | 買到什麼 | 棒數 |
|---|---|---|---|---|
| 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.py 加 RUN_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_ |
remote;宿主薄連接器 jedi- |
同一份碼兩種跑法 |
| 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 撐到第二個產品出現都夠。
| 地位 | 誰 | 意思 |
|---|---|---|
| 函式庫,不適用 | 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 寫入,切出去換分散式一致性問題、零收益 |
今天「每 request 拿 uid 查 users 表」綁死進程,那是實作不是本質。改業界標準兩段式:發 token時把 tenant/org paths/capabilities 塞進 claims;驗 token由每個消費者自己用公鑰驗簽、從 claims 建 context、注入 RLS——不打 iam;撤銷走共享 Redis;只有名冊查詢(非 request path)走 HTTP 且可快取。這就是 Keycloak 模式,改完 API 進程每 request 只做本地驗簽,延遲比查 DB 還低。
自有 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
pip install jedi-survey,register() 掛 route,port 注入實作 A。今天的樣子。/api/survey/* 路由過去;宿主只裝 connector 申報,port 注入實作 B。| 容器 | 內容 | 性質 |
|---|---|---|
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 轉發與主系統隔離部署;升為獨立容器但仍不是業務服務 |
第一欄是本檔已定案的事(現在就是這樣),第二欄是什麼情況要重議,第三欄是重議後會變成什麼。這張表不是待辦清單。
| 已定案 | 什麼情況要重議 | 重議後變成 |
|---|---|---|
| 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 |
| 套件 | 行數 | 表 | 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 | — |
| 來源 | 目標 | 意義 |
|---|---|---|
common → tenants、org_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_uid(login_service.py:132)api_logs 寫入點在主專案 common/middleware/app_mw.py,同步 session.addconfig、survey 兩個 schema;30 張表有 RLS policyrequests./httpx. 為零detection 23 次、tool_id 13 次、scan 7 次;消費者 detection 5 處、主專案 app/remote_agent 3 處ssp_control_implementation_query.py),屬骨幹不隨套件走jedi-python-package 的依賴,疑似誤填infra/ 仍有 __tablename__ = "workflow_executions" 的 model,與 flow-engine 同名poams 在 oscal-v2 與 compliance-audit 各定義一次、roles 在 iam 與 oscal-v2 各定義一次,需確認是同表兩 ORM 還是不同 schema| 輪 | 判準 | 結果 | 為什麼被否決 |
|---|---|---|---|
| 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 | 定案 | 裁決 | 依據 |
|---|---|---|---|---|
| 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 輪派兩個 subagent 讀完 25 支的 README 疆界宣告、表欄位語意、port 設計、消費者用途、依賴方向、變動節奏,每支回答固定 7 題。結論寫進正文定案表「內容證據」欄;本節只記盤點抓到的債。
port 重複宣告(第 2 階契約層要收)
ILicenseGuard/IProjectRoleGuard/IIdentityGuard 在 task-platform 與 compliance-audit 各一份逐字相同INotifier 在 iam、survey 各宣告一次,供應者都是 notificationISettingsReader/INotifier 樣板在 flow-engine、device、information-system 三份零使用IUserNameResolver 在 jedi-common 與 log-forwarding 各宣告一份同形狀越層與反向依賴(第 3 階要解)
*_repo_impl,繞過 domain 層task_surveys 對 flow-engine 與 device 做 ORM relationship,與自家 README「存 id 不建外鍵」相違infra/model/project_participant.py:6);併入 task-platform 後仍是跨包 relationship通用套件綁死產品的洩漏(各套件下次動時順手修)
| 套件 | 洩漏 | 修法 |
|---|---|---|
| remote-agent | agent_tasks.detection_tool_id NOT NULL;IAgentTaskLifecycleListener.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 走哪條路 |
| 層次 | 理由 | 今天還成立 |
|---|---|---|
| 圖論 | 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_project 的 ProjectDomainService;同一時刻 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 那條債。
architecture-handbook/jedi-module-map-proposal.html(09-07,未 commit)的 remote-agent 獨立與「刻意不合」理由框架architecture-handbook/dependency-map.md 寫「功能套件彼此零直接依賴」——第四階段後已不成立,實況見附錄 Aarchitecture-handbook/package-taxonomy.md 的「同族≠同疆界」仍有效,本檔加第三個概念「同單位」architecture-handbook/project-platform-decision.md(插座 vs 插頭),是第 3 階申報制的依據