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

FR-105 oscal 與 module_frame 逐檔分類

✅ 三棒完成並驗收(2026-09-16)——module_frame 81 檔、oscal 163 檔全數分類,清理棒完成,主專案 route 從 195 減到 189。母卡 CM-1838。

狀態:✅ 已完成 文件 2 份

FR-105 oscal 與 module_frame 逐檔分類

§1

結論

主專案四層 636 檔/62,442 行裡,oscal 與 module_frame 兩支就佔一半 (244 檔/30,938 行)。FR-102 已把 26 個模組盤到「模組層級」,但對這兩支明講 「單一標籤蓋不住內部差異」——它們裡面同時住著主專案自己的業務(解析 Word/Excel、 產生範本、比對文件差異)與轉接殼(主專案不做事,只把前端請求翻譯成 jedi_oscal_v2 套件聽得懂的形狀),混在同一個 service 目錄,看目錄分不出來。

要動這兩個模組的人需要一張逐檔的圖:哪一支可以自由改,哪一支動了會牽動套件契約。 本案就是產這張圖。只分析、不動任何程式。

第 1 棒(module_frame,81 檔/10,942 行)已完成,三個主要發現:

  1. 真業務比殼多,不是各半。 行數比 50.8%(真業務):35.0%(轉接殼):14.1%(接線與型別), 真業務約為殼的 1.45 倍。FR-102 粗估的「約各半」方向對、比例要修正。

  2. 沒有兩份真相。 原本懷疑六支 v2 子物件殼(party/components/inventory/leveraged/ system_characteristic/ssp_resources)與 app/oscal/ 同名五支各寫一份欄位對應表—— 實查發現 oscal 那邊是直接 import module_frame 這邊的對應表, module_frame 是定義處、oscal 是引用者。連 app/flow_control/ 也在借同一份。 這不是重複,是刻意的單一真相。

  3. 沒有死碼,但有五個對外不可達的端點。 81 支全部通過四層查證 (模組內 import/模組外 import/DI 註冊/route 註冊)。 最接近的是兩個 route 註冊被註解掉的 Resource 類別, 以及三個前端只在「被註解掉的程式碼」裡呼叫的下載/匯入端點。 證據已備齊,但清不清屬異動,本案只記錄。

逐檔分類表、route 清單、統計數字、可立刻收的清單 → module-frame-classification.md

第 2 棒(oscal,163 檔/19,996 行)已完成,三個主要發現:

  1. 這是主專案最大的一塊自有資產,不是套件的殼。 行數比 57.5%(真業務 11,494): 23.8%(轉接殼 4,757):18.1%(接線與型別),真業務是殼的 2.4 倍,比 module_frame 那邊的 1.45 倍更懸殊。原因是它獨佔了「把客戶既有文件吃進系統」 那條線——Word 解析(2,027 行)與文件比對(1,384 行)合計 3,411 行零套件引用。

  2. 五支 SSP 子物件殼與 module_frame 那六支是同一份、不是兩份。 五支全部直接 import module_frame 那側的欄位對應表,檔頭都自陳「完全相同……故直接共用, 確保單一真相」。兩個模組的分類表在這一點上對得起來。

  3. 六支死碼、126 行,全在 api/ 層。 一支被註解掉的 route 檔,加上五支 互相引用成封閉圈的序列化器——它們用 marshmallow 的字串式 Nested 互相引用, grep import 抓不到,且與活著的同功能類別名字只差三個字母(SspSystemCharacteristic vs SystemCharacteristic),是清理時的地雷。另有兩條資源庫端點對外不可達。

逐檔分類表、61 條 route 清單、統計數字、可立刻收的清單 → oscal-classification.md

決策者裁示(2026-09-16):

  1. 六支殼的「空值哨兵」記債不動,只在檔頭補陷阱註解(已做)。
  2. module_frame 與 oscal 的依賴方向維持現狀、不反轉;FR-106 反轉稿留存暫不動。
  3. 三支殼與業務混住的千行大檔不拆。
  4. Excel 欄位契約去重落點在 module_frame 側(CM-1841 已做)。
§2

總進度表

棒次 範圍 卡號 狀態 產出
第 1 棒 module_frame 81 檔/10,942 行、45 條 route CM-1839 🟢 完成(2026-09-16,HEAD 98d756bb) module-frame-classification.md
第 2 棒 oscal 163 檔/19,996 行、61 條 route CM-1840 🟢 完成(2026-09-16,HEAD 84b8426a) oscal-classification.md
第 3 棒 清理棒:兩模組不可達端點/死序列化器/FE 殘骸,Excel 欄位契約去重 CM-1841 ✅ 驗收通過(2026-09-16,BE 22c65991+7b017144+b1a80773、FE ea74482) 六條端點下架、五支死序列化器移除、Excel 欄位契約單一定義

兩個模組並排看(細節都在兩份分類表裡):

module_frame oscal
規模 81 檔/10,942 行、45 條 route 163 檔/19,996 行、61 條 route
真業務 5,563 行(50.8%) 11,494 行(57.5%)
轉接殼 3,831 行(35.0%) 4,757 行(23.8%)
真業務:殼 1.45 : 1 2.4 : 1
死碼 0 6 檔/126 行(全在 api/)
守門位置 route 層 decorator 為主 app service 層為主(資源域,要先反查專案才知道判誰)

依賴方向是 oscal → module_frame:跨模組 import 共 12 處,其中 9 處是 oscal 引用 module_frame(五支 SSP 子物件的欄位對應表、Excel sheet 定義), 反向只有 3 處。這與規模直覺相反(oscal 兩倍大、概念上更底層),已列為待裁第一題。

§3

需求討論紀錄

為什麼切成兩棒

兩支共 244 檔/30,938 行,一棒做不完。按模組切的理由是依賴方向: v2 子物件殼的欄位對應表在 module_frame 這邊定義,oscal 那邊是引用者—— 先做定義處,第 2 棒才有對照基準。

四類的判準(兩棒共用)

看依賴方向,不看檔名也不看直覺:

  • 真業務:主要邏輯自己寫,jedi_* 引用少或零;刪掉這支功能就消失。
  • 轉接殼:主體是「開交易 → 呼叫套件 → 欄位對應」,商業規則在套件。 ⚠️ 殼不等於可刪——它是「主專案回答套件問題」的必要接線(FR-080 定的終局形狀的一半)。 判定只標性質,並另外標「這支殼有沒有帶主專案獨有的規則」(權限守門、產品擴充欄位、多租戶); 帶了就是「殼+業務」,不能整支說成殼。
  • 共用設施:沒有 route、沒有自己的業務語意,是給別的模組或別的檔用的(DTO、序列化器、mapper、port)。
  • 死碼:四層全零才算(模組內 import/模組外 import/DI 註冊/route 註冊)。單層零命中不算。 發現疑似死碼只記錄不刪。
  • 還看不準:允許存在,但要寫「卡在哪、要看什麼才判得了」。

判「前端有沒有在用」要查四層

FR-100 與 FR-102 都踩過「前三層全空、第四層才發現是活的」:

  1. 前端 API 常數表 src/config/api/api.js
  2. 前端路由 src/config/router/index.js
  3. 資料庫的選單表 public.ui_routes
  4. 非瀏覽器的呼叫者——排程腳本、bin/、scripts/、端對端測試

第 1 棒在第 2 層又抓到一種新情況:常數有定義、但呼叫它的前端程式碼整段被註解掉。 這種要標成「疑似死」而不是「活」,光看 grep 命中數會誤判。

第 1 棒實查修正的既有紀錄

原本記載 實查結果
派工卡寫 module_frame 對外 route「43 條」 45 條(AST 實跑)。FR-102 總表 A 寫的就是 45,是卡片轉述時的筆誤
FR-102 寫最大兩支 service「全自寫」 1,008 行那支成立;1,500 行那支有 18 個套件引用(都是讀資料的來源),準確說法是「主體邏輯自寫、資料來源在套件」
FR-102 寫 generator.py 零套件引用 成立,且整包 excel_template/ 7 檔 1,656 行全都零套件引用
FR-102 寫 party/components/inventory/leveraged「同型」 成立,但同型的是六支不是三支(要補 system_characteristic 與 ssp_resources)

FR-102 對本模組的五句證據沒有一句已失效,兩句需要精確化、一句需要補充。

決策者對「解耦」的回饋與 FR-106

決策者看完兩棒分類表後說:「我覺得解偶還是沒有做的很好」。首腦據此寫了 FR-106 反轉稿(把 SSP 對應層從 module_frame 歸位 oscal,反轉依賴方向)。 決策者裁:「先不動,與主專案牽連太多,後續釐清再處理,紀錄留著」—— 反轉只解模組間方向,不解三支千行大檔殼與業務混住的糾纏,等新功能帶真實需求再回頭。

待決策者裁的四件事(皆已裁,裁示見上方「決策者裁示」)

兩個模組都盤完後,有四件遇到「兩種合理判法」而不自選的:

  1. 🔴 module_frame 與 oscal 誰是誰的上游。 現況是大模組依賴小模組 (oscal 引用 module_frame 9 處),與直覺相反。維持現狀零風險,但每個新來的人 都會想反過來改、而改了會壞;反轉則要動 11 支 service。 判斷需要的資訊:這兩個模組未來會不會有一個進套件——若 module_frame(資源庫) 要進,現在的方向剛好;若 oscal 要進,方向就是錯的。

  2. 殼裡的「空值哨兵」要不要退回套件解。 套件的資料表有幾個欄位不准留空, 但前端允許使用者留空,主專案這一側就塞了假值進去(例如全零的識別碼)、 讀出來時再換回空白。要嘛維持現狀(零風險,但這個「看不見的約定」任何第三個 寫入者不知道就會寫壞資料),要嘛請套件把欄位改成可留空(要動套件的資料表定義與 migration)。 在專案 SSP 那一側問題更嚴重一級——範本是內部資料,專案 SSP 是租戶的實際稽核資料, 假值會跟著匯出進客戶的系統安全計畫書。 判斷需要的資訊:jedi_oscal_v2 除本專案外還有沒有別的使用者。

  3. Excel 欄位契約那約 25 行逐字重複的常數要抽到哪裡。 三個候選落點 (common//留在 module_frame 讓 oscal 引用/進套件)各有代價。 與第 1 題綁死,建議一起裁。 另外兩邊的變數名不一致 (HEADER_LABELS vs EXPECTED_HEADERS)比重複本身更麻煩——grep 同一個名字只會找到一半。

  4. 那些對外不可達的東西要不要清。 兩個模組合計:module_frame 五個端點、 oscal 六支死碼(126 行)+兩條資源庫端點,另有三條前端指向不存在後端端點的殘骸。 證據都已備齊(四層查證結果寫在各自分類表的 §4), 但「清」屬異動,本案只分析不動程式。其中資源庫的「發布」端點風險最高—— 它是公版治理的唯一途徑,收了等於砍功能。

§4

文件

以下全部由 build 掃資料夾產生,新增檔案重 build 即自動出現。標題連結指向渲染後的 HTML,md 連向源檔。

其他文件

文件 類型 標題 最後更新
module-frame-classification / md 文件 module_frame 逐檔分類 2026-09-16
oscal-classification / md 文件 oscal 逐檔分類 2026-09-16
§5

Notion 卡

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

關係 卡號 標題 狀態
母案 CM-1838 FR-105 oscal/module_frame 逐檔分類 —