---
title: module_frame 逐檔分類
status: 🟢 **完成**（2026-09-16，實查 HEAD `98d756bb`）。81 檔全數分類，無跳過。
notion:
  parent: {id: CM-1839, title: "FR-105 第 1 棒 module_frame 逐檔分類——81 檔／10,942 行，v2 六支殼是否與 oscal 鏡像（只分析不動程式）", url: "https://app.notion.com/p/FR-105-1-module_frame-81-10-942-v2-oscal-3dd346da4cd0819c9ab5c3f7c520f1f8"}
relates: [FR-102, FR-100, FR-080, FR-069, FR-038, FR-036, FR-032]
---

# module_frame 逐檔分類

**實查日期**：2026-09-16 ｜ **當時 HEAD**：`98d756bb` ｜ **branch**：`feature/review`

> 🔴 **本表是某一天的快照，引用前先重驗。** 每個數字下方都附了產生它的指令，重跑一次比相信這份表安全。

---

## 一句話結論

`module_frame` **不是「真業務與轉接殼各半」，真業務比殼多**：81 支檔案裡，
**5,563 行（50.8%）是主專案自己寫的業務**（Excel 範本產生器、YAML／Excel 匯入、BPMN 流程編排），
**3,831 行（35.0%）是薄薄一層的欄位對應殼**（把前端的欄位名翻成 OSCAL 的形狀、再交給 `jedi_oscal_v2` 套件存），
其餘 1,548 行（14.1%）是序列化器、DTO、mapper 這類接線與型別。

而那六支被懷疑「與 oscal 那邊各寫一份」的殼，**實查後發現沒有兩份真相**——
oscal 那五支是**直接 import 這邊的對應表**來用，這邊才是唯一的定義處。

---

## ① 現況盤點

### 四層規模

| 目錄 | 檔數 | 行數 |
|---|---:|---:|
| `api/module_frame` | 25 | 2,763 |
| `app/module_frame` | 31 | 7,416 |
| `domain/module_frame` | 12 | 348 |
| `infra/module_frame` | 13 | 415 |
| **合計** | **81** | **10,942** |

另有 `di_containers/module_frame/module_frame_containers.py`（不在四層內，判 DI 註冊時要看）。

<details>
<summary>怎麼重跑</summary>

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

用 `git ls-files` 不用 `find`——`find` 會撈到 `__pycache__` 與未入版控的殘留。

⚠️ **行數會因「怎麼數」差 9 行**：`wc -l` 數的是換行符個數，
Python 的 `sum(1 for _ in open(f))` 數的是行物件個數——檔尾沒有換行符時後者多算一行。
本文全篇用 `wc -l`（10,942）；FR-102 總表 A 用的是 Python 數法（10,951）。
**兩個數字都對，差的是數法不是程式碼**，比對時要用同一種。

</details>

### 對外 route：45 條

用 AST 數（不是 regex——`add_resource(` 常換行寫，regex 會漏抓）。
45 條全部掛在 blueprint `module-frame`、前綴 `/api/1.0`。

<details>
<summary>怎麼重跑</summary>

FR-102 README「怎麼重跑」③ 的腳本，把掃描目錄改成 `api/module_frame`。

</details>

> ⚠️ **與派工卡上的「43 條」不符——實際是 45 條。** 這不是新增的 route：
> FR-102 總表 A 該列（`README.md:58`）寫的就是 45，卡片轉述時誤植為 43。
> 45 是本棒 AST 實跑的結果。

另有 **2 條被註解掉不計入**（`grep -rn "^\s*#.*add_resource" api/module_frame/`）：

| 被註解的 route | 註解說明 |
|---|---|
| `ModuleFramesRoute` → `/module-frames` | `[FR-038 DEAD-V1｜2026-06-18 停用待清理]` 清單已改走 resource-library；FE 無 caller |
| `ModuleFrameStartRoute` → `/module-frame/start` | `[FR-038 DEAD-V1｜2026-06-18 停用待清理]` 建立走 `/module-frame/clone`；FE 無 caller |

### route 清單與前端四層查結果

四層＝① FE `src/config/api/api.js` 常數 ② FE 路由 `src/config/router/index.js`
③ DB `public.ui_routes`（DEV `localhost:5432/guidant_ai_dev`）④ 非瀏覽器呼叫者（`scripts/`、e2e）。

**第三層先講清楚**：`ui_routes` 全庫只有 **1 列**與本模組相關——
`id=24, name='module-frame', url='/module-frame/module-frame', enable=1`。
它是**選單項**不是 API，管的是「左側選單看不看得到資源庫」這一件事；
45 條 API route 不會逐條出現在 `ui_routes` 裡。因此下表第三層對所有 route 都是同一個答案
（該選單開著＝整組頁面可達），逐條列會變成 45 個一樣的格子，故不設欄，在此統一交代。

```bash
PGPASSWORD=<查 .env> psql -h localhost -p 5432 -U cm_app -d guidant_ai_dev \
  -c "SELECT id, name, url, enable FROM public.ui_routes WHERE name ILIKE '%module%' OR url ILIKE '%module%';"
```

FE 路由第二層共 9 條 `/module-frame/*`（`router/index.js:412-491`）：
`module-frame`／`module-frame-items`／`:uid/template-edit`／`import-docx`／`:uid/import-docx`／
`import-excel`／`:uid/import-excel`／`import-excel/preview/:parseUid`。

| # | route 路徑 | 吃哪支 service | FE①常數 | FE②路由 | ④非瀏覽器 | 判定 |
|---|---|---|---|---|---|---|
| 1 | `GET/POST /module-frame`、`GET/PUT/DELETE /module-frame/<uid>` | `ModuleFrameService` | `MODULE_FRAME` | ✅ | `scripts/e2e_test_module_frame_with_docx.py:174` POST／e2e `module-frame-import.steps.js:211` DELETE | **活** |
| 2 | `GET/POST/PUT/DELETE /module-frame/item[/<uid>]` | `ModuleFrameItemService` | `MODULE_FRAME_ITEM` | ✅ `ModuleFrameItems.vue:182,196,218,245` | — | **活** |
| 3 | `PUT /module-frame/item/xml/<uid>` | `ModuleFrameItemService` | `MODULE_FRAME_ITEM_XML` | ✅ `WorkflowSetupEditor.vue:391,406,416` | — | **活** |
| 4 | `POST /module-frame/clone` | `ModuleFrameService` | `MODULE_FRAME_CLONE` | ✅ `ModuleFrame.vue:634` | — | **活** |
| 5 | `GET /module-frames/menu` | `ModuleFrameService` | `MODULE_FRAME_MENU` | ✅ `menuStore.js:99,233` | — | **活** |
| 6 | `GET /module-frame/download/template` | （route 層直接 `send_from_directory`） | `MODULE_FRAME_TEMPLATE_DOWNLOAD` | ⚠️ **僅在被註解的程式碼中**（`ModuleFrame.vue:668`） | — | **疑似死** — 見 ④ |
| 7 | `GET /module-frame/download/yaml-template` | （同上） | `MODULE_FRAME_YAML_TEMPLATE_DOWNLOAD` | ⚠️ **僅在被註解的程式碼中**（`ModuleFrame.vue:672`） | — | **疑似死** — 見 ④ |
| 8 | `POST /module-frame/import/template` | `ModuleFrameImportService` | `MODULE_FRAME_IMPORT_TEMPLATE` | ⚠️ **僅在被註解的程式碼中**（`ModuleFrame.vue:737`） | — | **疑似死** — 見 ④ |
| 9 | `POST /module-frame/import/verify` | `ModuleFrameImportService` | `MODULE_FRAME_IMPORT_VERIFY` | ✅ `ModuleFrame.vue:1019` | — | **活** |
| 10 | `POST /module-frame/import` | `ModuleFrameImportService` | `MODULE_FRAME_IMPORT` | ✅ `ModuleFrame.vue:1050` | — | **活** |
| 11 | `POST /module-frame/import/yaml` | `ModuleFrameImportService` | `MODULE_FRAME_IMPORT_YAML` | ✅ `ModuleFrame.vue:847` | — | **活** |
| 12 | `GET/PUT /module-frame/<uid>/system-characteristic` | `ModuleFrameSystemCharacteristicService` | 有 | ✅ `SspBasicSection.vue` | — | **活** |
| 13-14 | `/module-frame/<uid>/components[/<item_uid>]` | `ModuleFrameComponentsService` | 有 | ✅ `SspComponentsLeveragedInventoryTab.vue` | — | **活** |
| 15-16 | `/module-frame/<uid>/inventory[/<item_uid>]` | `ModuleFrameInventoryService` | 有 | ✅ 同上 | e2e `module-frame-party-factory` | **活** |
| 17-18 | `/module-frame/<uid>/leveraged[/<item_uid>]` | `ModuleFrameLeveragedService` | 有 | ✅ `SspLeveragedSection.vue` | e2e 同上 | **活** |
| 19-20 | `/module-frame/<uid>/parties[/<party_uid>]` | `ModuleFramePartyService` | 有 | ✅ `ModuleFramePartiesPanel.vue` | e2e 同上 | **活** |
| 21-23 | `/module-frame/<uid>/ssp-resources`、`/items[/<item_uid>]` | `ModuleFrameSspResourcesService` | 有 | ✅ `ModuleFrameSspDevicesPanel.vue`／`…InfoSystemsPanel.vue` | — | **活** |
| 24-25 | `/module-frame/<uid>/control-defaults[/<ci>]` | `ModuleFrameControlDefaultService` | `MODULE_FRAME_CONTROL_DEFAULT(S)` | ✅ | e2e `module-frame-defaults.feature` | **活** |
| 26-27 | `/module-frame/<uid>/objective-defaults[/<od_uid>]` | `ModuleFrameControlObjectiveDefaultService` | 有 | ✅ | e2e 同上 | **活** |
| 28-29 | `/module-frame/<uid>/reference-documents[/<doc_uid>]` | `ModuleFrameReferenceDocumentService` | 有 | ✅ | — | **活** |
| 30-33 | control／objective 的 `…/reference-documents[/<doc_uid>]` attach／detach 四條 | 同上 | 有 | ✅ | — | **活** |
| 34-39 | `/module-frame/<uid>/control-defaults/` 的 `export`／`template/download`／`import/template`／`import/verify`／`import/validate`／`import` | `ModuleFrameTemplateImportService` | `MODULE_FRAME_TEMPLATE_*` 六個 | ✅ | — | **活** |
| 40 | `GET /module-frame/<uid>/ssp-import-template` | `SspImportTemplateAppService` | `MODULE_FRAME_SSP_IMPORT_TEMPLATE` | ✅ | — | **活** |
| 41 | `GET /ssp/<ssp_uid>/excel-template` | 同上 | `SSP_EXCEL_TEMPLATE` | ✅ | — | **活** |
| 42 | `GET /ssp-import-template` | 同上 | `SSP_IMPORT_TEMPLATE_BY_FRAMEWORK_VERSION` | ✅ | — | **活** |
| 43 | `GET /module-frame/<uid>/ssp-export` | `SspExportAppService`（**oscal 的**） | `MODULE_FRAME_SSP_DOCUMENT_EXPORT` | ✅ | — | **活** |

> 表列 43 列是把成對的 list／item route 併列顯示，展開後即 45 條路徑。

### 這模組自己持有的表

`compliance.module_frames` + `compliance.module_frames_trans`（多語系翻譯表）兩張，
ORM 在 `infra/module_frame/models/`。模組所有其他資料——控制項預設值、AO 預設值、
程序書池、元件、資產清冊、外部授權、系統特性、參與方——**都不在這兩張表**，
而是存在該 module_frame 的**樣板 SSP**（`module_frames.template_ssp_id` → `oscal.ssps`）底下，
由 `jedi_oscal_v2` 的 `SspService` 負責讀寫。這是本模組「殼」那一半的由來。

---

## ② 逐檔分類表

### 四類的判準（依賴方向，不是檔名）

- **真業務**：主要邏輯自己寫，`jedi_*` 引用少或零；刪掉這支功能就消失。
- **轉接殼**：主體是「開 `@transaction` → 呼叫套件 service → 欄位對應」，商業規則在套件。
  **殼不等於可刪**——它是「主專案回答套件問題」的必要接線。要另外標「有沒有帶主專案獨有規則」。
- **共用設施**：沒有 route、沒有自己的業務語意，是給別的模組或別的檔用的（DTO、序列化器、error code、port、mapper）。
- **死碼**：全 repo 零 import、零 route 註冊、零 DI 註冊（四層都要查才算）。
- **還看不準**：允許存在，要寫卡在哪。

---

### `api/module_frame`（25 檔／2,763 行）

| 檔 | 行 | 類別 | 依賴證據 | 殼帶主專案規則？ |
|---|---:|---|---|---|
| `__init__.py` | 263 | **共用設施** | blueprint 組裝；21 個 host import（全是自己模組的 route 類別），jedi=0。零 DB、零業務 | — |
| `routes/__init__.py` | 0 | **共用設施** | 空 `__init__` | — |
| `routes/module_frame_route.py` | 144 | **轉接殼** | 呼叫 `ModuleFrameService`；jedi=2（`get_user_context`／`return_response`）。`ModuleFramesRoute`（44-62）與 `ModuleFrameStartRoute`（113-131）兩個類別**已無 route 註冊**（`__init__.py:88,93` 被註解） | ✅ 帶 — `@require_capability("module-frame.create/update/delete")` 三處守門 |
| `routes/module_frame_item_route.py` | 105 | **轉接殼** | 呼叫 `ModuleFrameItemService`；jedi=2 | ✅ 帶 — `require_capability` 三處 |
| `routes/module_frame_import_route.py` | 162 | **真業務**（殼＋業務混住） | jedi=4；**route 層自己用 pandas 讀 Excel、比對 7 欄 header、比不上就 `raise ConflictError`**（:57-77），不是純轉呼叫 | ✅ 帶 — `require_capability` ＋ header 驗證是產品規則 |
| `routes/module_frame_control_default_route.py` | 139 | **轉接殼** | 呼叫 `ModuleFrameControlDefaultService`；serializer dump | ✅ 帶 — `require_capability` |
| `routes/module_frame_control_objective_default_route.py` | 147 | **轉接殼** | 同上，吃 objective service | ✅ 帶 — `require_capability` |
| `routes/module_frame_reference_document_route.py` | 281 | **轉接殼** | 6 個 Resource 類別（池 CRUD ＋ control／objective 兩組 attach/detach）全轉呼叫 `ModuleFrameReferenceDocumentService` | ✅ 帶 — `require_capability` |
| `routes/module_frame_template_import_route.py` | 260 | **轉接殼** | 6 個 Resource 轉呼叫 `ModuleFrameTemplateImportService`；blob 回傳走 `send_file` | ✅ 帶 — `require_capability` |
| `routes/ssp_import_template_route.py` | 169 | **轉接殼** | 3 個 Resource 轉呼叫 `SspImportTemplateAppService`；jedi=1 | ✅ 帶 — `require_capability` |
| `routes/module_frame_system_characteristic_route.py` | 56 | **轉接殼** | 轉呼叫；service 從 `oscal_container` 注入（不是自己模組的 container） | ✅ 帶 — `require_capability` |
| `routes/module_frame_components_route.py` | 78 | **轉接殼** | 同上，`Containers.oscal_container.module_frame_components_service` | ✅ 帶 — `require_capability` |
| `routes/module_frame_inventory_route.py` | 78 | **轉接殼** | 同上 | ✅ 帶 — `require_capability` |
| `routes/module_frame_leveraged_route.py` | 91 | **轉接殼** | 同上 | ✅ 帶 — `require_capability` |
| `routes/module_frame_party_route.py` | 95 | **轉接殼** | 同上，多一層 marshmallow serializer | ✅ 帶 — `require_capability` |
| `routes/module_frame_ssp_resources_route.py` | 99 | **轉接殼** | 同上 | ✅ 帶 — `require_capability` |
| `routes/mf_ssp_export_route.py` | 71 | **轉接殼** | 呼叫的是 **oscal 的** `SspExportAppService`（`app/oscal/service/export/`），本模組零實作 | ✅ 帶 — `require_capability` ＋ RFC 5987 中文檔名編碼（`_send_attachment`） |
| `serializers/__init__.py` | 0 | **共用設施** | 空 | — |
| `serializers/module_frame.py` | 183 | **共用設施** | 9 個 marshmallow Schema；host=2（引 oscal 的 profile／framework schema），jedi=1 | — |
| `serializers/module_frame_control_default.py` | 46 | **共用設施** | 純 Schema，零 import | — |
| `serializers/module_frame_control_objective_default.py` | 49 | **共用設施** | 同上 | — |
| `serializers/module_frame_item.py` | 38 | **共用設施** | 同上 | — |
| `serializers/module_frame_party.py` | 43 | **共用設施** | 同上 | — |
| `serializers/module_frame_reference_document.py` | 61 | **共用設施** | 同上 | — |
| `serializers/module_frame_template_import.py` | 105 | **共用設施** | 同上 | — |

---

### `app/module_frame`（31 檔／7,416 行）

#### service/ — 13 支，模組的重心

| 檔 | 行 | 類別 | 依賴證據 | 殼帶主專案規則？ |
|---|---:|---|---|---|
| `ssp_import_template_app_service.py` | 1,500 | **真業務**（含殼段，見下方拆解） | jedi=18／host=4。這 18 個 jedi import **全是讀資料的來源**（`jedi_iam` 使用者／單位、`jedi_asset` 設備／資訊系統、`jedi_oscal_v2` SSP／profile／catalog、`jedi_compliance_audit` 專案擴充），不是把工作交出去；檔內 70+ 個私有方法做的是「把八個來源的資料揉成 Excel 九張表的列」 | — |
| `module_frame_template_import_service.py` | 1,008 | **真業務** | jedi=4／host=1。openpyxl 手刻 9 欄 Excel（建 workbook、套配色邊框、下拉、隱藏 A 欄）＋解析＋逐列驗證＋批次 upsert。套件只提供 IR／statement 兩個 entity 型別 | — |
| `module_frame_import_service.py` | 415 | **真業務** | jedi=4／host=5。YAML／Excel → module_frame 結構的遞迴轉換（`save_import_module_frame_data_recursive`）＋BPMN 產生（`create_module_frame_bpmn_format`）＋HTML 標籤剝除。核心是自寫的樹狀遞迴 | — |
| `module_frame_party_service.py` | 323 | **轉接殼＋業務** | jedi=6／host=2。主體是 FE 欄位 ↔ OSCAL props 對應表（檔頭 10 行對照表），寫入委派 `SspService` | 🔴 **帶三條**：① **產品擴充軟參照** `matched-user-id`／`matched-org-unit-id`（OSCAL parties 無此核心欄）② **同名去重**（同 metadata 下同 type+name 擋下，raise Conflict；2026-06-24 從 idempotent 改成 raise）③ **暱稱 enrich**（`_enrich_user_org_names` 反查 `jedi_iam` user／org_unit 補名字）。另有 legacy 容忍：`role` prop 舊值 fallback |
| `module_frame_service.py` | 304 | **真業務＋殼** | jedi=11／host=9，模組內依賴最雜。`clone_module_frame`（129-224，96 行）是**自寫的 BPMN 深拷貝**——複製主流程、逐個複製子流程、把主流程 XML 裡的 called_element_id 全部換成新 id；`start_project_from_module_frame`（226-265）編排啟動專案。其餘 CRUD 是薄殼轉 domain service | ✅ 帶 — `assert_scope_writable`（租戶 scope 守門）；BPMN clone 是主專案自有邏輯 |
| `module_frame_control_objective_default_service.py` | 269 | **轉接殼＋業務** | jedi=4／host=3。AO 預設值落樣板 SSP 的 statement props；lazy upsert | ✅ 帶 — ① AO∈control 守門（`_is_objective_in_control`）② 暱稱 enrich ③ 「delete=清空值不刪 IR」的產品語意 |
| `module_frame_reference_document_service.py` | 257 | **轉接殼＋業務** | jedi=4／host=4。程序書池 CRUD ＋跨控制項 mapping，落 `jedi_compliance_audit` 的 `ssp_reference_documents` | ✅ 帶 — ① 檔名 enrich（`_enrich_file_metadata`，`ssp_reference_documents` 無 title 欄，FE 顯示靠這層補）② mapping context_type 的 `control_implementation`／`objective` 慣例 |
| `module_frame_control_default_service.py` | 240 | **轉接殼＋業務** | jedi=3／host=3。控制項預設值落樣板 SSP 的 IR props | 🔴 **帶** — **control∈profile 守門**（`_is_control_in_profile`，只擋新建不擋既有；檔頭記 2026-07-17 DEV 有 23 條 guard 加嚴前的歷史違規資料，刻意不動）＋暱稱 enrich |
| `module_frame_inventory_service.py` | 220 | **轉接殼＋業務** | jedi=3／host=3 | 🔴 **帶** — ① FR-032 軟參照欄 `asset-type`／`ref-type`／`ref-id`（產品擴充）② 同名去重 raise ③ **M2M 清空修正**：`_build_implemented` 一律回 `[]` 不回 `None`，繞開 `BaseRepositoryImpl.update()` 對 None silent-skip 的坑 ④ 暱稱 enrich |
| `module_frame_item_service.py` | 219 | **真業務** | jedi=5／host=2。BPMN Call Activity 的增刪改——`add_module_frame_item` 自己組子流程 BPMN、把 call activity 插回主流程 XML；`delete` 反向拆 | ✅ 帶 — `assert_scope_writable`（檔內註解說明「守門留這裡不是套件內：套件沒有 route 也不持有宿主的租戶語意」） |
| `module_frame_leveraged_service.py` | 211 | **轉接殼＋業務** | jedi=4／host=3 | 🔴 **帶** — ① 五個 FE 欄位走 props 擴充（v2 LA 無核心欄）② **NN 欄空值哨兵**：`party_uuid` 空落 NIL-UUID（uuid 型別吃不下 `""`）、`date_authorized` 空落 `date.today()`，讀回時 reshape 成 None ③ 同名去重 ④ 暱稱 enrich |
| `module_frame_components_service.py` | 211 | **轉接殼＋業務** | jedi=4／host=3 | ✅ 帶 — ① `protocol`／`port_ranges`／`security_auth` 三鍵＋LA 軟參照走 props ② 同名去重 raise ③ `build_leveraged_title_map`／`enrich_leveraged_titles` 兩個 module-level helper（批次查避 N+1）④ 暱稱 enrich |
| `module_frame_system_characteristic_service.py` | 186 | **轉接殼＋業務** | jedi=4／host=3 | ✅ 帶 — ① `target-type`／`owner-uid` 走 props ② **NN 欄安全預設**（首次 insert 時 5 個 NN 欄先給空字串／空陣列，避免部分 payload 撞 NOT NULL）③ 空 SC 時回 placeholder、PUT 首次 auto-create 的產品語意 ④ 暱稱 enrich |
| `module_frame_ssp_resources_service.py` | 101 | **轉接殼**（薄，真的只是轉呼叫） | jedi=2／host=2。**全部委派 `app/oscal/service/ssp_resources_context_service.py`**；自己只做 mf uid → `template_ssp_id` 反查 | ⚠️ 帶一點 — 缺 template SSP 時的兩種回法：`list` 回空陣列（合法狀態：剛建還沒匯入）、寫入 raise 412。這是產品的 UX 判斷 |
| `service/__init__.py` | 0 | **共用設施** | 空 | — |

##### `ssp_import_template_app_service.py` 的殼段／業務段拆解

卡片問「一支檔混了多少種事」。按方法區塊估算：

| 區段 | 約行數 | 性質 |
|---|---:|---|
| 兩張 OSCAL 中英對照常數表（`OSCAL_COMPONENT_TYPE_ZH` 15 條、`OSCAL_PARTY_ROLE_ZH` 10 條） | 35 | **業務**（產品的中文標籤，對齊 FE i18n 檔） |
| 3 支對外方法（`generate`／`generate_for_ssp`／`generate_by_framework_version`）＋入口解析 | 約 130 | **殼**（守門、取 mf、組 bundle、丟給 generator） |
| lookup 抓取（`_fetch_*_lookup`／`_fetch_*_lookup_helpers` 共 12 支） | 約 225 | **殼**（逐一去四個套件的 domain service 撈清單） |
| 資料預填（`_populate_filled_data*`／`_build_*_row`／`_build_parties`／`_build_components_v3`…） | 約 640 | **業務**（entity → Excel 列的欄位揉合，含 FR-032 device／info-system 標籤反查、LA title map、控制項樹壓平） |
| 控制項與 AO 組列（`_build_controls_*`／`_flatten_tree_to_rows`／`_load_existing_*_map`） | 約 300 | **業務**（catalog 樹壓平成列 ＋ 既有值 overlay） |
| 小工具（label 解析、檔名組裝、locale 判定、props 解析） | 約 170 | **共用設施**（同檔內用） |

粗分約 **殼 355 行／業務 975 行／工具 170 行**。這個拆法是按方法區塊估的，不是逐行計數。

#### excel_template/ — 7 檔／1,656 行，整包零 jedi 引用

`grep -rnE '^\s*(from|import) jedi_' app/module_frame/excel_template/ | wc -l` → **0**。
卡片要求實查確認，已確認。

| 檔 | 行 | 類別 | 依賴證據 |
|---|---:|---|---|
| `generator.py` | 652 | **真業務** | 純 openpyxl，零 jedi、零 DB。建 9 張資料 sheet ＋ 6 張隱藏 lookup sheet、header 黃底必填／灰底選填、DataValidation（inline enum 與 named range 兩種）。**被三個模組外的地方 import**：`ssp_import_template_app_service`、`scripts/archive/regenerate_reference_templates.py`、`test/test_excel_template_generator_required_fill.py` |
| `sheet_definitions.py` | 617 | **真業務**（亦是跨模組契約） | 零 import（只有 dataclass／StrEnum）。定義 `ColumnDef`／`SheetDef`／`LookupSource` 與 8 張 sheet 的欄位規格。**`app/oscal/service/excel_parser/sheet_handlers.py:43` 直接 import 這裡的 10 個 sheet 常數**——匯出端與匯入端共用同一份欄位定義 |
| `header_i18n.py` | 176 | **真業務** | 零 import。欄位標題的 zh_Hant_TW／en 對照 dict（檔頭寫明「第一版 hardcode，後續整合 babel `.po` 時改 `_resolve_header()` 內部即可」） |
| `lookup_builder.py` | 108 | **共用設施** | host=1（引 `sheet_definitions.LookupSource`）。建隱藏 lookup sheet ＋ 註冊 openpyxl `DefinedName` |
| `data_validation_builder.py` | 54 | **共用設施** | 零 import。兩支 helper：`build_enum_dv`（inline，超 255 字元拋 `EnumValuesTooLongError`）、`build_named_range_dv`。**`app/auth/service/user_import_template_app_service.py:28` 借用後者** |
| `styles.py` | 38 | **共用設施** | 只有 openpyxl style 常數。**`app/auth/…user_import_template_app_service.py:29` 借用三個常數** |
| `__init__.py` | 11 | **共用設施** | 只有 docstring |

#### dto/ 與 code/ — 8 檔／240 行

| 檔 | 行 | 類別 | 依賴證據 |
|---|---:|---|---|
| `dto/module_frame_dto.py` | 70 | **共用設施** | jedi=1（`WorkflowTemplateDTO`）／host=1。被 `module_frame_service` 用 |
| `dto/module_frame_reference_document_dto.py` | 57 | **共用設施** | 零 import dataclass |
| `dto/module_frame_control_default_dto.py` | 49 | **共用設施** | 零 import；另被 `test/test_api_module_frame_control_default_route.py:23` 用 |
| `dto/module_frame_control_objective_default_dto.py` | 47 | **共用設施** | 同上 |
| `dto/module_frame_menu_dto.py` | 34 | **共用設施** | host=1 |
| `code/module_frame_error_code.py` | 32 | **共用設施** | jedi=1（`BaseCode`）。13 個 error code；檔頭記 2026-07-23 與 `common/code/error_code.py` 撞號 9 組後改本檔側新號 |
| `__init__.py`／`dto/__init__.py`／`code/__init__.py` | 7／0／0 | **共用設施** | 空或只有 docstring |

---

### `domain/module_frame`（12 檔／348 行）

| 檔 | 行 | 類別 | 依賴證據 |
|---|---:|---|---|
| `entity/module_frame_entity.py` | 113 | **真業務** | 零 import。`ModuleFrameEntity`（24 個欄位）＋四個 JOIN 用的輕量 summary entity（framework／framework_version／catalog_group／catalog_control／profile）。**四個 summary entity 的存在理由**：列表頁要顯示框架與 profile 資訊，但不想把整個 oscal entity 拖進來 |
| `service/module_frame_domain_service.py` | 76 | **真業務** | jedi=3／host=5。9 支方法全是薄轉發到 repo，但 `verify_module_frame_exists_by_uid` 帶「不存在就 raise NotFound」的產品語意。**被 8 處模組外注入**：`oscal_containers` 8 處、`flow_control_containers:339`、`domain/oscal/service/ssp_context_resolver.py:80` |
| `dto/module_frame_dto.py` | 49 | **共用設施** | jedi=1／host=1。`ModuleFrameDTO`／`WorkflowDTO`／`JobDTO`／`ActionDTO`——**這是 YAML 匯入用的中介型別**，與 `app/module_frame/dto/module_frame_dto.py` 同名不同物（後者是 API response DTO） |
| `entity/module_frame_query_filter.py` | 37 | **共用設施** | 零 import。`ModuleFrameQueryEntity`。⚠️ `is_delete: Optional[int] = 0` 是**非-None 預設**——不明確傳就永遠帶 `is_delete=0` 的 WHERE（這裡是刻意的：軟刪除預設過濾掉已刪列） |
| `repository/module_frame.py` | 33 | **共用設施** | jedi=3。`IModuleFrameRepo` 抽象介面，7 支 abstract method |
| `entity/module_frame_menu_entity.py` | 23 | **共用設施** | 零 import |
| `code/module_frame_action_type_enum.py` | 15 | **共用設施** | 零 import。`ActionType`（5 值）＋`ParserAdapterType`（1 值）。**`ActionType` 被 `app/flow_engine/service/workflow_execution_service.py:12` 用**——本模組唯一被 flow_engine 使用的東西 |
| `repository/__init__.py`／`service/__init__.py` | 1／1 | **共用設施** | 一行 |
| `__init__.py`／`code/__init__.py`／`entity/__init__.py` | 0／0／0 | **共用設施** | 空 |

---

### `infra/module_frame`（13 檔／415 行）

| 檔 | 行 | 類別 | 依賴證據 |
|---|---:|---|---|
| `repository/module_frame_repo_impl.py` | 136 | **真業務** | jedi=3／host=8。繼承 `BaseRepositoryImpl`；含 module_frames ↔ oscal 跨 schema 的 JOIN 查詢與多語系 failover |
| `models/module_frame.py` | 91 | **真業務** | jedi=3／host=1。`ModuleFrame` ORM，18 欄。含 `scope`（FR-042 租戶 scope）、`template_ssp_id`（FR-036 跨 schema soft-ref，刻意不建 FK constraint）、兩個 `created/updated_user_name` hybrid_property。**被 `infra/flow_control/repository/flow_control_project_repo_impl.py:19` import**（跨模組唯一一處） |
| `adapter/yaml_to_module_frame_parser_adapter.py` | 65 | **真業務** | jedi=1／host=4。YAML → `ModuleFrameDTO` 的遞迴解析 |
| `mapper/module_frame_mapper.py` | 41 | **共用設施** | host=2。entity ↔ model |
| `models/module_frame_trans.py` | 29 | **真業務** | jedi=1。多語系翻譯表 ORM，`ondelete='CASCADE'` |
| `mapper/module_frame_menu_mapper.py` | 23 | **共用設施** | host=2 |
| `adapter/parser_adapter_factory.py` | 17 | **共用設施** | jedi=1／host=4。依 `ParserAdapterType` 回對應 adapter；DI 註冊為 Singleton（`module_frame_containers.py:120`），注入 `module_frame_import_service`（:129），實際被呼叫於 `module_frame_import_service.py:254`。⚠️ **工廠只有一個分支**（YAML），非 YAML 一律拋 `BadRequestError` |
| `ports/parser_port.py` | 9 | **共用設施** | host=1。`ParserPort` ABC，單一 abstract method |
| `models/__init__.py` | 4 | **共用設施** | re-export 兩個 model |
| `__init__.py`／`adapter/__init__.py`／`mapper/__init__.py`／`repository/__init__.py` | 0×4 | **共用設施** | 空 |

---

## ③ 分類統計

分類單位是檔。混住兩種性質的檔（如 `module_frame_party_service.py` 是殼＋業務、
`module_frame_service.py` 是業務＋殼）按**主體**歸類，另在 ② 表的證據欄註明混住情形。

| 類別 | 檔數 | 佔比 | 行數 | 佔比 |
|---|---:|---:|---:|---:|
| **真業務** | 15 | 18.5% | 5,563 | 50.8% |
| **轉接殼** | 23 | 28.4% | 3,831 | 35.0% |
| **共用設施** | 43 | 53.1% | 1,548 | 14.1% |
| **死碼** | 0 | 0% | 0 | 0% |
| **還看不準** | 0 | 0% | 0 | 0% |
| **合計** | **81** | 100% | **10,942** | 100% |

> **共用設施佔過半檔數但只佔 14.1% 行數**——因為裡面有 16 支空的或只有一兩行的
> `__init__.py`，加上一批純 dataclass 的 DTO 與 mapper。**看行數比看檔數準。**

<details>
<summary>怎麼重跑統計</summary>

把 ② 表的每一列（檔路徑 → 類別）餵進下面這段，行數用 `wc -l` 現算：

```bash
# 逐檔行數（分類請照 ② 表）
git ls-files api/module_frame app/module_frame domain/module_frame infra/module_frame \
  | grep '\.py$' | while read f; do printf '%s\t%s\n' "$(wc -l < "$f"|tr -d ' ')" "$f"; done
```

行數合計必須等於 10,942，檔數必須等於 81；對不上就是分類有漏或重複。

</details>

**按層分布**：

| 層 | 真業務 | 轉接殼 | 共用設施 |
|---|---:|---:|---:|
| `api/` | 1 檔／162 行 | 14 檔／1,813 行 | 10 檔／788 行 |
| `app/` | 8 檔／4,891 行 | 9 檔／2,018 行 | 14 檔／507 行 |
| `domain/` | 2 檔／189 行 | 0 | 10 檔／159 行 |
| `infra/` | 4 檔／321 行 | 0 | 9 檔／94 行 |

三件事從這張表看得出來：

1. **殼全部集中在 `api/` 與 `app/` 兩層**，`domain/` 與 `infra/` 零殼——
   代表這模組的「自持資料那一半」（兩張表的 entity／repo／model）是乾淨的主專案資產，
   混住只發生在對外那兩層。
2. **`app/` 一層就佔了真業務的 88%（4,891／5,563 行）**，其中光三支大檔
   （`ssp_import_template` 1,500、`template_import` 1,008、`import_service` 415）就 2,923 行。
3. **`api/` 的 14 支殼共 1,813 行**是典型的 route 轉呼叫層，每支 56～281 行，沒有異常肥大的。

**對 FR-102 那句「真業務與 v2 轉接殼約各半」的回答**：以行數算是
**真業務 5,563 ／ 轉接殼 3,831 ＝ 1.45 : 1**，**真業務明顯多於殼，不是各半**。
FR-102 該句是模組層級的粗估，方向（兩種都有、混住）對，比例要修正。

---

## ④ 可以立刻收的

> 判準是「說得出證據才叫可收」。下面每一項都附了怎麼驗它真的沒人用。

### 4.1 兩個已無 route 註冊的 Resource 類別

`api/module_frame/routes/module_frame_route.py` 裡的 `ModuleFramesRoute`（第 44-62 行）
與 `ModuleFrameStartRoute`（第 113-131 行），以及 `__init__.py:14` 對它們的 import。

**四層查證結果**：

| 層 | 結果 |
|---|---|
| route 註冊 | ❌ `__init__.py:88` 與 `:93` 兩行 `add_resource` 皆被註解，註解標 `[FR-038 DEAD-V1｜2026-06-18 停用待清理]` |
| 模組內 import | 只有 `__init__.py:14` 那一行（就是為了那兩行被註解的 `add_resource` 而留） |
| 模組外 import | 0（`grep -rn "ModuleFramesRoute\|ModuleFrameStartRoute" --include='*.py' .` 只命中定義本身、`__init__.py` 那行 import、兩行註解，以及 `docs/api/module-frame/generate_docx.py:40` 的一張圖說文字） |
| DI 註冊 | 不適用（Resource 類別不進 DI） |

**怎麼驗**：
```bash
grep -rn "ModuleFramesRoute\|ModuleFrameStartRoute" --include='*.py' . | grep -v __pycache__
grep -rn "/module-frames\b\|/module-frame/start" ~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/
```

⚠️ **但它們背後的 service 方法不能一起收**：
- `ModuleFramesRoute` 吃的 `get_module_frames_and_pager` 是 `ModuleFrameDomainService`
  也有的同名方法，而 domain 那支被 repo 層用著。
- `ModuleFrameStartRoute` 吃的 `start_project_from_module_frame`（`module_frame_service.py:226-265`）
  **確實只有這個被註解的 route 在呼叫**——但它裡面編排的「啟動專案 → 建摘要報告」是完整功能，
  砍掉等於砍功能。**列在此處只是標出「目前對外不可達」，不是建議砍**。

### 4.2 兩支只在被註解的前端程式碼中出現的下載端點

`GET /module-frame/download/template` 與 `GET /module-frame/download/yaml-template`
（`module_frame_import_route.py` 的 `ModuleFrameTemplateDownloadRoute`／`ModuleFrameYamlTemplateDownloadRoute`）。

**四層查證結果**：

| 層 | 結果 |
|---|---|
| FE① `api.js` 常數 | ✅ 有定義（`MODULE_FRAME_TEMPLATE_DOWNLOAD:154`、`MODULE_FRAME_YAML_TEMPLATE_DOWNLOAD:155`） |
| FE② 實際呼叫 | ⚠️ **只出現在被註解掉的函式裡**：`ModuleFrame.vue:668`（`// const downloadExcelTemplate = () => {`）與 `:672`（`// const downloadYamlTemplate`） |
| ③ `ui_routes` | 不適用（不是選單） |
| ④ 非瀏覽器 | 0 命中（`grep -rn "download/template\|download/yaml-template" bin/ scripts/` 與 e2e repo 皆空） |

同組還有 `POST /module-frame/import/template`（`ModuleFrameImportTemplateRoute`），
FE 的 `importFromExcel()` 整支被註解（`ModuleFrame.vue:737`）。
**注意這三支是同一組**：下載 Excel 範本 → 填 → 上傳匯入，前端把整條流程註解掉了。
但同檔的 `import/verify`／`import`／`import/yaml` 三支**仍在用**（`:1019`／`:1050`／`:847`），
所以 `ModuleFrameImportService` 這支 service **不可整支收**。

**怎麼驗**：
```bash
cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
grep -rn "MODULE_FRAME_TEMPLATE_DOWNLOAD\|MODULE_FRAME_YAML_TEMPLATE_DOWNLOAD\|MODULE_FRAME_IMPORT_TEMPLATE\b" src/
# 三處命中全部前綴 "//"
```

⚠️ 這兩支 route 讀的是 `GLOBAL_DOWNLOAD_DIR` 下的 `audit_frame_template_v1.xlsx`／`.yaml`
兩個隨版檔案；收 route 時要一併確認那兩個檔還有沒有別的用途。

### 4.3 沒有找到的東西（如實記錄）

卡片點名的兩個「可以立刻收」候選，實查後**都不成立**：

**(a) 六支 v2 殼與 oscal 五支「兩份真相」——不成立。**
oscal 那五支**直接 import module_frame 這邊的對應表**，不是各寫一份：

| oscal 的檔 | 從 module_frame import 了什麼 |
|---|---|
| `ssp_components_app_service.py:28,31` | `ModuleFrameComponentsService as _CompMap`、`_build_props`、`build_leveraged_title_map`、`enrich_leveraged_titles` |
| `ssp_inventory_items_app_service.py:27` | `ModuleFrameInventoryService as _InvMap`、`_build_props`、`_build_implemented` |
| `ssp_leveraged_app_service.py:37` | `ModuleFrameLeveragedService as _LaMap`、`_NIL_PARTY_UUID`、`_build_props`、`_parse_date` |
| `ssp_party_app_service.py:43,158,164` | `_build_props`、`_to_email_list` |
| `ssp_system_characteristic_app_service.py:24` | `ModuleFrameSystemCharacteristicService as _ScMap` |

五支的檔頭都明寫「與資源庫範本版**完全相同**……故直接共用其純 mapping 靜態方法，
確保兩端一致、單一真相。差別只在 ssp_id 從哪來 + 權限」。
**module_frame 這邊是定義處，oscal 那邊是引用者**，收不掉也不該收。

第六支 `module_frame_ssp_resources_service.py` 方向相反——它 import
`app/oscal/service/ssp_resources_context_service.py`，共用的實作在 oscal 那邊。

另外 `app/flow_control/service/assessment_plan_app_service.py:57` 也 import
`module_frame_party_service` 的 `_build_props` 與 `_to_email_list`——**三個模組共用同一份對應表**。

**(b) `module_frame_template_import_service.py`（1,008 行）與
`app/oscal/service/ssp_control_impl_import_service.py`（431 行）是不是重複實作——部分重複，但不是可立刻收的。**

實查逐項對照：

| 項目 | MF 端 | oscal 端 | 是否相同 |
|---|---|---|---|
| Excel 9 欄標題 | `EXPECTED_HEADERS`（:88-98） | `HEADER_LABELS`（:47-57） | **逐字相同**（兩份各寫一份） |
| `VALID_STATUSES` 六值 | :78-86 | :43-46 | **逐字相同**（兩份） |
| 樣式常數（字型／填色／框線／對齊） | :101-112 | :59-70 | **逐字相同**（兩份） |
| props 名（`implementation-status`／`-description`） | 自己定義 `_P_STATUS`／`_P_DESC`（:56-57） | 從 `ssp_control_implementation_service` import（:35-39） | 值相同、來源不同 |
| 控制項母體 | profile → catalog（`_load_profile_controls`） | `ssp.import_profile_id` → catalog | 路徑不同 |
| 寫入落點 | 樣板 SSP 的 IR／statement | living SSP，且 delegate 給 `SspControlImplementationService` | **不同** |
| 檔案大小上限 | 無 | `MAX_FILE_SIZE = 10MB`（:71） | **MF 端缺這道** |

**結論：可抽出共用的是「Excel 欄位契約與樣式」那約 40 行常數**（三組逐字相同），
不是整支 service。而那 40 行要抽到哪裡、抽了會不會又變成 module_frame 與 oscal
互相 import 的第三條線，屬設計問題——列進 ⑤。

**(c) 沒有死碼。** 81 支全部通過四層查（模組內 import／模組外 import／DI 註冊／route 註冊）。
最接近的是 4.1 那兩個 Resource 類別，但它們在 `__init__.py` 有 import，
不符「四層全零」的死碼定義，故歸「疑似死碼」記錄而非死碼。

---

## ⑤ 要設計才能動的

只說「為什麼現在動不了、需要什麼前置」，不給完整方案。

### 5.1 Excel 欄位契約散在三處，抽出來會製造新的跨模組依賴

**現況**：SSP 控制項現況說明的 Excel 格式（9 欄標題、6 個狀態值、配色邊框）
在 `module_frame_template_import_service.py`（資源庫範本端）與
`app/oscal/service/ssp_control_impl_import_service.py`（專案端）**各寫一份逐字相同的常數**。
第三處是 `app/module_frame/excel_template/sheet_definitions.py`——它定義的是另一套
（SSP 匯入的九張表），被 `app/oscal/service/excel_parser/sheet_handlers.py` import。

**為什麼現在動不了**：抽到哪裡沒有現成答案。抽進 `common/` 要問「Excel 欄位標題算不算跨模組共用型別」；
留在 module_frame 讓 oscal import，會讓 oscal → module_frame 的依賴再多一條
（目前已有六條：五支 service mapping ＋ `sheet_definitions`）；
抽進 `jedi_oscal_v2` 套件則要問「中文欄位標題是不是套件該知道的事」——
CLAUDE.md 對套件的判準是「屬套件 domain 的才進套件」，中文標題像是產品知識。

**需要的前置**：先確定 module_frame 與 oscal 這兩個模組的最終疆界（誰是誰的上游），
再決定共用常數往哪邊放。這件事等 `oscal` 的逐檔分類結果（FR-105 另一支產出）。

### 5.2 六支 v2 殼「殼裡混著業務」，拆不拆會影響套件契約 → **需要決策者裁**

**現況**：六支殼每一支都不是純轉呼叫，都帶了主專案獨有的規則（見 ② 表的「殼帶主專案規則」欄）。
以 `module_frame_leveraged_service.py` 為例，它做三件事：

1. **欄位對應**（FE 名 ↔ OSCAL props）— 這是殼該做的
2. **NN 欄空值哨兵**（`party_uuid` 空落 NIL-UUID、`date_authorized` 空落 `date.today()`）—
   這是在**繞套件 schema 的 NOT NULL 約束**
3. **同名去重、暱稱 enrich** — 這是產品規則

第 2 項是關鍵：檔內註解寫「套件 `import_ssp` 用 `party-uuid or ""` 對 uuid 欄是 latent bug
（無 party-uuid 的 SSP 匯入會 500），但屬 import 路徑、本期非 scope」。
也就是說**主專案這一側在替套件的設計缺口補位**。

**兩種合理判法，後果不同**：

| | A 案：維持現狀（哨兵留主專案） | B 案：把 NN 約束的問題退回套件解 |
|---|---|---|
| 做法 | 六支殼繼續各自帶哨兵邏輯 | 套件把 `party_uuid`／`date_authorized` 改成 nullable，主專案拿掉哨兵 |
| 好處 | 不動套件，零風險 | 主專案的殼真正變薄；套件 `import_ssp` 那個 latent bug 一起解掉 |
| 代價 | 哨兵是「看不見的約定」——寫入時塞 NIL-UUID、讀出時 reshape 回 None，兩邊必須配對。任何第三個寫入者（例如 docx 匯入）不知道這個約定就會寫出前端看得到 NIL-UUID 的資料 | 動 `jedi_oscal_v2` 的 schema＝動 DB 欄位約束＝要 migration，且 oscal-v2 有其他 consumer |

**這題不自選，列出來問決策者。** 判斷需要的資訊：`jedi_oscal_v2` 除了本專案還有沒有別的
consumer、以及決策者對「主專案替套件補位」的容忍度。

### 5.3 `module_frame_service.py` 是模組內依賴最雜的一支，拆之前要先決定 BPMN 歸誰

**現況**：304 行、jedi=11／host=9。它同時碰四個外部套件
（`jedi_flow_engine` 的 workflow template 與 execution、`jedi_task_platform` 的 participant、
`jedi_common` 的分頁與 session）與三個主專案模組
（`app/flow_engine`、`app/project_summary_report`、`domain/module_frame`）。

裡面 96 行的 `clone_module_frame` 是自寫的 BPMN 深拷貝
（複製主流程 → 逐個複製子流程 → 把主流程 XML 裡的 `called_element_id` 全換成新 id）。
`module_frame_item_service.py` 的 219 行也幾乎全是 BPMN 增刪改。

**為什麼現在動不了**：「資源庫範本要不要綁一條 BPMN 流程」這件事本身
正在 FR-104（流程疆界重新分析）被重新檢視。
在那邊有結論之前，動這兩支 BPMN 邏輯等於押注一個還沒定的方向。

**需要的前置**：FR-104 的結論。

### 5.4 `di_containers/oscal/oscal_containers.py` 註冊了六支 module_frame 的 service

`oscal_containers.py:57,60,63,66,69,88` 註冊了 `module_frame_system_characteristic_service`、
`components`、`inventory`、`leveraged`、`party`、`ssp_resources` 六支，
而它們的 route（在 `api/module_frame/`）也是從 `Containers.oscal_container` 取注入。

**這不是錯**——這六支殼的資料落點在 `oscal.ssp_*` 表，DI 依落點分組有其道理。
但它造成「module_frame 的 route 從 oscal 的 container 取 service」這個反直覺的接線，
新人照 `module_frame_containers.py` 找會找不到。

**為什麼現在動不了**：移動 DI 註冊會同時改到 oscal 那側的 wiring，
而 DI 簽章漂移是靜默的（Factory 是 lazy，對不上只在該端點被打時才炸）。
要動需要把六支的端點逐一實打驗過。

**需要的前置**：`oscal` 的逐檔分類，確認 oscal 那側對這六支的依賴全貌。

---

## 對 FR-102 總表 B 該列的逐句核對

FR-102 `README.md:101` 對 `module_frame` 的證據欄共 5 句。逐句核對結果：

| FR-102 原句 | 核對結果 |
|---|---|
| 「自己持有 `module_frames`／`module_frames_trans` 兩張表」 | ✅ **成立**。`infra/module_frame/models/` 恰兩支 ORM，`__init__.py` re-export 兩個 |
| 「最大兩支全自寫（`ssp_import_template_app_service.py` 1,500 行、`module_frame_template_import_service.py` 1,008 行）」 | ⚠️ **行數成立，「全自寫」要修正**。1,500 行那支 jedi=18（從四個套件讀資料），說「全自寫」會讓人以為零套件依賴；準確說法是「主體邏輯自寫、資料來源在套件」。1,008 行那支 jedi=4 且只用到兩個 entity 型別，「全自寫」對它成立 |
| 「`excel_template/generator.py`（652 行）零 jedi 引用」 | ✅ **成立且可再強化**。不只該檔，`excel_template/` **整包 7 檔 1,656 行零 jedi 引用**（實跑 `grep -rnE '^\s*(from\|import) jedi_' → 0`） |
| 「`module_frame_party_service.py`（323 行）檔頭自陳……主體是 FE 欄位 ↔ OSCAL props 對應表」 | ✅ **成立**。檔頭第 24-38 行就是那張對照表 |
| 「`components`／`inventory`／`leveraged` 同型」 | ✅ **成立**，且應補上 `system_characteristic` 與 `ssp_resources`——**同型的是六支不是三支**（前五支同型；`ssp_resources` 略不同，它委派 oscal 的 context service） |
| 總表 A 的「對外 route 45」 | ✅ **成立**（本棒 AST 重跑得 45）。派工卡上轉述的「43」是筆誤 |
| 需另開卡欄「真業務與 v2 轉接殼約各半」 | ⚠️ **方向成立，比例要修正**。實際行數比是真業務 50.8%／轉接殼 35.0%／共用設施 14.1%，真業務約為殼的 1.45 倍，不是各半 |

**沒有任何一句已失效**；兩句需要精確化（「全自寫」與「約各半」），一句需要補充（同型是六支）。

---

## 需要決策者裁的

1. **六支 v2 殼裡的「NN 欄空值哨兵」要不要退回套件解**（§5.2 的 A／B 兩案）。
   關鍵未知：`jedi_oscal_v2` 除本專案外還有沒有別的 consumer。

2. **Excel 欄位契約那約 40 行逐字重複的常數要抽到哪裡**（§5.1）。
   三個候選落點各有代價，且與「module_frame 和 oscal 誰是誰的上游」這個更大的疆界問題綁在一起。

3. **§4.1 與 §4.2 那五個對外不可達的端點要不要清**（兩個被註解的 Resource ＋ 三個前端只在註解中呼叫的下載／匯入端點）。
   證據已備齊（四層查證結果見該節），但「清」屬異動、本棒只分析不動程式。
