架構手冊 · 套件分層 · living

五層分類、合併與拆分判準、四個可量指標

這頁回答「這支套件該歸哪一層、兩支該不該合、進步有沒有量得出來」。分層決定誰可以依賴誰(只准往下),判準決定一支套件該管多大範圍(合不合),指標決定驗收(做完有沒有變好)。來源是 FR-080 第 2 棒分析報告。

五層 25 → 21 依賴只准往下 四個可量指標

②層的名字「橫切」待決策者定案。 第 2 棒討論中用過「中介層」「橫切」兩個名字,決策者說先放著、之後再定。本頁一律用橫切,定案後全頁一次改名。層的內容與判準不受名字影響。

這頁是 FR-080 第 2 棒分析報告 第 1、2 節的長期規範版:分析報告記的是「當時怎麼判的、討論中推翻了什麼」,這頁記的是「以後照這個判」。 與 套件分類地圖 的分工:那頁把套件分成十族(誰會裝、壞了會有什麼後果),是給「要加新能力時該照哪個先例抄」用的;這頁分五層(誰可以依賴誰),是給「這支能不能用那支」用的。族與層是兩個獨立的切法,同一族的套件可能分在不同層。

§1

五層,依賴只准往下

「層」決定的是誰可以依賴誰。判一支套件屬於哪一層,就問兩個問題:拔掉它會怎樣、換一個完全不同的產品還要不要它。

層 業界叫法 它是什麼 壞了會怎樣 換一個產品
① 基座 kernel / core library(核心函式庫) 所有東西踩在它上面:資料庫連線與交易、例外格式、查詢底座、代表「現在是誰在操作」的資料結構 全停 一定帶著走,不能有第二套
② 橫切 middleware / cross-cutting concern(橫切關注點) 站在功能外面的路上,每個請求都會經過、但功能本身不會去呼叫它 全停或全開 帶著走,實作可以換
③ 平台服務 platform / shared service(共用服務) 每個業務都會用、但自己不含業務邏輯的「功能」:寄信、存檔案、系統設定、下拉選單、公告、資產清冊 該功能停,其他照跑 選裝
③.5 平台骨架 task engine(任務引擎,像 Jira 的 issue 引擎、ServiceNow 的 task 表) 本產品獨有:專案、任務、指派、流程,是所有業務模組插上去的插座 任務推不動 只有同類型的產品才要
④ 業務 feature / domain module(功能模組) 這個產品之所以是這個產品的東西 該業務停 只有合規稽核類的產品要

依賴方向是硬規則:④ → ③.5 → ③ → ② → ①,永遠不能反過來。

①是純函式庫不是插件,也是唯一允許被任何一層直接依賴的。②③④三層共同的包裝方式叫插件——「插件」不是第五層,是這三層都用同一種裝法(Odoo 那套系統裡的 base、mail、account 三個模組,裝法完全一樣)。三層的差別只在依賴方向。

「middleware」這個字有兩個意思要分清楚:狹義是指請求進來時排隊經過的攔截點(Flask 的 before_request);廣義是指夾在基座與業務之間的通用層。jedi-common 是①基座,比 middleware 更底層,不屬於②。

§2

②橫切層怎麼運作:誰寫、誰掛、誰宣告

②不是「一包公共工具箱」,是一排各自獨立、各管一件事的套件。它們做的動作只有三種:

動作 做什麼 實例
擋下 不符合就攔住,請求根本進不到功能裡 iam 驗證登入權杖、license 到期後轉唯讀、integrity 的啟動閘門與鎖定機制
補資訊 幫請求帶上功能需要、但功能不該自己去查的東西 iam 組出「現在是誰在操作」的資料結構、開交易時注入租戶路徑讓資料庫做隔離、跨網域請求的標頭
記錄 看過就好,不改也不擋 log 的 API 存取紀錄、稽核事件、integrity 每 4 小時抽查一次

四個問題各有各的歸屬,不可以互相跑到對方的位置:

問題 歸哪裡 實例
中介處理本身怎麼做 ②層,各自一支套件 jedi-iam、jedi-log、jedi-integrity、jedi-license-runtime
這個產品要掛哪幾支 主專案的接線盤 core/app_factory.py、common/authz/__init__.py 的決策表、config/app_modules.py
這支功能要被哪幾個管 功能自己的 API 上一行宣告 @auth_required、require_capability("xxx")、license_guard.require("ai-dashboard")
中介之間共用的演算法 ①基座,或另外開一個小套件 建議把數位簽章驗證(Ed25519)與機器指紋計算從 license-runtime 拉出來

功能只能「挑」不能「自己寫」。 FR-048 之前,「這個人有沒有權限」的檢查在四個地方各寫了一套,收斂成 common/authz 一套之後,CLAUDE.md 就明文禁止再另立新的檢查函式,講的就是這條規則。唯一例外是要先查出這是誰的資料才知道判誰的那種檢查(例如判斷這人是不是這個專案的負責人),查資料那一步在功能自己的服務層,但真正的判定仍然呼叫 common/authz。

在插件的設計裡,中介是「接口」(port,就是套件對外宣告「我需要有人幫我做這件事」的一組函式,由主專案接上實作):套件只宣告自己需要有人做登入驗證、授權檢查,主專案接線時把實作塞進來。四支有 API 的系統套件都刻意不給守衛預設值、沒接線就拒絕掛載——寧可服務起不來,也不能默默變成沒有任何保護的 API。

§3

25 → 21 支歸位表

整併後 21 支各屬哪一層(合併理由見下一節的判準與分析報告第 3 節):

層 套件
① 基座 jedi-common
② 橫切 jedi-iam、jedi-log(含轉發)、jedi-integrity、jedi-license-runtime
③ 平台服務 jedi-notification、jedi-file-upload、jedi-system-core、jedi-asset、jedi-bulletin、jedi-issue、jedi-remote-agent、jedi-ai-bot、jedi-ai-dashboard
③.5 平台骨架 jedi-task-platform、jedi-flow-engine
④ 業務 jedi-oscal-v2、jedi-compliance-audit、jedi-survey、jedi-detection、jedi-evidence-classification

四組合併(25 → 21):

合併後 由誰組成 一句理由
jedi-asset jedi-device + jedi-information-system 決策者裁定;主專案的資產清冊表早就用一個「資產型別」欄位把設備與資訊系統當成同一張表的兩種型別
jedi-task-platform jedi-task-platform + jedi-participant 整個套件庫裡唯一一組互相 import 對方的循環;DEV 環境實查 12,479 筆指派全部對得上專案表
jedi-system-core jedi-system-config + jedi-system-menu 兩支都不到 1,200 行、都沒 import 別的套件、都沒外鍵、都只是字典型的增刪改查、常常同一批改
jedi-log jedi-log + jedi-log-forwarding 決策者裁定;兩支同屬②橫切層,「記錄」與「轉發到客戶的資安監控系統」是同一條日誌管線的上下游

明確不併的三組(看起來像但不是同一件事):oscal-v2 與 compliance-audit(前者是國際合規標準的資料模型、任何合規產品都可能用,後者是本產品的稽核流程)、integrity 與 license-runtime(一個管這台機器有沒有被竄改、一個管這個客戶有沒有買授權)、detection 與 remote-agent(一個是業務、一個只是通道)。

新產品起手式:裝這五支就有登入、存取紀錄、系統設定、下拉選單、寄信——jedi-common、jedi-iam、jedi-log、jedi-system-core、jedi-notification。其餘依產品需要選裝。

§4

以後新東西怎麼判:三個問題

① 拔掉它會怎樣

系統全停 → ①② 只少一個功能 → ③④

② 換一個不做稽核的產品還要不要

要 → ③ 不要 → ④

③ 它站在哪裡——站在功能外面的路上、功能不會呼叫它 → ②橫切,核心寫成請求攔截;功能會主動呼叫它做事 → ③④。

判完層之後形狀就定了,剩下只有一條硬規則:依賴只准往下。

§5

判準:一把尺,全程只用這組

「套件要不要合併」與「要不要分開部署」是兩件事,用兩組不同的問題判,不可以互相借用。講合併/不動時是在講一支套件該管多大範圍;講同一個行程/分開部署時是在講部署形態。

兩支套件該不該合併

滿足下面任一條就傾向合併:

問題 怎麼判
**資料是不是同生共死? 兩邊的表會想建外鍵、同一筆交易裡會同時寫兩邊、少了一邊另一邊就沒意義 → 合。這來自 FR-069 訂的規則:想建外鍵就代表這兩張表該住在同一支套件裡。只存對方 id 但不建外鍵的那種弱關聯不算。**
換一個產品會不會被一起拿走? 一個跟稽核無關的新產品要裝 A,就一定也要裝 B(或反過來)→ 合。各自能單獨用 → 不合,即使功能看起來同一類
是不是同一個人在養?(輔助條件) 從來沒有各自演化過、每次都同一批改版、改一邊必改另一邊 → 支持合。前兩條都不成立時,光憑這一條不能合

反過來,只要有下面任一種情況就不合併,即使上面三條都很像:

不合的情況 說明
對方是很多人都依賴的供應者 合了之後會害一堆本來不依賴它的套件被迫依賴它
合了會變成雜物袋 套件開機時要做一堆不相干的事
兩支不在同一層 橫切層與功能層不混,層不同就不合

「跨層不合」這條是 FR-080 討論中新增的,因為犯過兩次同型的錯:一次判「log 併進 system-core」(log 是②橫切、config 與 menu 是③功能);一次差點拿「轉發可以失敗」這個部署理由來當合併理由。

判「該管多大範圍」跟判「要不要分開跑」是兩把尺,不可以互相借用。

一支套件能不能分開部署

問題 怎麼判
資料交換能不能全部改成網路呼叫? 它跟主專案之間的資料關聯要能全部改成「網路呼叫 + 只存對方 id 的弱關聯」。有跨界外鍵就不行,得先改掉
有沒有獨立的生命週期理由? 不同的擴縮型態(吃 CPU/吃 I/O/要跑很久)、不同的發版節奏、不同的安全邊界、不同的操作者
分開之後哪些查詢會斷? 主專案有哪些「一次撈很多張表」的查詢會斷掉?能不能改成呼叫 API 之後在程式裡拼起來,或另外做一份專門給查詢用的資料表?

沒有第二條理由(獨立的生命週期)就不分開部署。 微服務的成本(網路、資料一致性、部署、監控)要有理由才值得付。FR-080 的結論是這一輪只做插件,微服務降級為以後再看的路線圖。

§6

指標基線(2026-09-09)

四個可以量的指標,由 scripts/deliverables/plugin_metrics.py 實際算出來。後面每一棒做完都跑一次然後回寫,這樣「有沒有進步」是量出來的,不是講出來的。

# 預設路徑(BE = 本 repo,套件庫 = jedi-python-package)
python scripts/deliverables/plugin_metrics.py

# 指定路徑 / 輸出成機器可讀的 JSON
python scripts/deliverables/plugin_metrics.py --be <BE 根目錄> --mono <套件庫根目錄> --json
指標 這個數字在量什麼 基線(2026-09-09) 目標
(a) 套件之間直接 import 對方的處數 一支套件直接 import 另一支的類別,換系統就會斷 120 只剩 compliance-audit 對 oscal-v2 那一批(刻意保留)
(b) 宣告了卻沒人用的接口 套件宣告「我需要有人幫我做這件事」但自己一次都沒呼叫 16(其中樣板 10) 0
(c) 主專案接線程式的行數 主專案裡「把套件的接口接上實作」那些程式 7,457 下降(合併會消掉一部分,不會歸零)
(d) 裝上就有 API 的套件數 註冊一行、API 就長出來、主專案不用自己寫業務 17(機械判準)/12(嚴格判準) 19 支左右(flow-engine 與純函式庫除外)

(a) 分布:compliance-audit 72、survey 18、detection 14、issue 9、participant 5、task-platform 2。計算時排除 jedi_common(它是①基座,允許被任何人直接依賴)與套件對自己的 import。

(b) 與 (d) 跟第 2 棒手寫的數字對不上,是「兩邊量的東西不一樣」不是「算錯」,所以兩種讀法都留著。

  • (b) 腳本算出 16、第 2 棒寫 10:第 2 棒是手工列五支有殼沒肉套件的「讀設定」與「發通知」兩個樣板接口(5×2=10);腳本是系統性掃描,那 10 個全部命中(腳本輸出會標 ★),另外多抓到 6 個:detection 的 IEvidenceSink/IAgentDirectory、participant 的 IProjectDirectory/IAuditLogger、task-platform 的 IProjectRoleGuard/IIdentityGuard。這 6 個逐一實查確認套件源碼裡一次都沒用到——它們的轉接器欄位寫成 Optional[Any],連型別標註都沒用上那個抽象類別,所以宣告了等於沒宣告。16 是實況,10 是它的子集。
  • (d) 腳本算出 17、第 2 棒寫 12:卡片訂的機械判準是「有 api/ 目錄 + 有掛路由 + 主專案有掛這個插件」,這條會把「能用但要背一串」那一等(compliance-audit、detection、survey、participant、task-platform)也算進來——它們機械上確實註冊得起來,只是對別支的依賴走的是直接 import 不是接口。第 2 棒的 12 是「拿了就能用」那一等,另外含 jedi-integrity(它根本沒有 api/ 目錄,是啟動時的閘門不是 API 插件,機械判準抓不到)。兩個數字量的是不同東西:17 量的是「機械上裝得起來」,12 量的是「裝起來不用連帶背一串依賴」。

(c) 怎麼算出來的:di_containers/ 底下全部 .py 共 5,554 行 + 15 支轉接/接線檔共 1,903 行 = 7,457。用 wc -l 交叉核對一致。那 15 支的清單寫死在腳本的 HOST_WIRING_FILES 裡,接線檔增減時要同步改。

§7

這頁怎麼維護

  • 層有變動時才改這頁:新抽一支套件要歸層、某支改層、合併案落地。
  • 指標基線每棒回寫:FR-080 各棒收口時跑一次腳本,把新值追加到「指標基線」表(舊的那列保留,看得到趨勢)。
  • ②層改名定案時:把頁首那則待決提示拿掉,全頁「橫切」一次改成定案的名字。
  • 判準被推翻時:上面那些合併與分開部署的判準,如果因為實作發現不成立,改這裡並在分析報告第 9 節記一筆。