這頁怎麼維護
- 層有變動時才改這頁:新抽一支套件要歸層、某支改層、合併案落地。
- 指標基線每棒回寫:FR-080 各棒收口時跑一次腳本,把新值追加到「指標基線」表(舊的那列保留,看得到趨勢)。
- ②層改名定案時:把頁首那則待決提示拿掉,全頁「橫切」一次改成定案的名字。
- 判準被推翻時:上面那些合併與分開部署的判準,如果因為實作發現不成立,改這裡並在分析報告第 9 節記一筆。
架構手冊 · 套件分層 · living
這頁回答「這支套件該歸哪一層、兩支該不該合、進步有沒有量得出來」。分層決定誰可以依賴誰(只准往下),判準決定一支套件該管多大範圍(合不合),指標決定驗收(做完有沒有變好)。來源是 FR-080 第 2 棒分析報告。
②層的名字「橫切」待決策者定案。 第 2 棒討論中用過「中介層」「橫切」兩個名字,決策者說先放著、之後再定。本頁一律用橫切,定案後全頁一次改名。層的內容與判準不受名字影響。
這頁是 FR-080 第 2 棒分析報告 第 1、2 節的長期規範版:分析報告記的是「當時怎麼判的、討論中推翻了什麼」,這頁記的是「以後照這個判」。 與 套件分類地圖 的分工:那頁把套件分成十族(誰會裝、壞了會有什麼後果),是給「要加新能力時該照哪個先例抄」用的;這頁分五層(誰可以依賴誰),是給「這支能不能用那支」用的。族與層是兩個獨立的切法,同一族的套件可能分在不同層。
「層」決定的是誰可以依賴誰。判一支套件屬於哪一層,就問兩個問題:拔掉它會怎樣、換一個完全不同的產品還要不要它。
| 層 | 業界叫法 | 它是什麼 | 壞了會怎樣 | 換一個產品 |
|---|---|---|---|---|
| ① 基座 | 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 更底層,不屬於②。
②不是「一包公共工具箱」,是一排各自獨立、各管一件事的套件。它們做的動作只有三種:
| 動作 | 做什麼 | 實例 |
|---|---|---|
| 擋下 | 不符合就攔住,請求根本進不到功能裡 | 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。
整併後 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。其餘依產品需要選裝。
系統全停 → ①② 只少一個功能 → ③④
要 → ③ 不要 → ④
③ 它站在哪裡——站在功能外面的路上、功能不會呼叫它 → ②橫切,核心寫成請求攔截;功能會主動呼叫它做事 → ③④。
判完層之後形狀就定了,剩下只有一條硬規則:依賴只准往下。
「套件要不要合併」與「要不要分開部署」是兩件事,用兩組不同的問題判,不可以互相借用。講合併/不動時是在講一支套件該管多大範圍;講同一個行程/分開部署時是在講部署形態。
滿足下面任一條就傾向合併:
| 問題 | 怎麼判 |
|---|---|
| **資料是不是同生共死? | 兩邊的表會想建外鍵、同一筆交易裡會同時寫兩邊、少了一邊另一邊就沒意義 → 合。這來自 FR-069 訂的規則:想建外鍵就代表這兩張表該住在同一支套件裡。只存對方 id 但不建外鍵的那種弱關聯不算。** |
| 換一個產品會不會被一起拿走? | 一個跟稽核無關的新產品要裝 A,就一定也要裝 B(或反過來)→ 合。各自能單獨用 → 不合,即使功能看起來同一類 |
| 是不是同一個人在養?(輔助條件) | 從來沒有各自演化過、每次都同一批改版、改一邊必改另一邊 → 支持合。前兩條都不成立時,光憑這一條不能合 |
反過來,只要有下面任一種情況就不合併,即使上面三條都很像:
| 不合的情況 | 說明 |
|---|---|
| 對方是很多人都依賴的供應者 | 合了之後會害一堆本來不依賴它的套件被迫依賴它 |
| 合了會變成雜物袋 | 套件開機時要做一堆不相干的事 |
| 兩支不在同一層 | 橫切層與功能層不混,層不同就不合 |
「跨層不合」這條是 FR-080 討論中新增的,因為犯過兩次同型的錯:一次判「log 併進 system-core」(log 是②橫切、config 與 menu 是③功能);一次差點拿「轉發可以失敗」這個部署理由來當合併理由。
判「該管多大範圍」跟判「要不要分開跑」是兩把尺,不可以互相借用。
| 問題 | 怎麼判 |
|---|---|
| 資料交換能不能全部改成網路呼叫? | 它跟主專案之間的資料關聯要能全部改成「網路呼叫 + 只存對方 id 的弱關聯」。有跨界外鍵就不行,得先改掉 |
| 有沒有獨立的生命週期理由? | 不同的擴縮型態(吃 CPU/吃 I/O/要跑很久)、不同的發版節奏、不同的安全邊界、不同的操作者 |
| 分開之後哪些查詢會斷? | 主專案有哪些「一次撈很多張表」的查詢會斷掉?能不能改成呼叫 API 之後在程式裡拼起來,或另外做一份專門給查詢用的資料表? |
沒有第二條理由(獨立的生命週期)就不分開部署。 微服務的成本(網路、資料一致性、部署、監控)要有理由才值得付。FR-080 的結論是這一輪只做插件,微服務降級為以後再看的路線圖。
四個可以量的指標,由 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 棒手寫的數字對不上,是「兩邊量的東西不一樣」不是「算錯」,所以兩種讀法都留著。
IEvidenceSink/IAgentDirectory、participant 的 IProjectDirectory/IAuditLogger、task-platform 的 IProjectRoleGuard/IIdentityGuard。這 6 個逐一實查確認套件源碼裡一次都沒用到——它們的轉接器欄位寫成 Optional[Any],連型別標註都沒用上那個抽象類別,所以宣告了等於沒宣告。16 是實況,10 是它的子集。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 裡,接線檔增減時要同步改。