FR-102 · 需求索引 · 本頁由 build 掃資料夾生成

FR-102 主專案全模組現況地圖

🟢 一棒完成(2026-09-16,實查 HEAD b18b2ff4)。26 個模組全數盤完,無跳過。卡 CM-1824。本表是某一天的快照,引用前先重驗——重跑指令見文末「怎麼重跑」。

狀態:詳見下方 文件 0 份

FR-102 一頁看完

實查日期:2026-09-16 | 當時 HEAD:b18b2ff4 | branch:feature/review

🔴 本表已部分失效(2026-09-16 當天下午 FR-103 四棒落地)。 下列四項的狀態已變, 總表未逐格改寫——以本段為準:

模組 表上寫的 現況
report 🟢 實的 已整條線退役(CM-1830)——bin/report_downloader/+api/report/+app/report/ 全刪
translate 🟠 殘留 本機 .pyc 已清(CM-1830),目錄不存在
user_auth_provider 🟡 殼 已上移 jedi-iam(CM-1832)——主專案只剩 core/plugins/identity.py 接線
survey/task_survey 🔵 接線殼(在 api/) 接線已收進 core/plugins/survey.py(CM-1834)——api/ 兩目錄已刪,主專案側 23 支套件全部同形

另:ui_routes 12 列 enable=0 選單已全數退役(CM-1833),出貨基線已重產。 總 route 數 200 → 199(report 那條)。

🔴 引用前先重驗,本表會過期。 這是 2026-09-16 那天的地貌,不是恆真的事實。 主專案正在持續模組化,一支 commit 就可能讓某個模組從「實的」變成「殼」。 文末「怎麼重跑」那節給了所有指令,重跑一次比相信這份表安全。

🔴 本文只寫現況,不給建議。 決策者 2026-09-16 明確要求:不排優先序、不判輕重緩急。 「這個模組是殼」是現況,「所以應該搬走」是建議——後者不在這份文件裡。


§1

結論

  1. 26 個模組,分布是:實的 14 個、接線殼 4 個、殼 1 個、殘留 2 個、死的 0 個、需查證 0 個,另 5 個是「單層存在的共用設施」(跨模組櫃檯/port 實作/共用型別,不屬上述五類的業務模組形狀,見總表備註)。

  2. 主專案真正自己持有資料的只剩 19 張表(infra/ 底下的 ORM model 全數)。對照 DEV 庫 169 張表,代表資料層絕大多數已經在套件裡;主專案 infra/ 現在主要是三種東西——自己的表、跨疆界唯讀查詢(readmodel)、套件 port 的宿主實作(adapter)。

  3. 對外 route 共 200 條:api/ 底下 199 條(來自 15 個在 config/app_modules.py 註冊的模組),加上 core/plugins/remote_agent.py 單獨掛的 1 條檔案下載端點。另有 7 條被註解掉(oscal 3、module_frame 2、flow_control 1、flow_engine 1),不計入。

  4. 規模高度集中:oscal(19,996 行)與 module_frame(10,951 行)兩支就佔主專案四層總量的一半(62,442 行中的 30,947 行,49.6%)。其餘 24 個模組平均不到 1,400 行。

  5. 兩個模組的狀態標籤蓋不住內部差異:oscal 與 module_frame 內部同時住著「真業務實作」與「套件轉接殼」兩種 service,混在同一個 service 目錄。模組層級只能標一個標籤,要分清哪幾支是哪一類需逐檔判——已標為需另開卡。

  6. 兩處殘留是實查撞到的(不是推論):api/translate/ 在 git 已零檔案、工作目錄只剩 .pyc;app/evidence_classification/ 零行 Python、只有兩個與套件位元相同的 JSON。詳見總表 B 的證據欄。


§2

總表 A:規模與對外 route

每格是「檔數/行數」,只計 git 追蹤的 .py(不含 __pycache__、不含 .json/.docx/.html 等資料檔)。

模組 api app domain infra 小計 對外 route 路徑前綴
oscal 67/3,984 46/10,964 36/4,549 14/499 163f/19,996L 61 /api/1.0
module_frame 25/2,765 31/7,418 12/351 13/417 81f/10,951L 45 /api/1.0
flow_control 7/1,151 17/3,995 — 10/2,873 34f/8,019L 11 /api/1.0/grc
flow_engine 17/1,323 16/3,042 27/856 20/1,120 80f/6,341L 19 /api/1.0
cloud_integration 8/533 23/3,350 19/771 19/1,154 69f/5,808L 10 /api/1.0
project 12/1,523 3/488 — — 15f/2,011L 34 /api/1.0
readmodel — 4/131 — 14/1,749 18f/1,880L 0 —
system_config 5/344 7/785 — 4/481 16f/1,610L 3 /api/1.0
project_summary_report 7/345 7/347 12/189 10/351 36f/1,232L 7 /api/1.0
associations — 9/294 16/286 13/442 38f/1,022L 0 —
upload_file — 3/364 — 4/353 7f/717L 0 —
setup 3/150 5/423 — 2/90 10f/663L 2 /api/1.0
auth — 5/335 — 2/40 7f/375L 0 —
survey 1/174 — — 2/163 3f/337L 0(全在套件) /api/1.0
notification — 6/291 — 2/37 8f/328L 0 —
user_auth_provider 7/190 3/95 — — 10f/285L 4 /api/1.0
remote_agent 3/63 — — 5/196 8f/259L 1(plugin 掛) /api/1.0
notify_config 5/67 3/97 — — 8f/164L 1 /api/1.0
detection_tools — 4/161 — — 4f/161L 0 —
report 2/83 3/49 — — 5f/132L 1 /api/1.0
identity — — — 2/85 2f/85L 0 —
version 3/42 — — — 3f/42L 1 /api/1.0
task_survey 1/33 — — — 1f/33L 0(全在套件) /api/1.0
common — 4/11 — — 4f/11L 0 —
evidence_classification — 2/0 — — 2f/0L 0 —
translate 0/0 — — — 0f/0L 0 —
合計 174f/12,765L 202f/32,636L 123f/6,995L 137f/10,046L 636f/62,442L 200

四層合計含各層根目錄的 __init__.py 4 檔 9 行(api/ 3、app/ 4、domain/ 0、infra/ 2),故 632 + 4 = 636 檔。

survey/task_survey 的「對外 route 0」指主專案這一側 0 條——它們實際提供的端點在 jedi-survey 內,由主專案這兩個目錄組 adapters 後掛上去。

remote_agent 的 1 條是 GET /api/1.0/agents/files/<uid>,不經 config/app_modules.py,由 core/plugins/remote_agent.py::mount() 掛。


§3

總表 B:狀態與證據

與套件的關係:① 實體在主專案 ② 主專案是殼、實體在套件 ③ 接線殼(route 在套件、主專案組 adapters)④ 與套件無關 狀態:🟢 實的 / 🟡 殼 / 🔵 接線殼 / ⚫ 死的 / 🟠 殘留 / ⬜ 共用設施(不屬五類的形狀,見下方說明)

模組 關係 狀態 前端有沒有在用(四層) 證據 需另開卡
oscal ①+部分② 🟢 實的 ✅ 活。api.js 15 個 OSCAL_* + 37 個 SSP_* 常數;FE 路由 /compliance-framework/*;ui_routes compliance-framework-manage enable=1 domain/oscal 4,549 行是真 domain 資產(docx 解析 892+CMMC adapter 860+parser core 555);import_diff/ssp_diff_service.py(751 行)零個 jedi_oscal_v2 引用、全自寫;infra/oscal 自己持有三張匯入 job 表。但 framework_app_service.py(247 行)幾乎每支就一行轉呼叫套件,檔頭自陳「補套件沒有的主專案職責——分頁、filter」 是 — 真業務與轉接殼混住同一 service 目錄,模組層級單一標籤蓋不住
module_frame ①+部分② 🟢 實的 ✅ 活。api.js 37 個 MODULE_FRAME_*;FE 路由 /module-frame/* 8 條;ui_routes module-frame enable=1 自己持有 module_frames/module_frames_trans 兩張表;最大兩支全自寫(ssp_import_template_app_service.py 1,500 行、module_frame_template_import_service.py 1,008 行);excel_template/generator.py(652 行)零 jedi 引用。但 module_frame_party_service.py(323 行)檔頭自陳「寫入層換成套件 jedi_oscal_v2 SspService 子物件 CRUD,對外 API 不變」,主體是 FE 欄位 ↔︎ OSCAL props 對應表;components/inventory/leveraged 同型 是 — 同上,真業務與 v2 轉接殼約各半
flow_control — — ✅ 活。api.js 31 個 GRC_*;e2e 直打 /api/1.0/grc/project/* 本棒不做判定——FR-104(CM-1823)正在深挖。此處只記規模(34f/8,019L、11 條 route、前綴 /api/1.0/grc)與四層分布 否 — 已有 FR-104 在做
flow_engine — — ✅ 活。api.js 7 個 /flow-engine/* + 2 個 job-evidence;FE 路由 /flow/template-manage;ui_routes flow-template-manage enable=1 同上,本棒不做判定。只記規模(80f/6,341L、19 條 route)。自己持有五張表(workflow_executions/flow_templates/job_evidences/stage_objects/workflow_execution_control_mapping) 否 — 已有 FR-104 在做
cloud_integration ④ 與套件無關 🟢 實的 ✅ 活。api.js 7 個 INTEGRATION_GDRIVE_*;FE 路由 /settings/cloud-integrations;ui_routes cloud-integrations enable=1;第四層另有排程(core/scheduler.py 每 5 秒跑 drive_sync_worker、每 6 小時 webhook_channel_renewer) 四層俱全全自寫:自己持有 tenant_drive_integrations/drive_sync_jobs/drive_folder_mappings 三張表,自己實作 Google API client(301 行)與 OAuth client(115 行);app/ 3,350 行含四支 job handler + worker。對套件是「消費」(用 jedi_file_upload 存檔、用 jedi_flow_engine 反查 job),不是被殼住。決策者先前已明確裁不動 否
project ③ 接線殼+一支真業務 🔵 接線殼 ✅ 活。api.js 6 個 AUDIT_ROUND_*+AR_IMPORT/AP_DOCX;FE 路由 /project/projects/:id/round/:roundUid/* 5 條;ui_routes project-list/audit-manage enable=1 domain/project 與 infra/project 完全不存在;34 條 route 中 29 條直接注入 jedi_compliance_audit 的 app service(audit_round/assessment_result/poam/ap_docx_import/ar_import),route body 是一行轉呼叫(實查 audit_round_route.py 32 個 HTTP method 皆此形)。唯一真業務是 project_start_app_service.py(481 行,clone 資源庫三件組+建 participants+初始化 SSP+產準備期 job) 否 — 結構清楚,看 route import 即可判
project_summary_report ④ 與套件無關 🟢 實的 ✅ 活。api.js 6 個常數;FE 路由 /project-summary-report 3 條;ui_routes report-manage enable=1 四層齊全且都是主專案:自己持有 project_summary_reports/project_summary_report_histories 兩張表;全模組只有 2 處非 jedi_common 引用(為 FK join 取 Project ORM)。route 層自己做 weasyprint PDF 匯出+bleach XSS 白名單 否
system_config ① 實體在主專案 🟢 實的 ✅ 活。api.js /system/config//system/configs//system/security-policy;FE 路由 /system/security-policy、/system/storage-config;ui_routes security-policy/storage-config enable=1 api/system_config/__init__.py 檔頭:「只剩三條沒搬進套件的 route……純 CRUD 三條已搬進 jedi-system-core」;留下的是吃產品規則那些:guarded_system_config_service.py(284 行)是套件 service 的子類,內含「出廠快照對外不存在」與「SMTP/LDAP 落點釘死 ROOT tenant」兩條產品規則;infra/system_config/system_config_root_reader.py(321 行)是全專案「繞 RLS 讀 ROOT 設定」的 canonical 否
setup ① 實體在主專案 🟢 實的 ✅ 活(但**不在 ui_routes,正常)。FE 路由 /setup,由登入頁偵測「系統尚未初始化」自動導入;api.js 有 SETUP_STATUS/SETUP_PROVISION 兩條未登入可達端點,身分證據是 install.sh 印的一次性 X-Setup-Token;setup_wizard_service.py(395 行)自己編排 D13 落法(建客戶第一個業務租戶+管理員),轉呼叫 jedi_iam 既有 service 但守門與編排全在主專案;infra/setup/setup_state_reader.py 繞 RLS 讀,因為呼叫端未登入。不在 ui_routes 是正常的**——它不是選單頁面,是開站流程 否
notify_config ① 實體在主專案 🟢 實的 ✅ 活。api.js NOTIFY_CONFIG_TEST;FE 路由 /system/notify-config;ui_routes notify-config enable=1 單一端點 POST /notify-config/<channel>/test;notify_config_test_service.py(97 行)自己做兩段——_resolve_value() 在 changePwd=False 時從 DB 既存列補 secret(避免使用者重貼 token),送出段委派 jedi_notification 否
user_auth_provider ② 主專案是殼 🟡 殼 ✅ 活。FE UserProfileForm.vue POST 綁定/DELETE 解綁;FE 路由 /system/ldap-config;ui_routes ldap-config enable=1。無獨立選單是正常的——綁定功能掛在使用者資料頁裡 jedi-iam 已有完整全鏈(entity/query entity/repo impl/mapper/model/domain service/app service/auth_provider port);主專案 route 三支方法全是一行轉呼叫,型別直接標 JediUserAuthProviderService。唯一例外:ldap_service.py(88 行)有兩條主專案的資安防護——CM-1283(讀不到 ROOT 列會把 LDAP 密碼靜默清空而畫面顯示成功)、CM-1564(主機位址變更時沿用舊密碼=把服務帳號密碼送去任意主機)。FR-100 已記錄同一結論 否 — 結論已確定,細節在 FR-100
report ④ 與套件無關 🟢 實的(api 層)/app 層有一支零呼叫者 ⚠️ 前端沒在用、但它是活的。api.js 無常數、FE 全 repo 零命中、ui_routes 的 report-view enable=0——第四層才是它的使用者:bin/report_downloader/report_downloader.py(Selenium,登入 Wazuh 下載 11 種資安報表 PDF 寫進 SCHEDULE_REPORT_DIR),這支 API 讓人預覽那些檔 scheduler_report_route.py(60 行)send_file() 直讀 SCHEDULE_REPORT_DIR(預設 /reports/),含 path traversal 防護 _safe_report_path()(擋 ./../分隔符/絕對路徑+realpath 容器檢查)。.env.sample:121-122 註解寫明用途。實查另抓到:app/report/common/common_utils.py(41 行,get_schedule_report_fileinfo)全 repo 零呼叫者,是 file-list 端點移除後留下的;common/middleware/license_readonly_mw.py:110 白名單仍列 /schedule-report/file-list,該路徑已無註冊 否 — FR-100 已定案「活的、不可刪」
version ④ 與套件無關 🟢 實的 ⚠️ 前端沒在用、但它是活的。api.js 無常數、FE 零命中、ui_routes 零命中——第四層是使用者:e2e site-regression/scripts/lib/version-probe.js 打它抓版本歸檔;build smoke scripts/build/smoke_release.sh:167 拿它當探針②;scripts/build/probe_container.sh:433 拿它驗 socketio 模式沒載 REST(期望 404) 全模組 3 檔 42 行,單一端點 GET /api/1.0/version,免認證(檔頭自陳「供 site-regression 歸檔管線/外部健康檢查抓版本」),版本來源三層解析在 common/util/app_version.py 否
survey ③ 接線殼 🔵 接線殼 ✅ 活。api.js 23 個 survey 相關常數;FE 路由 /survey/manage//survey/designer/:id//survey/fill//survey/preview/:id;ui_routes survey-manage enable=1 api/survey/__init__.py 檔頭:「route 已上移至 jedi-survey 套件(FR-069 4.6 / P11)。本檔瘦成『組 adapters + 建 blueprint』」。主專案這 3 檔只做接線:build_adapters() 組 12 個 service provider + 6 支 adapter;infra/survey/adapters.py(163 行)是套件 port 的宿主實作。這是正常終局形狀,不是問題 否
task_survey ③ 接線殼 🔵 接線殼 ✅ 活(與 survey 同一套 FE 頁面) 全模組 1 檔 33 行,create_module() 從 api.survey 借 adapters/config,只回作答層 blueprint。檔頭說明為何不併進 api/survey:main.py 載入迴圈是「一個模組收一個 blueprint」,且 blueprint 名是 endpoint 名前綴,併掉會讓 url_for('task-survey.x') 靜默匹配到 0 條 否
remote_agent ③ 接線殼+一支例外 🔵 接線殼 ✅ 活。FE 路由 /system/remote-agent-manage;ui_routes remote-agent-manage enable=1(管理端點在套件)。主專案留下的那 1 條 GET /agents/files/<uid> 的呼叫者是 agent 本身(第四層,非瀏覽器) config/app_modules.py:108-112 自陳「管理端點上移 jedi-remote-agent,掛載改由 core/plugins/remote_agent.py 的 mount() 負責」,該列已註解掉。api/remote_agent/__init__.py 全檔只有一行 docstring。留下的檔案下載端點理由寫在檔頭:「回的是 binary 串流不是 JSON 信封,授權走 detection 的 resolver 清單,搬進套件會逼套件認得 detection_orchestration」。infra/remote_agent/ 三支 adapter 各實作套件 ABC 否
auth ② 主專案是殼(僅剩守門與範本) 🟢 實的(留下的三支都是產品知識) ✅ 活(route 在 jedi-iam,由 core/app_factory.py 的 register_identity(..., mount_api=True) 掛)。FE 路由 /auth/user-manage//auth/role-manage;ui_routes 兩者 enable=1 config/app_modules.py 首行自陳「身分模組的 route 整組上移 jedi-iam……api/auth/ 目錄已刪除」。主專案 app/auth 剩三支,每支都有檔頭說明為何留下:user_app_service.py 只剩 root admin 保護(「哪個帳號受保護」是主專案業務知識);role_app_service.py 只剩寫入端 license 守門(要查 DB,route 層做不到);user_import_template_app_service.py(107 行)是產品客製的 Excel 範本產生。infra/auth/root_admin_reader.py 是繞 RLS 的提權查詢(子租戶 session 查 root admin 得 0 列 → 會被判「不是 root admin」而放行) 否
notification ② 主專案是殼(僅剩產品知識) 🟢 實的(留下的是產品規則) ✅ 活(測試信 route 在 jedi-notification,由 register_notification(..., mount_api=True) 掛;SocketIO namespace /socket/notification 在 config/socketio_namespaces.py:43)。主專案這份的呼叫者是內部 12 個消費點(走 library 用法不經 route),實查 flow_control/project_service.py:25、flow_engine/workflow_execution_service.py:54 + 5 顆 DI container config/app_modules.py 自陳「測試信 route 上移 jedi-notification……api/notification/ 目錄已刪除。⚠️ 通知功能本身不受此影響:12 個消費點走套件的 library 用法」。notification_service.py(110 行)自己做租戶 context 判定與 channel 分派,送出委派 jedi_notification;test_mail_service.py(121 行)檔頭記 CM-1496:SMTP 是全系統共用一組存在 ROOT 租戶,業務租戶走一般 tenant-scoped 查詢必然 404 否
upload_file ② 主專案是殼(僅剩產品知識) 🟢 實的(留下的是產品規則) ✅ 活(route 在 jedi-file-upload,由 register_file_upload(..., mount_api=True) 掛)。主專案這份的呼叫者是 di_containers/upload_file/(core/plugins/file_upload.py:95 交進套件當 file_upload_service) config/app_modules.py 自陳「上傳模組的 route 整組上移 jedi-file-upload……api/upload_file/ 目錄已刪除」。managed_file_upload_service.py(364 行)是 jedi_file_upload.FileUploadService 的子類,產品知識是「從 system_config(group=STORAGE_CONFIG)讀儲存設定,取代套件原本從 env 讀」;infra/upload_file/ 4 檔含 remote agent adapter 與繞 RLS 的 storage config reader 否
detection_tools ② 主專案是殼(兩支是 shim) 🟢 實的(task_type_declaration.py 是真的) ✅ 活(35 條 route 在 jedi-detection,由 core/plugins/detection.py 掛)。FE 路由 /plugin/detection-profile-manage//plugin/tool-plugin-manage;ui_routes 兩者 enable=1;e2e 直打 /api/1.0/detection-tools 4 檔中 2 檔是明文 shim:service/__init__.py 與 dto/__init__.py 檔頭皆寫「[shim] 已隨 FR-069 4.4 搬進 jedi_detection.app.{service,dto}(CM-1489)……新 code 請直接 import 套件」,作法是遞迴把每個子模組指向套件同一物件(只掛頂層別名會讓同一 class 被建兩份,isinstance() 與 SQLAlchemy registry 判失敗)。真的那支是 task_type_declaration.py(109 行),被 core/plugins/task.py:85 與 flow_control/job_service.py:12 使用 否
readmodel ④ 與套件無關 ⬜ 共用設施(跨疆界唯讀櫃檯) 間接活——api/flow_control/routes/job_route.py:30 與 api/flow_engine/routes/flow_engine_route.py:18 使用 MyJobsAppService;di_containers/flow_control/:100 使用 AuditorDashboardService 這是刻意設計、不是殘留。infra/readmodel/__init__.py 檔頭:「跨疆界純讀報表查詢層——主專案櫃檯,永遠不隨套件走」,判準是「這段 SQL 讀了幾個疆界的表?跨疆界純讀 → 這裡;單疆界 → 留該模組;會寫 → 留該模組」。動機寫得很明確:聚合查詢的 SQL 字串裡藏著對其他模組表的依賴,grep 守衛掃不到(表名在字串裡不是 import)、harness 也測不出,誤搬進套件的後果是靜默的。四個子資料夾(tasks/audit/oscal/detection)共 10 支查詢,每支檔頭有「疆界拼盤」表 否
associations ④ 與套件無關 🟢 實的 ❌ 無直接前端入口(無 route)。呼叫者是內部:app/flow_engine/workflow_execution_service.py:40 用 ProjectAssessmentPlanMappingService、app/flow_control/project_service.py:37 用 ProjectAssessmentPlanMappingDomainService 四層俱全(38 檔)、自己持有三張關聯表(project_assessment_plan_mapping/project_org_unit_mapping/project_system_characteristic_mapping)。實查發現三組之間使用強度不一:ProjectAssessmentPlanMappingService 有兩個模組外的呼叫者;ProjectOrgUnitMappingService 與 ProjectSystemCharacteristicMappingService 的呼叫者只有 DI container 註冊(associations_containers.py:114/:127),沒有業務端 import 是 — 三組中兩組只在 DI 註冊、無業務呼叫者,是不是仍在用需逐一追(可能經 DI 注入到某處但本次 grep 沒撈到)
identity ③ 接線殼 ⬜ 共用設施(port 實作) 間接活——di_containers/identity/:21,33、core/plugins/api_log.py:161,189、common/util/audit_nickname.py:11 都指向它 infra/identity/user_name_resolver.py 檔頭:「IUserNameResolver(D8 身分名冊)的唯一主專案實作——審計欄位補暱稱。🔴 不要再複製一份:四支套件(jedi-asset/jedi-bulletin/jedi-file-upload/jedi-log)共用這一支」。檔內記兩個坑:不進 DI container 會讓 created_user_name 恆為 None;少 @transaction 會被降級吞成 warning、API 照回 200。test/test_module_boundaries.py:1343 列為明列例外 否
common ④ 與套件無關 ⬜ 共用設施(共用型別) 間接活——全 repo 19 處使用 全模組 11 行:base_dto.py 6 行(@dataclass BaseDTO 只有 to_dict())、common_schema.py 5 行(BooleanResponse),另兩檔是空 __init__.py。BooleanResponse 5 處(notify_config/user_auth_provider 兩支/system_config 的 route @marshal_with)、BaseDTO 4 處 否
evidence_classification ② 實體在套件 🟠 殘留 ❌ 前端不經此(10 條 route 在 jedi-evidence-classification,由 core/plugins/evidence_classification.py 掛)。FE 路由 /project/projects/:id/ap/:apUid/classify-evidence/* 打的是套件端點 零行 Python(2 個空 __init__.py),只剩 resources/ 下兩個 JSON。實查 md5:cmmc_l1_aos.json 與 cmmc_l1_canon.json 與套件 jedi_evidence_classification/resources/ 下同名檔位元相同。scripts/build/build_release.sh:674-677 對這件事有明文說明:「⚠️ --include-data-dir 仍留著 app/evidence_classification/resources——那份是 scripts/evidence/classify/ 底下獨立原型腳本的輸入(它們不 import 套件),不是產品執行期讀的。產品執行期讀的是套件版」。現存使用者:scripts/evidence/classify/classify_evidence_drive.py:73 與 prototype-ui/generate.py:12,另 build_release.sh:787 把它打進 binary、test/test_build_integrity_manifest.py:42 把它列為 resources 層 否 — 現況與理由都已在 build 腳本寫明
translate ② 實體已刪 🟠 殘留 ❌ 已退役(CM-1821,四方查證無呼叫者) git 已零檔案(git ls-files api/translate 回 0),退役 commit 是 16ff211c。工作目錄下該目錄仍在,內容只有 5 個 .pyc(__pycache__/ 下的舊編譯產物)。這是本機工作目錄的殘留,不是版控內的殘留 否

⬜「共用設施」這個標籤是什麼

卡片給的五類(實/殼/接線殼/死/殘留)是為業務模組設計的。readmodel、identity、common 三個不是業務模組——它們沒有 route、沒有自己的業務語意,是「給別的模組用的東西」:跨疆界唯讀查詢櫃檯、port 的宿主實作、共用型別。硬套五類會失真(說它們「實的」對,但看不出它們與 oscal 是完全不同的東西;說「殼」則完全錯)。故另標一類,並在證據欄寫清楚它們是什麼。

survey/task_survey/remote_agent/project 四個仍歸 🔵 接線殼,因為它們有對外 route(或負責掛 route),符合接線殼定義。


§4

需另開卡深挖的(只列事實,不排序)

模組 卡在哪
oscal 19,996 行內部同時住著真業務(parser/diff/adapter)與轉接殼(framework_app_service 型),混在同一 service 目錄。要分清哪幾支是哪一類需逐檔判,超出「深度到模組為止」的範圍
module_frame 同上。主專案表的真業務(Excel 範本 1,500 行+generator 652 行)與 v2 子物件轉接殼(party/components/inventory/leveraged)約各半
associations 三組 mapping service 中,ProjectOrgUnitMappingService 與 ProjectSystemCharacteristicMappingService 的呼叫者只有 DI container 註冊,沒撈到業務端 import。是真的沒人用、還是經 DI 注入到某處而本次 grep 沒撈到,需逐一追

不列入的:flow_control/flow_engine 由 FR-104(CM-1823)深挖中,本棒不重複判定。


§5

實查撞到的兩件小事(不是建議,只是現況)

  1. app/report/common/common_utils.py(41 行,get_schedule_report_fileinfo)全 repo 零呼叫者——grep -rn "get_schedule_report_fileinfo" --include='*.py' . 只命中定義本身。它是給已移除的 file-list 端點用的。
  2. common/middleware/license_readonly_mw.py:110 的白名單仍列 /schedule-report/file-list,該路徑在 api/report/ 已無註冊(FR-100 已記錄同一件事)。另 docs/system-design/scripts/generate_swagger.py:2431,2433 與 test/test_license_readonly_gate_routing.py:91 也還在提它。

§6

怎麼重跑

這節的存在理由:FR-104 的前身(flow-boundary-design.md,已刪除)就是因為數字過期半個月無人察覺而報廢。 下面每條指令都在 2026-09-16 於 b18b2ff4 實跑過,輸出即本文數字。

① 算四層規模(總量)

cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
for L in api app domain infra; do
  printf "%-8s %s 檔 / %s 行\n" "$L" \
    "$(git ls-files "$L" | grep '\.py$' | wc -l | tr -d ' ')" \
    "$(git ls-files "$L" | grep '\.py$' | xargs wc -l | tail -1 | awk '{print $1}')"
done

用 git ls-files 而非 find——find 會撈到 __pycache__ 與未入版控的殘留(api/translate/ 正是這種,find 看得到、git 看不到)。

② 算每個模組的四層規模(總表 A)

cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
python3 - <<'PY'
import os, subprocess, collections
layers = ['api','app','domain','infra']
mods = {d for L in layers for d in os.listdir(L)
        if os.path.isdir(os.path.join(L,d)) and d != '__pycache__'}
cnt = collections.defaultdict(lambda: [0,0])
for p in subprocess.run(['git','ls-files']+layers, capture_output=True, text=True).stdout.split():
    parts = p.split('/')
    if len(parts) < 2 or not p.endswith('.py'):
        continue
    n = sum(1 for _ in open(p, encoding='utf-8', errors='ignore'))
    cnt[(parts[0], parts[1])][0] += 1
    cnt[(parts[0], parts[1])][1] += n
print(f"{'module':26}{'api':>12}{'app':>12}{'domain':>12}{'infra':>12}{'total':>13}")
for m in sorted(mods):
    row, tf, tl = [], 0, 0
    for L in layers:
        v = cnt.get((L,m))
        if v:
            row.append(f"{v[0]}/{v[1]}"); tf += v[0]; tl += v[1]
        else:
            row.append("—")
    print(f"{m:26}{row[0]:>12}{row[1]:>12}{row[2]:>12}{row[3]:>12}{f'{tf}f/{tl}L':>13}")
PY

③ 數對外 route(總表 A 的 route 欄)

用 AST 不用 regex——add_resource( 常換行寫,regex 會漏抓(本棒第一版就漏了 module_frame 的 2 條與 oscal 的 2 條)。

cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
python3 - <<'PY'
import os, ast
tot = 0
for d in sorted(os.listdir('api')):
    p = os.path.join('api', d)
    if not os.path.isdir(p) or d == '__pycache__':
        continue
    prefix, paths = set(), []
    for root, dirs, files in os.walk(p):
        dirs[:] = [x for x in dirs if x != '__pycache__']
        for f in sorted(files):
            if not f.endswith('.py'):
                continue
            try:
                tree = ast.parse(open(os.path.join(root,f), encoding='utf-8', errors='ignore').read())
            except SyntaxError:
                continue
            for node in ast.walk(tree):
                if not isinstance(node, ast.Call):
                    continue
                fn = node.func
                name = fn.attr if isinstance(fn, ast.Attribute) else (fn.id if isinstance(fn, ast.Name) else '')
                if name == 'add_resource' and len(node.args) >= 2:
                    paths += [a.value for a in node.args[1:] if isinstance(a, ast.Constant)]
                elif name == 'route' and node.args and isinstance(node.args[0], ast.Constant):
                    paths.append(node.args[0].value)
                elif name == 'Blueprint':
                    for kw in node.keywords:
                        if kw.arg == 'url_prefix' and isinstance(kw.value, ast.Constant):
                            prefix.add(kw.value.value)
    u = len(set(paths)); tot += u
    print(f"{d:24} routes={u:3}  prefix={sorted(prefix)[0] if prefix else '—'}")
print("api/ 合計:", tot)
PY

AST 只看得到沒被註解的呼叫,故輸出即「實際生效數」。被註解掉的另數:

grep -rn "^\s*#.*add_resource" api/ --include='*.py'

api/ 之外還有 1 條:core/plugins/remote_agent.py::mount() 單獨掛的 GET /agents/files/<uid>。查法:

grep -rn "add_resource" core/plugins/*.py

④ 看哪些模組有註冊(哪些已上移套件)

cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
python3 -c "
import re
s = open('config/app_modules.py').read()
act = re.findall(r'^\s*\"([a-z_]+)\",\s*\$', s, re.M)
print('生效中:', len(act)); print(act)
"

這支檔的註解本身就是遷移史——每個被移除的模組旁邊都寫了搬去哪個套件、由哪一行掛載。讀它比 grep 快。

⑤ 查前端有沒有在用 —— 🔴 四層都要查

只查一到三層會判錯。 FR-100 踩過:report 前三層全綠(api.js 無常數/FE 零命中/ui_routes enable=0),第四層才發現它服務一支 Selenium 程式。本棒 version 也是同型——前三層全空,第四層是 e2e 版本探針與 build smoke。

FE=~/Projects/Billows/Audit-Manager/compliance-manager-fe
E2E=~/Projects/Billows/Audit-Manager/compliance-manager-test
BE=~/Projects/Billows/Audit-Manager/compliance-manager-be
KEY='<要查的端點片段,例如 google-drive>'

# ① FE api.js 常數
grep -n -- "$KEY" $FE/src/config/api/api.js

# ② FE 路由(掛著就打得進去,即使選單關了)
#    ⚠️ 這層要用**頁面路徑**(如 cloud-integrations)不是端點片段(如 google-drive)
#       ——兩者常常不同名。先用下面第二條找出是哪些 .vue 在用,再回頭查它們掛在哪條路由
grep -n -- "$KEY" $FE/src/config/router/index.js
grep -rln -- "$KEY" $FE/src/          # 全 FE repo(這條才是可靠的「有沒有人用」)

# ③ DB ui_routes —— 使用者實際看得到什麼的真相來源
#    🔴 欄位是 name / url / enable(不是 path)
cd $BE && export PGPASSWORD=$(grep -oE 'DB_PASSWORD=\S+' .env | cut -d= -f2)
psql -h localhost -p 5432 -U cmmgr -d guidant_ai_dev -At -F'|' \
  -c "select enable, name, url from public.ui_routes order by enable desc, url"

# ④ 🔴 非瀏覽器呼叫者 —— 最常被漏掉的一層
grep -rn -- "$KEY" $BE/bin/ $BE/scripts/ $BE/core/scheduler.py $BE/common/middleware/ 2>/dev/null
grep -rn -- "$KEY" $E2E --include='*.js' --include='*.feature' 2>/dev/null | grep -v node_modules

四層矛盾本身就是線索,不是要挑一層相信:FE 的 cruising feedback 就是「選單關了但路由還在」被抓出來的(FR-100)。

DEV 庫座標:localhost:5432 / guidant_ai_dev / 帳號 cmmgr(密碼查 .env)。唯讀查詢不必請示。

⑥ 判斷「是殼還是實的」

沒有一條指令能回答這題——要讀程式在做什麼。但下面三步能大幅縮短:

# a. 先讀模組 __init__.py 的 docstring。本 repo 的慣例是把去留理由寫在檔頭,
#    通常直接寫「route 已上移 jedi-xxx」或「刻意留主專案,理由是…」
cat api/<模組>/__init__.py app/<模組>/__init__.py 2>/dev/null

# b. 看該模組引用了哪些套件(引用多 ≠ 是殼,要看引用的是「資料層 primitive」還是「整支 service」)
grep -rhoE '(from|import) (jedi_[a-z_0-9]+)' api/<模組> app/<模組> domain/<模組> infra/<模組> 2>/dev/null | sort -u

# c. 開最大的 2-3 支 service,看它是自己實作、還是每個 method 一行轉呼叫
git ls-files app/<模組> | grep '\.py$' | xargs wc -l | sort -rn | head -5

判準:method 本體是「自己算/自己拼/自己判」還是「return self._pkg_service.xxx(...)」。行數不是判準——framework_app_service.py 有 247 行,但主體是分頁與排序的膠水,業務在套件裡。

⑦ 看主專案還持有哪些表

cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
grep -rn "__tablename__" --include='*.py' infra/ | grep -v __pycache__ | sed 's|infra/||' | sort

2026-09-16 實查是 19 張(含 1 個 view vw_user_job_queue)。對照 DEV 庫總表數:

psql -h localhost -p 5432 -U cmmgr -d guidant_ai_dev -At -F'|' -c "
select table_schema, count(*) from information_schema.tables
where table_type='BASE TABLE' and table_schema not in ('pg_catalog','information_schema')
group by 1 order by 2 desc"

§7

需求討論紀錄

日期 事項
2026-09-16 決策者 review 主專案剩餘 route(FR-100)後提出:要開發新功能,需要先看到主專案剩下什麼的完整地圖。明確要求只給現況、不給建議——不排優先序、不判輕重緩急
2026-09-16 首腦精算範圍(636 檔/62,442 行/約 25 個模組)並開卡 CM-1824,卡上帶已查出的訊號與已有結論的模組,要求規模數字重跑
2026-09-16 本棒實查完成。實際模組數 26 個(卡上估「約 25 個」)。差異來源是 identity/common/evidence_classification 這類單層、極小或零行的目錄——按「四層同名目錄算一個模組」的定義它們都成立,故全數列入
2026-09-16 五類狀態標籤不足以描述 readmodel/identity/common 三個非業務模組,另加 ⬜ 共用設施一類並在文中說明理由(未自創用於業務模組的新類別)

§8

相關

  • FR-100(CM-1820)主專案剩餘 route 逐支盤點——本表 route 欄與前端四層查法的前身;report/user_auth_provider/translate 的結論直接引用該案
  • FR-104(CM-1823)流程疆界重新分析——flow_control/flow_engine 兩模組的判定由該案負責,本表只記規模
  • FR-099(CM-1816~1819)意見回饋與標籤併入 jedi-issue——本表盤點時該案五個目錄已刪除,故不在總表上
  • FR-090/FR-080/FR-069 模組化母源——config/app_modules.py 的註解記錄了每一支的遷移
§9

Notion 卡

卡片內容(決策紀錄、驗收條件)以 Notion 為準,本頁只記座標。

關係 卡號 標題 狀態
母案 CM-1824 FR-102 主專案全模組現況地圖——636 檔逐模組盤點,只給現況不給建議(一棒,唯讀) —