狀態:分析完成,待決策者拍板|建立日期:2026-08-30|對應卡:CM-1446(FR-069.9) 母案:FR-069(design.md §7「疆界盤點案」)|純分析案,未動任何程式碼 本文為第四階段(P6/P9–P13)與第三階段(老套件補殼)的開工門檻——地圖未拍板前,該兩階段不排棒。
| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-08-30 | 初版:六組平行盤查+首腦交叉驗證,產出去向表與 D10–D16 待拍板清單 | CM-1446 |
盤了 26 支套件+15 個主專案候選模組,結論是「合併 3 組、退役 3 支、下架評估 1 支、其餘獨立補殼」,套件數從 26 收斂到約 15–17 支。
五組候選疆界的驗證結果只有兩組成立,這正是先盤點再動工的價值——若照原假設直接開工,有三組會白做:
| 原假設 | 驗證結果 |
|---|---|
| ① 檔案疆界(file-upload+resource-store) | ❌ 假設錯。兩支零耦合、領域不同;真相是 resource-store 整支死碼該退役 |
| ② 問卷疆界(survey+task_survey) | ✅ 成立且證據最硬(跨半 DB 外鍵) |
| ③ 檢測疆界(device+detection_*) | 見 §3 ③ |
| ④ OSCAL 疆界 | ⚠️ 部分成立:oscal 應用層併入 v2,但 module_frame 不併;且「三角」的第三邊抓錯了 |
| ⑤ 追蹤疆界(issue+bulletin+feedback) | ❌ 證偽。互相 import=0、整合範式相反、租戶模型相反 |
兩項退役查證:jedi-oscal v1 → 可退役;jedi-log → 不可整支退役,但應拆兩半(一半活著、一半是「載入即炸」的不可用碼)。
本案順帶挖出四個 P0 級問題(詳 §5),其中兩個是會炸的地雷、兩個是出貨污染。
照 D9(jedi-iam 案)的同一套五判準,六組平行盤查後由本案彙整者逐項交叉驗證(不採信分組自報,關鍵結論一律親跑指令覆核):
| # | 判準 | 判讀方式 |
|---|---|---|
| ① | 實際耦合 | 跨套件 import 具體到符號+穿透層級(碰 app/domain 介面=淺;碰 infra/ORM model=深,深穿透=「一個能力被切成兩個目錄」的鐵證) |
| ② | 發版歷史 | 各支有無獨立的發版節奏 |
| ③ | 主專案膠水 | A 產品特定(合理留宿主)/B 通用流程(任何產品都要重抄=切錯線的稅)/C 純重複 |
| ④ | 死碼盤點 | 零呼叫功能鏈、零筆資料的表 |
| ⑤ | pyproject 依賴健檢 | 虛掛依賴、測試依賴錯放主依賴 |
pg_stat_user_tables.n_live_tup 對分割表母表一律回報 0。本案彙整者第一輪即被騙——pg_stat 說 api_logs/system_logs 是 0 列,實際 count(*) 是 102,205 與 615,122 列。若照 pg_stat 判讀,會得出「jedi-log 是死碼可退役」的完全相反結論。零筆判定一律用 count(*)。public 58/oscal 58/compliance 46/survey 15/config 13 張表)。用 public.<table> 查 survey 或 detection 的表會得到「relation does not exist」而誤判成死碼。jedi-log 底下沒有任何 jedi_log 模組,實際是 jedi_api_log 與 jedi_system_log 兩個 package。用 jedi_log 掃會零命中而做出假的退役結論。全 26 支中只有這一支有此落差。monorepo 有兩支 CVE 全掃 commit(0bdadde、c6188f7)各一次動 18 支套件的 pyproject。這不是「同批升版=獨立生命從未兌現」的證據,只是資安批次補丁——它對 18 支一視同仁,若當成親緣證據會推出「全部 18 支都該合併」的荒謬結論。多數套件顯示「最後異動 2026-07-25」即由此造成的假象。判準②一律只看 feature/fix 驅動的 release。
圖例:🟢 獨立補殼(第三階段照常)|🔵 併入某疆界|🔴 退役/下架|⚪ 已定案(D9,不在本案範圍)
| # | 套件 | 規模 | 主專案 import | 去向 | 依據摘要 |
|---|---|---|---|---|---|
| 1 | jedi-common | 105檔/5,104行 | 821 | 🟢 獨立(地基) | 全生態地基。第二階段續瘦身;system_logs canonical 寫入者在此 |
| 2 | jedi-auth | 164檔/10,783行 | 181 | ⚪ → jedi-iam | D9 已拍板 |
| 3 | jedi-login | 90檔/4,361行 | 26 | ⚪ → jedi-iam | D9(→auth 30 處 import,深穿透 infra) |
| 4 | jedi-mfa | 53檔/2,283行 | 10 | ⚪ → jedi-iam | D9 |
| 5 | jedi-captcha | 35檔/1,922行 | 5 | ⚪ → jedi-iam(降 turnstile 模組) | D9 |
| 6 | jedi-department | 41檔/2,164行 | 0 | ⚪ 🔴 下架 | D9 已判(與 org_units 重複、無多租戶) |
| 7 | jedi-oscal(v1) | 452檔/24,672行 | 0(12 處全註解) | 🔴 退役 | 四 repo 零可執行引用、無套件依賴、無 DB 表、poetry.lock 已無其蹤 → §3 ④ |
| 8 | jedi-oscal-v2 | 445檔/26,321行 | 165 | 🔵 OSCAL 疆界核心(吸收 oscal 應用層通用件) | → §3 ④ |
| 9 | jedi-survey | 98檔/10,928行 | 51 | 🔵 問卷疆界核心(吸收 task_survey 作答層) | → §3 ② |
| 10 | jedi-flow-engine | 133檔/10,201行 | 163 | 🟢 獨立補殼 | 主專案第 4 熱依賴;與 flow_control 的關係屬終局選項 |
| 11 | jedi-file-upload | 69檔/3,948行 | 37 | 🟢 獨立補殼(升級為檔案疆界) | 有真實第二消費者(jedi-issue pin 並用 5 符號)+有獨立發版節奏 → §3 ① |
| 12 | jedi-resource-store | 37檔/1,594行 | 0 | 🔴 退役 | 程式碼零 import、public.resources 表四環境皆不存在、路由 2026-02 已刪 → §3 ① |
| 13 | jedi-issue | 149檔/5,596行 | 20 | 🟢 獨立(但58% 死碼待清) | 追蹤疆界證偽 → §3 ⑤ |
| 14 | jedi-bulletin | 42檔/1,338行 | 3 | 🔴 下架評估(比照 department) | 套件 408 行 vs 主專案重寫 406 行;DEV 1 列/POC 0 列 → §3 ⑤ |
| 15 | jedi-log | 77檔/3,473行 | 8 | 🔵 拆兩半:jedi_api_log 併入 log 疆界;jedi_system_log 🔴 刪除 |
api_log 活著(102,205 列);system_log import 即炸 → §3 ⑤ |
| 16 | jedi-log-forwarding | 37檔/3,281行 | 4 | 🔵 log 疆界 | 與 api_log 職責互補(一個管落地、一個管轉發) |
| 17 | jedi-device | 41檔/2,882行 | 21 | 見 §3 ③ | — |
| 18 | jedi-notification | 44檔/3,745行 | 12 | 🟢 獨立補殼 | D9 已驗證「維持獨立」(12 消費點多與身分無關) |
| 19 | jedi-system-config | 46檔/3,625行 | 28 | 見 §3 ⑥ | — |
| 20 | jedi-system-menu | 45檔/2,446行 | 4 | 見 §3 ⑥ | — |
| 21 | jedi-project | 26檔/399行 | 75 | 見 §3 ⑥ | ⚠️ 399 行卻被引 75 次、21 處穿透 infra |
| 22 | jedi-information-system | 29檔/903行 | 9 | 見 §3 ⑥ | FR-032 G 抽取範本 |
| 23 | jedi-ai-bot | 15檔/1,043行 | 2 | 🟢 已具 D6/D7 契約 | 第一階段 P1 新抽 |
| 24 | jedi-integrity | 29檔/4,721行 | 10 | 🟢 已具 D6/D7 契約 | 第一階段 P2 |
| 25 | jedi-remote-agent | 70檔/3,757行 | 28 | 🟢 已具 D6/D7 契約 | 第一階段 P4 |
| 26 | jedi-license-runtime | 49檔/4,266行 | 9 | 🟢 已具 D6/D7 契約 | 第一階段 P5 |
結論:不合併。jedi-resource-store 退役;jedi-file-upload 獨立保留並吸收主專案通用膠水。信心度:高。
原假設把兩者放同組,推測是基於名字都像在講檔案。實測全錯:
| 面向 | jedi-file-upload | jedi-resource-store |
|---|---|---|
| 管什麼 | binary 檔案實體+多後端儲存(local/minio/seaweedfs) | 一張 link TEXT 欄——書籤/連結庫,帶 publish/hidden/start_date 等內容發佈語意 |
| 有沒有檔案 | 有,2,672 筆實體檔 | 沒有任何檔案,只有 URL 字串 |
| 資料表 | upload_files(2,672 列,三條 FK 被引用) |
public.resources——四環境全部不存在,連出貨基線 02-schema.sql 都沒有 |
| 外部依賴 | jedi-common+minio SDK(不碰任何業務套件) | jedi-common+jedi-auth 深穿透(infra/models/resource.py:11 直吃 User ORM 建 FK) |
| 真正的疆界歸屬 | 檔案 | 若真要留,是內容發佈/身分族,不是檔案族 |
雙向 import 皆為 0,連 generate_uuid() 都是各自複製(全庫 17 支套件各抄一份,canonical 早在 jedi_common/utils/common_utils.py:7)。
pyproject.toml:67 那一行 pin。無任何套件依賴它(poetry.lock 內零反向依賴)。to_regclass('public.resources') 皆為 NULL。api/resource/ 於 commit 5c5de8d7(2026-02-14)移除。docs/release_notes/v1.14.0.md:152 記載「CM-1143 遺留模組整鏈退役(cruise-project/resource/report+jedi-resource-store):Not started」。⚠️ 一項本案內部的判定更正(記錄以免後人重蹈):彙整者中途曾誤判「
oscal.resources0 列就是 resource-store 的表」,經①組實查推翻並由彙整者覆核確認分組正確、彙整者錯。全庫有兩處宣告__tablename__ = "resources":resource-store 的未宣告 schema(解析到public),jedi-oscal-v2 的明寫{"schema": "oscal"},欄位集合幾乎無交集(後者是citation/rlinks/base64/metadata_id,即 OSCALback-matter.resources[])。同名不同物。 修正後的結論更強:不是「表 0 列」而是「表從未存在」⇒ 退役是純粹的「刪 pin+刪目錄」,零資料風險,且不需把表的處置交給 OSCAL 疆界。 教訓:判斷表歸屬要看欄位與 schema 宣告,不能看表名。
退役的附帶收益:resource-store 宣告 jedi-auth>=0.1.5 且 3 處 import 它。提前移除=第 2.5 階段 jedi-iam 合併時少一個要跟著改的下游。
建議做法:併入既有 CM-1143 退役案,不另開卡(避免兩張卡管同一件事);但在 CM-1143 補一條註記——本案已完成套件端四項查證,該支可無條件、零風險移除,不必等 cruise-project/report 的分析結果。若 CM-1143 長期不啟動,這行 pin 可在 FR-069 任一棒順手帶走。
jedi-issue/pyproject.toml:19 pin 它並實際使用 5 個符號(local_issue_attachment.py:4-8)。remote_agent_adapter.py 273 行等,留主專案正確)/B 通用流程 ~280(該上移)/C 純重複 14 處(file_upload_service=... 在 8 個 container 逐字重複)。B 類該上移的證據最強:ManagedFileUploadService 的 docstring 自承「取代 jedi_file_upload 原本從 env vars 讀取的邏輯」——套件的設定來源模型主專案完全沒用、整支覆寫;而「依檔案自記的 storage_type 挑對後端讀回」這 117 行核心語意套件根本沒有。疆界切在錯的地方:套件保留了它做不好的(設定解析),卻沒承接它該做的(多後端讀取路由)。 旁證:jedi-issue 已在為同一缺口付稅(local_issue_attachment.py:28 必須 file_upload_service or FileUploadService(...),主專案得顯式注射才不會退化成 local)。
P0-① 架構債查證結果:已修完。api/uploadfile/app/uploadfile 等四目錄皆不存在,且 test/test_module_boundaries.py:92-96 有守衛測試焊死舊拼法不得復活。唯一殘留是 docs/api/uploadfile/ 文件目錄名(純文件,可順手更名)。
結論:task_survey 併入 jedi-survey,P11 從「抽新套件」改寫為「合併」。信心度:高。
task_survey 不是「疊在 survey 上的一層」,而是 jedi-survey 被切掉的下半身。四項互相獨立的證據一致指向同一結論:
證據 1:跨半身的 DB 實體外鍵(最硬,單獨即足以定案)
live DB 實查 pg_constraint,兩條真實 FK 橫跨「套件的表」與「主專案的表」:
survey.question_answers → survey.survey_questions
survey.question_answer_history_details → survey.survey_questions
套件邊界不可能靠 DB 外鍵縫合。 若照原 P11 抽成獨立的 jedi-task-survey,這兩條就變成「套件 A 的表對套件 B 的表下外鍵」——安裝順序、migration 順序、卸載全部綁死,本質上就不是兩個套件。而 jedi-survey 全庫零 migration(.sql 只有測試用 initdb),survey.surveys 的 migration 全在主專案 scripts/sql/——套件的表由宿主維護,這就是同疆界的定義。
證據 2:DB schema 的切法是刻意設計(實查 migration 原始碼確認)
scripts/sql/migrate_schemas.sql(2026-03-21)同一支腳本內做了分流判斷:
-- 2. jedi-survey 核心表搬遷 → surveys / survey_pages / survey_questions ... → survey schema
-- 4. answer 相關表搬遷 → task_surveys / question_answers / ... → survey schema
-- 5. job_evidences 搬至 compliance schema → compliance schema
設計者當下對每張表逐一判斷,把 job_evidences 明確送去 compliance,卻把五張作答表**與問卷核心表並列在同一節、放進同一個 survey schema`。當時心中的疆界就是「問卷=設計+作答」。
證據 3:穿透方向單一——上半身乾淨、下半身帶著所有耦合
task_survey → jedi_survey 共 13 條 import,最深的 5 條直取 ORM model 與 RepoImpl(infra/task_survey/models/task_survey.py:11 吃 Survey、question_answer.py:7 吃 SurveyQuestion、DI 直取 SurveyPageRepoImpl/SurveyQuestionRepoImpl)。反向 jedi-survey 對 task 概念零認知,只依賴 jedi_common,是乾淨葉節點。
這正是「被切一刀」的形狀:若是兩個真疆界,耦合應雙向或上半身有明確 port;現況是上半身根本不知道下半身存在,卻被下半身用 ORM 與 FK 直接抓住。
證據 4:套件公開 API 已被宿主業務語意侵入
jedi-survey/.../survey_snapshot_domain_service.py:36 的公開簽名是 create_snapshot(source_survey_uid, project_uid),:86 用 f"__snapshots__{project_uid}" 造資料夾名。project_uid 是主專案概念,早已寫進套件公開 API 與資料落地格式——所謂「乾淨的上游」在源碼層面不成立。
膠水量化(4,514 行):A 產品特定僅 14%、B 通用流程 61%。 作答的 upsert 三路徑、歷史版本與還原、作答狀態機(未填→編輯→待審→補充→完成)、多人即時共編 socket——全部零稽核語意,任何「問卷要指派給人作答」的產品都得重抄。這直接推翻「作答層是產品特定所以該留主專案」的可能結論。
發版歷史:排除 CVE 批掃後 2026 年 8 支實質 commit,5/8 由作答層拉動(FR-049/049.1 顯示條件 4 支+snapshot 機制 1 支)。FR-049.1 是最強一支——一個功能跨兩個 repo 分兩處實作,且消費端得對自己剛加的欄位寫防禦:
# app/task_survey/service/question_answer_service.py:41
dc = getattr(q, "display_condition", None) # 對自家剛加的欄位寫 getattr 防禦被排除的選項——維持原 P11 抽獨立套件:技術上不可行。 要斷開必須拆掉兩條 DB 實體 FK、改成應用層維護參照完整性,同時把 TaskSurvey.survey 的 association_proxy(survey_uid/survey_name/survey_version_no,被 DTO 與通知信直接使用)全改批次 enrich。代價是拆掉 DB 幫你保證的完整性,換來兩個永遠要對版的套件——而對版壓力已被 FR-049.1 實證。
反悔條件:出現「只要問卷編輯器、不要作答」(如純表單設計器產品)的真實消費者。目前零外部消費者,不成立。
結論:device 不併入檢測疆界。P9 內容物去掉 device、加上散在 common/ 的檢測領域碼。形態=插件,不服務化。信心度:高。
待驗假設「設備管理本就是為檢測服務」被硬證據推翻:
證據 1:程式碼層零耦合。 掃 detection_tools(99 檔)+detection_execution(30 檔)全部 157 檔,對 jedi_device 的 import =0。反向 jedi-device 也不認得任何檢測概念。唯一含 "device" 字樣的 6 處全是同名不同義(agent.device_fingerprint 屬 remote-agent、network_device 是分類 slug)。
證據 2:資料層鐵證(本組最硬的一項)。 實查 job_execution_devices 3,012 列的任務型態分佈:
general 3,009 列 | survey 3 列 | detection_tool 0 列
(對照:job_executions 有 detection_tool 型 21 列)
檢測型任務從未綁定過任何 device。 檢測的掃描目標走 JSONB scan_targets.hosts 手打字串(裸 IP/CIDR/repo key),與 devices 表無 FK、無任何 code 路徑相連。
證據 3:device 的真實消費者全在資產域。 21 處 import 分佈於 system_asset_snapshot(8)、ssp_import_template_app_service(7)、job_execution_device_mapping_service(6)、ssp_inventory_items/ssp_resources_context(各 2)——清一色是資產盤點/SSP/範本匯入,零檢測。發版歷史也一致:jedi-device 近兩次真實開發分別由資產盤點(FR-032 F)與主專案審計欄位規範驅動,零筆由檢測需求驅動。
證據 4:商業模型也不同疆界。 tenant_licenses 實查——檢測三個 module key(plugin/detection-profile/remote-agent-manage)各只有 6/25 租戶開通,而 device 是 25/25 全開。檢測是加購模組、device 是標配。
| 三問 | 實查回答 |
|---|---|
| 壞了宿主整停還是可降級? | 可降級。檢測是 job 的一種型態(21 列 vs general 11,012 列),掛了只是那些工單卡住,稽核/SSP/問卷/證據照跑 |
| 需要自己的資料主權嗎? | 不需要,且已證明不可能。檢測結果終點是宿主的 compliance.job_evidences 證據池與宿主 Minio;8 張檢測表有 6 張掛宿主 RLS |
| 同一套實例服務多產品嗎? | 不會。多實例分擔的角色已由 jedi-remote-agent 承擔(真正跑掃描的是客戶端 agent),雲端側只是「派工+收單+轉證據」的編排層 |
加碼證據:DetectionOrchestrationService 對宿主 workflow 有 9 處 assert_project_participant(...)+1 處 complete_job()。服務化後這些全變成回打宿主的網路 RPC——把「同一 transaction 內的授權+狀態轉換」拆成跨網路兩段,換來分散式一致性問題,收益為零。
⇒ 建議把 design.md §7 P9 那句「開工前先過服務化評估」改成結論式敘述,省掉開工前那一輪。
common/ 內的檢測領域碼約 1,567 行:detection_tools_error_code.py(209)、detection_assignment_params.py(173)、detection_secret_params.py(154)、detection_profile_archive.py(348)、scan_target_spec.py(307)、profile_extractor/(188) 等。抽套件不搬這些,等於「套件在主專案裡留了一半領域碼」。app/flow_control/service/job_service.py 全檔 1,144 行中約 785 行(69%)散在 17 個方法裡處理檢測(單 _replace_detection_tool_agent_assignments 就 169 行)。「import 名不變就不用改」對那 7 條成立,但這 785 行的去向(搬進套件+IJobBinding port,或留 grc)是 P9 最大的單一設計題,design.md 目前沒提。device 自身的處置:改列入資產域評估(與 module_frame 的 inventory 段、oscal.ssp_inventory_items 的 27 列 ref-type: device soft-ref 一起看);套件本身走第三階段輕量升級即可(把 api/device/ 196 行收回套件、清 7 支錯誤依賴、修 devive_*.py 檔名 typo),不必等疆界拍板。
結論:oscal 應用層的通用件併入 jedi-oscal-v2;module_frame 不併入、另立候選;P13 真正的攔路虎是 flow_control 不是 module_frame。信心度:中高。
耦合實況(實查,與 design.md 的敘述不符):主專案對 jedi_oscal_v2 的 import 按模組分佈——
oscal 77 | flow_control 62 | module_frame 18 | project 8 | associations 0
flow_control 的 62 條是 module_frame 的 3.4 倍。 P13 若只處理 module_frame,做完仍有 62 條橫跨 flow_control ↔︎ v2,疆界沒收乾淨。
穿透深度是本組最重要的發現:77 條 import 直取 jedi_oscal_v2.infra.repository.*RepoImpl(主專案 app service 自己 new 套件的 RepoImpl,繞過套件 app service 層)。最嚴重三處:assessment_result_app_service.py:36-64 一口氣拉 13 支、assessment_plan_app_service.py:37-55 拉 9 支、poam_app_service.py:24-30 拉 7 支。而 v2 自己的 AssessmentPlanService/PoamService/AssessmentResultService 就擺在那沒人用。
⇒ P13 真正的技術債是「v2 的 app service 層被判定為不夠用而遭繞過」,不是「應用層該不該進套件」。 建議 P13 開工前先派一棒查證:v2 service 的粒度是否為單文件 CRUD,而主專案要的是跨 AP/AR/POA&M/catalog 的交易編排?若是,正確做法是在 v2 內補一層編排 service,讓主專案從 13 支 RepoImpl 降為 1–2 支 service,而不是把 1,000 行的應用層整包搬進套件。
依賴方向:design.md 的建議要反過來。 design.md §7 建議「grc → oscal 單向」,實查現況是:
oscal → flow_control 11 條 | flow_control → oscal 1 條
主導方向是 oscal → flow_control,與建議相反(要達成 design 的目標得反轉 11 條而非 1 條)。且其中 6 條關於 ssp_reference_document*,兩條是 DDD 違規:
infra/oscal/repository/ssp_catalog_title_query.py:29 — from app.flow_control.service.ao_derivation import AO_PART_NAME(infra 層 import app 層)infra/oscal/clone/ssp_versioning_cloner_impl.py:19-20 — 直接 import infra.flow_control.model.ssp_reference_document{,_mapping} 兩個 ORM model⇒ 建議改為「oscal → flow_control 單向」(oscal 是被流程消費的資料層、flow_control 是流程編排層,讓資料層依賴流程層是反的)。前置決策:先拍板 ssp_reference_document* 兩張表的歸屬——它們現在住 flow_control,卻被 oscal 的 cloner 直接操作。這比 design.md 寫的「拍板依賴方向」更具體可執行。
module_frame 不併入 v2 的三個理由:① 概念不屬 OSCAL 標準(OSCAL 規範裡沒有 module frame);② 自有兩張表在 compliance schema(module_frames 156 列/module_frames_trans 110 列),不在 oscal schema;③ 依賴方向是 oscal → module_frame(15:2)——module_frame 不是 oscal 的下游,是上游的範本供應者。但它對 flow_engine(4)/participant(2) 也有依賴,獨立成套件成本不低 ⇒ 建議 P13 先不動 module_frame,標為「第二支候選、第四階段後期評估」。
| 證據 | 結果 |
|---|---|
| BE 真 import | 0(grep -rnE "^\s*(from|import)\s+jedi_oscal[^_]" 回 0 筆);字串命中 12 處全是註解,內容一致為「FR-038 2A: jedi_oscal imports removed」 |
| FE/test repo | 0 可執行引用(命中全在 changelog、對話紀錄、註解) |
| 其他套件依賴 | 0(全 26 支 pyproject 無人宣告 v1) |
pyproject.toml |
無 jedi-oscal pin,只有 jedi-oscal-v2==2.2.3 |
poetry.lock |
v1 不在 lock 內 |
| DB 表 | v1-only 的 27 個表名在 DEV/基線庫一個都不存在 |
| 最後異動 | 2026-06-18 後靜止 |
v1/v2 不共存的機制已查實:兩者共用同一個 jedi_common 的 Base,且有 13 個同名表同在 oscal schema——同一 MetaData 註冊同名表即 InvalidRequestError。memory 記載的「全切被強制」是真的,機制是 MetaData 撞名而非人為約定。
附帶收益:v1 宣告了 pymupdf(AGPL,即 2026-08 授權盤點的唯一紅燈 → CM-1235),v2 已不依賴它——退役可順帶消掉一個 AGPL 依賴來源。
建議做法:Nexus 標 deprecated + monorepo 目錄歸檔(不刪 git 歷史),採「先標 deprecated、觀察一個版本週期再歸檔」而非直接刪(唯一理論缺口是未查 Nexus 是否有本地四 repo 以外的下載者)。主專案端另需 poetry install --sync 清 .venv 殘留(見 §5 P0-3)。
FR-038 2B 查證結果(順帶回答):路由面大致完成——api/oscal/__init__.py live 61 條 vs 註解 6 條;api/module_frame/__init__.py live 43 vs 註解 2,且無任何 orphan route 模組。那 8 條註解掉的多是已判定的 DEAD-V1 清理(標記明確:「FE 無 caller」)。殘留集中在三支殭屍 service +其死測試(見 §5)。
結論:不存在「追蹤疆界」。三者各自獨立;唯一有數據支持的動作是 jedi-bulletin 下架評估。信心度:高。
「都在追蹤某種待辦/訊息」是命名層的錯覺,五個判準沒有任何一項支持合併:
| 反證 | 數據 |
|---|---|
| 互相 import = 0 | jedi-issue 與 jedi-bulletin 的 import 剖面無任何交集項(除所有套件都吃的 jedi_common) |
| 主專案整合範式相反 | bulletin 是繼承式擴充(3 個 import 全是 as BaseXxx 再 subclass);issue 是委派式呼叫(當黑盒,18 處 method call)。同疆界的東西不會需要兩種相反的整合方式 |
| 租戶模型相反 | jedi-issue 的 7 張表全部無 tenant_id、全部沒開 RLS;bulletin 與 feedback 則是 tenant-scoped + RLS 開啟。這與 D9 判 jedi-department 下架的判準是同一條 |
| 演化節奏無關聯 | 近 12 個月功能性 commit:bulletin 1 次、log 1 次、issue 5 次,且改動主題完全不相干 |
| 體積不同量級 | jedi-issue 純套件碼 2,879 行 vs jedi-bulletin 408 行——這是「都很小」的真相,不是同疆界 |
jedi-bulletin 下架評估的理由(比照 D9 的 jedi-department):套件純碼 408 行,而主專案為了用它重寫了 406 行(repo 97 vs 套件 71、domain service 47 vs 42、mapper 34 vs 26 等,實查逐檔比對)——被包裝的東西比包裝紙還小。真正在用的只有 3 個 base class,套件的 repo/domain service/mapper/query entity 全被主專案的版本取代。資料面:bulletins DEV 1 列、POC 0 列。套件化在此是淨負值:多一個發版對版負擔,換來 3 個 base class。
gitlab/github 整合線的判定:不是「與公告語意遠」的問題,而是它根本沒在跑——DEV 與 POC 雙環境的 ISSUE_INTEGRATE_CONFIG 皆 enable:false + 空憑證,feedback_issues 的 gitlab/github uid 全 NULL,_is_gitlab_enable()/_is_github_enable() 恆回 False。約 1,191 行套件碼從未執行過。
jedi-issue 死碼比例 58%(全案最高):project_member 全鏈 472 行(四 repo 零引用)+member 鏈約 700 行(唯一入口 /issue/get_members FE 零呼叫、members/role_members 表 DEV 與 POC 皆 0 列)+gitlab/github 1,191 行。2,879 行套件碼中約 1,663 行從未被執行過。
gitlab/github 是「已廢棄」還是「留給未來客戶」屬產品決策,程式碼判不了——列為待拍板(D14)。
這是本案最容易做錯的一題(三個陷阱在此交會:import 名落差、分割表 pg_stat 假 0、dist 名誤導)。
| 子套件 | 判定 | 證據 |
|---|---|---|
jedi_api_log |
✅ 活著,保留 | 主專案 8 條真實 import(common/middleware/app_mw.py:5-8 掛在 middleware 上,每個 request 都寫);api_logs DEV 102,205 列、POC 27,084 列 |
jedi_system_log |
🔴 刪除——不是死碼,是「載入即炸」的不可用碼 | 見下 |
jedi_system_log 的 P0 級問題(本案彙整者在 BE 的 .venv 內親自實測確認):
jedi-common 與 jedi-log 各自宣告了一個 __tablename__ = "system_logs" 的 model,且掛在同一個 Base.metadata 上。實測:
import jedi_common.logger.db_log.system_log.infra.models.system_log # OK
import jedi_system_log.infra.models.system_log
# → InvalidRequestError: Table 'system_logs' is already defined for this MetaData instance.任何人只要 import jedi_system_log,在 jedi-common 已載入的情況下(也就是必然)就會炸。 它不是「暫時沒人用」,是「用不了」。
且 615,122 列全部由 jedi-common 那支寫的:path/func_name/line_no 三欄只存在於 jedi-common 的 model,實查 615,122 列全部帶值 ⇒ 100% 由 jedi_common...DBLogHandler 寫入,jedi_system_log 對這張表寫過 0 列。jedi-log 那份 model 已漂移成 9 欄、不符 live schema 的 12 欄。
真正該收斂的是「jedi-common 的 db_log ↔︎ jedi-log 的 system_log」(同表兩份 model,其中一份還會讓另一份炸),而不是原本假設的「jedi-log ↔︎ log-forwarding」。合理的 log 疆界終局是三塊各歸其位:
| 角色 | 現況 | 建議 |
|---|---|---|
| 系統 log 落地 | jedi-common logger/db_log/(canonical,615k 列的實際寫入者)+ jedi-log jedi_system_log(死+炸) |
留 jedi-common,刪 jedi_system_log |
| API 存取 log 落地 | jedi-log jedi_api_log(活) |
保留 |
| log 轉發出去 | jedi-log-forwarding | 保留 |
三者互相在原始碼裡反覆提及對方的表(log-forwarding 的 plugin.py:106,111、common/code.py:14 都在講 system_logs)——一個套件反覆提到另一個套件的表,就是疆界訊號。建議在第三階段補殼時三者同組討論。
稽核埋點是否重疊? 實查 615,122 列中 [AUDIT: 開頭僅 1,353 列(0.22%)。common/util/audit_log.py(主專案已是 shim)→ jedi_common.utils.audit_log.audit()(決定訊息長相)→ DBLogHandler(決定怎麼落地)是一條鏈而非三份重複,jedi-common 檔頭已明說「互補而非重疊,故並存不合併」——實測同意。
結論:10 支確認獨立成立、1 支疆界待決(jedi-project)、0 支退役。 本組對「26→15」的收斂貢獻小,價值在排除假候選,避免第三階段花時間合併不該合的東西。
四個假候選全部不成立:
| 候選 | 判定 | 決定性證據 |
|---|---|---|
| system-config + system-menu | ❌ 語意相反 | 結構像雙胞胎,但 system_configs(20 列)有 tenant_id + RLS=t,是租戶機密設定;system_menus(74 列)無 tenant_id + RLS=f、(group,key) 全域唯一,是全站共用列舉字典(導覽選單另有 ui_routes 55 列)。合併會把租戶機密與全域字典放進同一租戶模型,是退步 |
| remote-agent + log-forwarding | ❌ 零交集 | log-forwarding 是進程內 logging handler 鏈(跑在雲端 BE 進程內);remote-agent 是機器身分與任務派送(mTLS/RS256 JWT/心跳)。「log-forwarding 是 agent 的一個能力」不成立 |
| integrity + license-runtime | ⚠️ 不合併,但有繞行要修 | integrity 的 ISignatureVerifier/IMachineFingerprint 實作由宿主從 jedi_license_runtime 注入——驗章能力繞了宿主一圈。合併能消掉這圈,但會把「檔案完整性」綁進「授權」。建議改把兩支純密碼學原語下沉 jedi-common,繞行自然消失 |
| notification 維持獨立 | ✅ D9 判斷成立 | 14 個消費點確認與身分無關;D9 說的「mfa 發信改 INotifier port」只需動 jedi-mfa/.../email_service.py 一個檔 |
🔴 真問題:jedi-project 是「被掏空的核心概念」(本組最重發現)
不是「太小該併掉」,而是一個疆界被切成兩半,主專案那半有 24,640 行:
| 面向 | 數據 |
|---|---|
| 套件體量 | 26 檔 / 399 行(無 tests、無 harness) |
| 主專案引用 | 75 處 / 53 檔(第 5 熱依賴) |
| infra 穿透 | 21 處(ORM model 15/mapper 4/repo_impl 2) |
| 主專案對應業務量 | flow_control 236 檔 / 24,640 行(套件的 62 倍) |
| 被其他套件依賴 | 0 |
| 兩份實體並存 | 套件 ProjectEntity 11 欄 vs 主專案 FlowControlProjectEntity 40+ 欄(docstring 自陳整合了 projects+extensions+OSCAL AP 計算欄位) |
| 表的歸屬 | compliance.projects 211 列,掛在它上面的 7 張 FK 表全在主專案,套件一張都沒有 |
穿透原因不是「套件 domain service 能力不足」,是疆界切錯——兩類用途都不是 domain service 能提供的:① JOIN 需要 ORM class 本身(13 處,例如 Project outerjoin ProjectExtension 再依 15 個衍生欄位排序,那需要套件認得主專案的表);② SQLAlchemy relationship() 需要 model class(4 處)。
建議:短期凍結、標記「疆界待決」、不補殼。399 行空殼補 D6/D7 五層插件殼是純浪費(殼比內容物大);而現在吸收回主專案會主動關掉 design.md §7 終局選項「project 平台化」的門——compliance.projects 與 ProjectStatus 是 jedi 生態的共同錨點。長期路徑與第二階段天然接軌:project_extensions 收進主表+JSONB 容器後,套件的 Project model 就長出主專案要的欄位,21 處穿透中至少 13 處的 JOIN 需求會自動消失。
此條與 D9 的 login→auth 形狀相同但結論相反:login 案是「兩支套件同疆界→合併」;project 案是「套件與宿主同疆界,但宿主那半的歸屬是商業決策」。差別在對造方是套件還是主專案。
jedi-flow-engine 未做完整判定:它有 40 處 infra 穿透、形狀與 jedi-project 同類,但套件本身有 5,465 行實體(非空殼),故不列同級問題。判它需要 flow-engine(12,765 行)+flow_control(24,640 行)+project 三方分析——建議另立一棒專做「流程疆界」,它與 jedi-project 是同一個結。
| 原棒次 | 改寫建議 | 理由 |
|---|---|---|
| P9 jedi-detection(detection_tools+detection_execution+device) | device 移出;加入 common/ 檢測領域碼約 1,567 行;形態改為結論式「插件,不做服務化評估」;新增設計決策題:job_service.py 785 行檢測碼的去向 |
device 與檢測零耦合、零資料關聯(§3 ③) |
| P11 jedi-task-survey(抽新套件) | 改寫為「問卷疆界合併」——task_survey 併入 jedi-survey | 跨半身 DB 外鍵使「抽成獨立套件」技術上不可行(§3 ②) |
| P13 oscal 三角拆解 | 三角的第三邊從 module_frame 改為 flow_control(62 條 vs 18 條);module_frame 降為「第二支候選、後期評估」;依賴方向建議反轉為 oscal → flow_control 單向;前置決策改為「拍板 ssp_reference_document* 兩表歸屬」;新增前置查證棒:v2 app service 為何被 77 條 RepoImpl 繞過 |
§3 ④ |
| P12 jedi-participant | 維持不變,仍建議先做 | 解核心環的鑰匙;且 P11 排在它之後可省一輪 port 設計 |
| P6/P10 | 維持不變 | 本案未發現反證 |
| 「併入四階段評估」的 feedback | 維持第四階段,但修正判準:design.md 說的「斷 2 條 system_config 線」實為空掛注入可直接刪(注入後從未呼叫);真正的阻礙是 jedi_auth.infra.models.User 的 infra 穿透 ⇒ 應排在第 2.5 階段 jedi-iam 之後 |
§3 ⑤ |
| — | 新增:「流程疆界」分析棒(flow-engine + flow_control + jedi-project 三方) | jedi-project 與 flow-engine 是同一個結(§3 ⑥) |
建議執行順序:P12(鑰匙)→ P11(問卷合併)→ P9(檢測)→ P10/P6 → P13(OSCAL,最重且需前置查證)。
不補殼的(避免「補完又扔」):jedi-resource-store(退役)、jedi-oscal v1(退役)、jedi_system_log(刪除)、jedi-bulletin(下架評估中)、jedi-project(疆界待決)。
建議首棒:jedi-information-system——903 行、9 個消費點、已有 IUserLookup dependency inversion 與守衛測試,是全庫依賴最乾淨的一支(唯一零虛掛依賴的老套件),補殼成本最低,適合驗證第三階段 SOP。
補殼順帶收益:老套件的虛掛依賴會被 standalone harness 自動抓出來(裝不全就跑不起來)——harness 就是虛掛依賴的照妖鏡,不需另開清理棒。實證:五支 P1–P5 新套件(皆有 harness)零虛掛,六支老套件全部有虛掛。
皆為本案分析副產品,均已由彙整者親自覆核。建議獨立開卡,不必等疆界拍板。
| # | 問題 | 證據 | 風險 |
|---|---|---|---|
| P0-1 | jedi_system_log 載入即炸 |
在 BE .venv 實測:先 import jedi-common 的 system_log(必然發生)後再 import jedi_system_log → InvalidRequestError: Table 'system_logs' is already defined |
上膛的槍。任何人「看到有這套件就拿來用」,第一行 import 就 500。且若有人加 extend_existing=True 硬繞,會得到一個缺 3 欄的 model 靜默覆蓋正確的那個 |
| P0-2 | **jedi-issue 漏宣告 jedi-common |
pyproject.toml 相依清單無 jedi-common,源碼卻 import 它 28 次** |
單獨 pip install jedi-issue 必炸。目前不暴露只因主專案剛好也裝了 jedi-common。這正是 FR-069「套件可獨立安裝」目標的直接反例,第三階段補殼首個會紅的 |
| P0-3 | .venv 殘留 jedi-oscal v1 |
.venv/.../jedi_oscal 與 jedi_oscal-0.0.23.dist-info 實體仍在,但**不在 poetry.lock(poetry update 未清) |
目前不炸(DI 走明文 allowlist 非萬用掃描),但它與 v2 有 13 個同名表同 schema 同 Base**——任何誤 import 立即 InvalidRequestError。建議 poetry install --sync |
| P0-4 | 6 張孤兒表隨 installer 出貨到每個客戶環境 | component_definitions+cd_capabilities/cd_components/cd_control_implementations/cd_implemented_requirements/cd_statements——全庫零 ORM 擁有者(grep __tablename__ 零命中)、六張全 0 列,只活在 scripts/init/02-schema.sql |
出貨污染。是 OSCAL Component Definition 的預留表,從未實作 |
| 類別 | 內容 |
|---|---|
| 死碼 | 三支殭屍 app service 595 行(oscal_audit_service.py 409/ssp_versioning_service.py 151/project_system_info_service.py 35,method 全回 None/{},外部 caller 皆 0)+ 1,236 行測試在測空殼(test_ssp_versioning_service.py 檔頭是模組層 pytest.skip,26 個 test 從未執行);task_survey_ref_items 全鏈 219 行(DB 0 列,概念已被 in-memory dict 取代);job_execution_device_mapping 全鏈 772 行(路由已註解、四 repo 零引用、DB 0 列,但 DI 仍 live-wired);device 監控子能力 21 行+2 表(零實作的預留:無 ORM、無 repo、無 service、無任何 INSERT 路徑);問卷答案 Excel 匯入鏈 342 行(FE 唯一呼叫點在 _archived/);jedi_common/utils/gen_comment.py 84 行(四 repo 零引用,且是全生態 openai 依賴的唯一理由——刪它可讓 26 支套件少扛一個 LLM SDK);jedi_common/session/database/base_model.py 19 行(design.md 已定案待刪) |
| 靜默失效 | SurveyPageDomainService DI 漂移——survey_containers.py:49-54 注入 survey_repo、question_answer_containers.py:50-54 漏注入,而套件側 verify_survey_exist() 寫的是無 else 的 if self.survey_repo: ⇒ 走後者取得的實例靜默不驗、回 None。目前未爆(該路徑只用 get_pages),屬上膛的槍 |
| 零呼叫者的完整實作 | archive_workflow_execution()(workflow_execution_service.py:1303)完整實作了 4 支歷史表歸檔,但全域零呼叫者(唯一非定義命中是 2026-04 對話裡被註解掉的一行);對應 hi_* 四張表全 0 列,而活表 workflow_executions 有 11,004 列。需 user 裁決:歸檔要接上(誰呼叫?專案結案時?)還是連表帶碼退役 |
| 依賴衛生 | 測試依賴錯放主依賴 20/26 支(非 D9 估的 5 支)——乾淨的只有 ai-bot/integrity/license-runtime/log-forwarding/remote-agent(皆有正確 dev group)+information-system(無測試)。主專案 pyproject.toml:113-118 已記載此問題會讓 --without dev 仍裝進 build 機 venv。虛掛依賴集中在六支老套件,最嚴重是 jedi-project 11 項全虛掛(399 行 CRUD 套件宣告了 pandas/pdfplumber/openpyxl/tqdm/ftfy,且 pytest 重複宣告兩次)。反向漏宣告:jedi-notification 用了 requests 未宣告、jedi-file-upload 用了 werkzeug/urllib3 未宣告(standalone harness 會炸) |
| 測試腐化 | jedi-file-upload tests/unittest 63 passed / 16 errors(測試停留在 factory 重構前的舊 signature,且 patch 一個模組層不存在的符號);tests/architecture 5 檔 978 行從未執行(缺 pytestarch 宣告,collect 即失敗);jedi-resource-store 931 行測試全綠地測著一個零消費者的殭屍——「綠燈不代表活著」的樣本 |
| 命名混淆 | FileUploadService vs UploadFileService 並存(僅語序不同、職責無法從名字分辨,route 同時注入兩支);jedi_system_log/app/service/system_menu_service.py 檔名與內容不符(class 是 SystemLogService,是從 jedi-system-menu 複製後沒改檔名的殘跡);generate_uuid() 全庫 17 支套件各抄一份(canonical 早在 jedi_common/utils/common_utils.py:7) |
| 潛在 500 | ai_dashboard API registry 兩筆 device 條目(api_registry.py:244,254)指向 associations_containers.py 內不存在的 provider;因 lambda 延遲求值,app 起得來,但 AI 儀表板選到這兩支就炸 |
| RLS 缺口(建議併入既有 CM-1445) | jedi-issue 7 張表全無 tenant_id 且無 RLS(labels 35 列全租戶共享)——與 CM-1445 追蹤的三張表性質不同(那三張是「有欄位沒開 RLS」,這七張是「連欄位都沒有」);compliance.job_execution_devices 是租戶資料關聯表卻無 RLS;upload_files 無 RLS(靠 path/storage_type 間接隔離,程式碼有明文「繞 RLS 借同型設定」的 fallback) |
| 版控污染 | jedi-device 把 dist/(8 個舊版 whl)、coverage_unittest/、testarch_report/ 進了版控;jedi-device/README.md 8 行且「目錄結構」段是空的 code block(D7-② 接入 README 標配完全缺席) |
| 文件與事實不符 | docs/spec-site/current/user-manual/developer-setup-guide.md:338 稱 jedi-resource-store 為「資源儲存抽象」(實為連結庫,且表從未存在);tests/DEPENDENCIES.md:23,308 描述其 resources 表與 fixture 寫法;docs/api/uploadfile/ 目錄名殘留舊拼法(程式碼已於 P0-① 收斂為 upload_file) |
編號接續 design.md §3 的 D1–D9。拍板後才改寫第三、四階段棒次表。
| # | 決策點 | 建議 | 信心 | 若反悔的條件 |
|---|---|---|---|---|
| D10 | 問卷疆界合併:task_survey 併入 jedi-survey,P11 從「抽新套件」改為「合併」 | 同意合併。跨半身 DB 外鍵使原方案技術上不可行;A 類產品特定僅 14% | 高 | 出現「只要問卷編輯器、不要作答」的真實消費者(如純表單設計器產品) |
| D11 | 檢測疆界:device 不併入;P9 改為「detection_* + common/ 檢測碼」;形態=插件 | 同意。零 import、零資料關聯(檢測型任務綁 device 0 列)、商業模型也不同(device 25/25 租戶標配 vs 檢測 6/25 加購) | 高 | 未來檢測改為「掃描目標必須來自資產主檔」的產品設計 |
| D12 | OSCAL 疆界:oscal 應用層通用件併入 v2;module_frame 另立;P13 第三邊改為 flow_control;依賴方向反轉為 oscal → flow_control 單向 | 同意,但 P13 開工前先派一棒查證「v2 app service 為何被 77 條 RepoImpl 繞過」——答案決定是「搬應用層進套件」還是「在 v2 內補編排層」 | 中高 | 查證結果顯示 v2 service 粒度其實夠用(則繞過屬歷史積習,該修的是主專案) |
| D13 | 三支退役/刪除:jedi-oscal v1(退役)、jedi-resource-store(併入 CM-1143)、jedi_system_log(刪除) |
同意三支。證據鏈皆齊備,其中 jedi_system_log 建議優先處理(是會炸的碼,不只是死碼) |
高 | v1/resource-store:Nexus 出現四 repo 以外的下載者(未查證項,故建議「先標 deprecated、觀察一版」而非直接刪) |
| D14 | jedi-bulletin 下架評估(比照 D9 的 jedi-department) | 建議下架,但屬產品決策:套件 408 行 vs 主專案重寫 406 行、DEV 1 列 POC 0 列 | 中 | 公告功能列入未來產品路線(則該做的是把主專案那 406 行收回套件,而非下架) |
| D15 | jedi-project 疆界待決:短期凍結不補殼,長期併入「專案疆界」而非吸收回主專案 | 同意凍結。現在吸收會關掉 §7 終局選項的門;且第二階段 ext 收斂會自動消掉 13 處穿透 | 中高 | 商業決策明確放棄平台化(則改走「吸收回主專案、jedi-project 退役」,399 行搬回去是一天的工) |
| D16 | jedi-issue 死碼去留(58% 從未執行):gitlab/github 整合 1,191 行 + member/project_member 鏈約 1,172 行 | 需 user 拍板——「已廢棄」還是「留給未來客戶」是產品決策,程式碼判不了。技術面已證明:雙環境 enable:false+空憑證+零筆資料 |
— | — |
job_service.py 785 行檢測碼「搬 vs 留」未拆解——只做行數盤點,未逐方法判斷哪些依賴宿主 job entity。這是 P9 開工前該做的第二層分析。archive_workflow_execution 零呼叫者是靜態 grep 結論——若有透過反射/排程設定/BPMN listener 動態調用,grep 抓不到。detection_profiles 內容庫服務化(FR-059/060,11 主檔/4,841 控制項列)有「中央維運公版、各站台拉取」的天然形狀,但未評估商業可行性(是否有中央維運需求、客戶站台是否允許外連)。標為第四階段之後的獨立評估項,不該塞進 P9。design.md 原定願景是「26 支收斂為約 12–15 支輪廓清楚的疆界套件」。本案盤點後的實際收斂路徑:
現況 26 支
−4 身分族合併(D9:auth+login+mfa+captcha → jedi-iam)
−1 jedi-department 下架(D9)
−3 本案退役(jedi-oscal v1、jedi-resource-store、jedi_system_log 子套件)
−1 jedi-bulletin 下架(D14,待拍板)
±0 問卷/OSCAL 疆界是「吸收主專案模組」,套件數不減但輪廓變清楚
────
≈ 17 支(若 D14 不下架則 18 支)
未達 12–15 支,但這是正確的結果——本案排除了四個假候選(system-config+menu、remote-agent+log-forwarding、integrity+license-runtime、追蹤三支),若硬湊數字去合併它們,會把租戶機密與全域字典塞進同一租戶模型、把檔案完整性綁進授權,是用架構退步換數字好看。
真正的收斂價值不在套件數,而在: