# jedi-oscal-v2 主專案側接線程式 — 切棒盤點

盤點日期：2026-09-21／盤點員：Opus 5／工作目錄：`compliance-manager-be`
基準 commit：`6b011d67`（feature/review）

---

## 1. 範圍界定結論

### 實際數字

| 界定法 | 檔數 | 行數 | 說明 |
|---|---|---|---|
| **路徑法（oscal 四層 ＋ di_containers/oscal）** | **159** | 20,504 | `api/oscal` 61、`app/oscal` 46、`domain/oscal` 36、`infra/oscal` 14、`di_containers/oscal` 2 |
| **引用法（程式裡寫了 `jedi_oscal_v2` 的檔）** | **46** | — | 其中 `app/oscal` 19、`app/module_frame` 10、測試檔 9、其餘散落 |
| **🔴 建議採用範圍（路徑法 ＋ 5 個外圍接線檔）** | **167** | **21,364** | 見下方「建議範圍」 |

### 191 跟 156 的差距怎麼來的

差距不是統計方法不同，而是**總表那個 191 是當初排順序時的估算，沒有對得上任何一種數法**。實際驗證過的數字：

- 用 `git ls-files` 數 oscal 四層加上 di_containers 是 **159 支 .py**（首腦說的 156 應該是漏了 di_containers 那 2 支跟 `api/oscal/__init__.py`，或是用了不含 di 的算法＝157）
- 用 `find` 連未進版控的檔一起數也是 159；連 `.pyc` 等非 .py 檔全算進去是 327，也不是 191
- 191 這個數字在總表裡出現過兩次，另一次講的是**流程範本表裡 191 筆沒有客戶歸屬的資料**（§3.1 第 43 項）。兩處數字一模一樣、但一個是檔案數一個是資料筆數，**高度懷疑是當初寫總表時抄串行了**

我試過各種加總組合（159＋module_frame 的 api 25＝184、再加 domain 12＝196），**沒有任何一種組合剛好等於 191**。結論：**191 是估的，請以本盤點的 167 為準**，並建議首腦回頭把總表 §0.5 第 1 列跟 §5 的「191 個檔案」改成 167。

### 建議範圍（167 支）

路徑法的 159 支，**再加 5 支住在別的目錄、但實際承擔 OSCAL 權限判斷或接線責任的檔**（加上 `di_containers/dashboard_apis/oscal.py` 共 8 支，其中 3 支已含在 159 內）：

| 補進來的檔 | 行數 | 為什麼一定要納入 |
|---|---|---|
| `common/authz/ssp.py` | 225 | **整個 SSP 的權限判斷只有這一支**。`app/oscal` 裡 9 個檔 import 它、共呼叫 54 次。不納入等於掃了 54 個呼叫點卻看不到被呼叫的那支怎麼判 |
| `infra/readmodel/oscal/ssp_control_implementation_query.py` | 257 | 控制項實作的查詢實作，直接組 SQL |
| `infra/readmodel/oscal/resource_library_query.py` | 141 | 資源庫的查詢實作 |
| `app/flow_control/service/oscal_stage_handlers.py` 等 3 支 | 342 | 稽核流程推進階段時會回頭改 OSCAL 資料，是跨模組的寫入入口 |
| `di_containers/dashboard_apis/oscal.py` | 36 | 儀表板那側的 OSCAL 接線 |

### module_frame 的建議處置：**這次不要納入，但建議另立一棒**

module_frame 有 81 支檔（api 25／app 31／domain 12／infra 13／di 2），確實跟 oscal 高度糾纏（10 支 app 檔直接 import `jedi_oscal_v2`）。但**我建議這次不掃**，理由是實際 grep 出來的守門狀況**兩邊差很多**：

- **module_frame 的權限守門做得相當完整**：25 支網址入口檔裡有 **15 支**用了 `require_capability`（也就是「這個角色能不能用這個功能」那種檢查），是正規做法
- **oscal 這邊 22 支網址入口檔裡只有 1 支**碰 `common.authz`

兩邊的風險型態不一樣，**檢查重點寫不到同一張卡裡**——oscal 這棒要問的是「這支端點到底有沒有人在守」，module_frame 要問的是「守是守了，但守的那個權限對不對、夠不夠細」。混在一起掃，研究員會拿錯檢查清單（這正是總表 §5 要求 oscal 主專案側跟套件本體必須分開掃的同一個道理）。

**建議**：module_frame 另開一支母卡、獨立排隊，等 oscal 這批掃完再排。若首腦要現在就排，至少要另寫一份檢查重點。

### 已掃過的部分：這塊完全沒被掃過

查了總表 §0.5 三張表與已掃完的 18 支套件清單，**oscal 主專案側 167 支沒有任何一支被前面的 arc 掃過**。唯一沾到邊的是 §3.1 第 67 項（FR-095 H1 掃專案任務平台時順手發現資源庫範本可以被竄改），那一條指向的 `resource_library_app_service.py` 正是本次 O2 棒的主角——**掃到的是問題，這支檔本身沒被完整掃過**。

---

## 2. 切棒表（10 棒，全部 ≤2,000 行；O9 除外，派工前再切）

界定範圍 **167 檔／21,364 行**。扣掉移去套件本體那批的 Word 內容抽取器 7 支（原 O7a／O7b，2,868 行）後，**實際進掃描 92 檔／17,224 行、切 10 棒**（逐檔 `wc -l` 實算、十棒無重複檔）；其餘 68 檔為資料結構宣告（空的 `__init__.py`、ORM model、entity 介面、純格式定義），沒有判斷邏輯，不排入掃描。

| 棒 | 在掃什麼（白話） | 檔數 | 行數 | 最大單檔 |
|---|---|---|---|---|
| **O1** | 控制項實作：每條合規控制項「我們怎麼做到的」的讀寫，含唯一那支權限判斷程式 | 6 | 1,943 | 1,037 行／60KB |
| **O2** | 資源庫、公版範本、SSP 匯出（含叫 LibreOffice 轉檔） | 11 | 1,772 | 530 行／32KB |
| **O3a** | Excel 上傳入口與確認流程（權限題） | 5 | 1,460 | 937 行／48KB |
| **O3b** | Excel 內容解析（惡意檔案題） | 7 | 1,047 | 428 行 |
| **O4** | Word 上傳與解析全鏈（含控制項實作單獨匯入） | 9 | 1,950 | 787 行／40KB |
| **O5** | SSP 六種子物件的增刪改查（元件／人員／設備／繼承授權／參考資料／系統特性） | 15 | 1,808 | 230 行 |
| **O6** | 程序書文件池 ＋ 匯入比對引擎（新舊版差異怎麼算、使用者選了要怎麼合併） | 6 | 1,917 | 751 行／36KB |
| **O8a** | 合規框架 PDF 解析工作 | 6 | 1,343 | 674 行 |
| **O8b** | 合規框架與版本增刪改查 | 9 | 1,479 | 351 行 |
| **O9** | 接線總裝：路由註冊表、依賴注入、稽核階段掛鉤、人員對帳、匯入線共用接縫檔 | 18 | 2,505 ⚠️ | 620 行 |

**O7a／O7b（Word 內容抽取器 7 支）不在本批**——純解析無權限議題，風險型態是惡意檔案，併進 jedi-oscal-v2 套件本體那批一起掃（理由見 §1 與 §4）。本批最終 **10 棒**。

⚠️ **O9 是唯一超過 2,000 行的一棒**（2,505 行）：它接手了 Excel 與 Word 兩條匯入線共用的接縫檔 `_common.py`（620 行）——該檔兩棒各自帶都會讓那一棒爆上限，重疊帶入則兩棒都爆。O9 排序在最後幾棒，**派工前再切**（可能的切法：人員對帳五支或 `bundle_restore.py` 另放）。runner 以派工當下的卡片 scope 為準。

---

### O1 — 控制項實作與權限判斷核心（6 檔／1,943 行）

> 白話：每一條合規控制項底下「我們公司怎麼做到這條」的文字記錄，是整個系統最核心的資料。這棒同時把唯一一支負責判斷「你能不能看／能不能改這份文件」的程式一起放進來。

```
api/oscal/routes/ssp/ssp_control_implementation_route.py        309
api/oscal/serializers/ssp/ssp_control_implementation.py          88
api/oscal/serializers/ssp/ssp_control_impl_objective.py          27
app/oscal/service/ssp_control_implementation_service.py        1037  ← 60KB，單檔最大
common/authz/ssp.py                                             225  ← 全系統唯一 SSP 權限判斷
infra/readmodel/oscal/ssp_control_implementation_query.py       257
```
最大單檔 1,037 行／60KB — **已達安全線上緣**，若研究員回報壓縮，下次把 `common/authz/ssp.py` 移到 O5。

---

### O2 — 資源庫、公版範本、匯出（11 檔／1,772 行）

> 白話：客戶自己建的範本庫、原廠給的公版範本，以及把系統安全計畫匯出成 Word／PDF 的整條路。**這棒優先級最高**——總表第 67 項已經掃到這裡有問題。

```
api/oscal/routes/resource_library_route.py                       62
api/oscal/routes/module_frame_template_ssp_route.py              38
api/oscal/routes/ssp/ssp_export_route.py                         72
app/oscal/service/resource_library_app_service.py               530  ← 零權限檢查 ＋ 9 段手寫 SQL
infra/readmodel/oscal/resource_library_query.py                 141
app/oscal/service/export/ssp_export_app_service.py              (見下)
app/oscal/service/export/ssp_docx_generator.py                  258
app/oscal/service/export/ssp_libreoffice_converter.py           124  ← 唯一呼叫外部程式的檔
app/oscal/service/export/ssp_v2_content_loader.py               217
app/oscal/service/export/ssp_export_model.py                    169
app/oscal/service/oscal_export_app_service.py                    47
```

---

### O3a — Excel 上傳入口與確認流程（5 檔／1,460 行）

> 白話：使用者把 Excel 傳上來、系統開一張解析工作單，使用者看過預覽後按確認才真的寫進去。這一棒是**權限題**——誰能傳、誰能看預覽、誰能按確認、送上來的編號有沒有驗歸屬。

```
api/oscal/routes/ssp/ssp_excel_import_route.py                  112
api/oscal/routes/ssp/ssp_scoped_excel_import_route.py           130
api/oscal/serializers/ssp/ssp_excel_import.py                   137
app/oscal/service/ssp_excel_import_app_service.py               937  ← 48KB
app/oscal/service/import_adapter/excel_to_oscal_ssp.py          144
```

### O3b — Excel 內容解析（7 檔／1,047 行）

> 白話：把 Excel 檔裡九張工作表的每一格讀出來、轉成系統看得懂的資料。這一棒是**惡意檔案題**——超大表格、公式注入、外部連結、解析上限有沒有設。

```
app/oscal/service/excel_parser/__init__.py                        9
app/oscal/service/excel_parser/parser.py                        153
app/oscal/service/excel_parser/sheet_handlers.py                428  ← 本棒最大
app/oscal/service/excel_parser/types.py                         111
app/oscal/service/excel_parser/v2_bundle.py                     217
app/oscal/service/excel_parser/validators.py                     66
app/oscal/service/excel_parser/version_check.py                  63
```

O3a 與 O3b 的拆點是**題目性質**（權限 vs 惡意檔案），不是行數——兩種題目的檢查清單不一樣，混在一起研究員會拿錯清單。

### O4 — Word 上傳解析鏈（9 檔／1,950 行）

```
api/oscal/routes/ssp/ssp_docx_import_route.py                   105
api/oscal/routes/ssp/ssp_control_impl_import_route.py           114
api/oscal/serializers/ssp/ssp_docx_import.py                    185
api/oscal/serializers/ssp/ssp_control_impl_import.py             59
app/oscal/service/ssp_docx_import_app_service.py                787  ← 40KB
app/oscal/service/ssp_control_impl_import_service.py            416
app/oscal/service/import_adapter/docx_to_oscal_ssp.py           152
app/oscal/service/import_adapter/party_match_enrich.py           78
app/oscal/service/import_adapter/v2_candidate_loader.py          54
```
⚠️ `app/oscal/service/import_adapter/_common.py`（620 行）是 Excel 與 Word 兩條匯入線共用的接縫檔，**掛在 O9 掃**。它不能放在 O3a 或 O4——哪一棒帶它，哪一棒就爆 2,000 行上限；重疊帶入兩棒則兩棒都爆。O3a 與 O4 的卡片裡已註明「這支共用檔在 O9」。

---

### O5 — SSP 六種子物件增刪改查（15 檔／1,808 行）

```
api/oscal/routes/ssp/ssp_components_route.py                     85
api/oscal/routes/ssp/ssp_party_route.py                          84
api/oscal/routes/ssp/ssp_inventory_items_route.py                84
api/oscal/routes/ssp/ssp_leveraged_route.py                      78
api/oscal/routes/ssp/ssp_resources_route.py                      88
api/oscal/routes/ssp/ssp_system_characteristic_route.py          51
app/oscal/service/ssp_components_app_service.py                 (各約 150-200)
app/oscal/service/ssp_party_app_service.py                      169
app/oscal/service/ssp_inventory_items_app_service.py            194
app/oscal/service/ssp_leveraged_app_service.py
app/oscal/service/ssp_resources_app_service.py
app/oscal/service/ssp_system_characteristic_app_service.py
app/oscal/service/ssp_resources_context_service.py              230
domain/oscal/service/ssp_project_resolver.py                    184  ← 「這份 SSP 屬於哪個專案」的解析
domain/oscal/service/ssp_context_resolver.py                    158
```

---

### O6 — 文件池與匯入比對引擎（6 檔／1,917 行）

```
api/oscal/routes/ssp/ssp_document_pool_route.py                 183
api/oscal/serializers/ssp/ssp_document_pool.py                   37
app/oscal/service/ssp_document_pool_service.py                  313
app/oscal/service/import_diff/ssp_diff_service.py               751  ← 36KB
app/oscal/service/import_diff/decision_merge.py                 318
app/oscal/service/import_diff/snapshot_views.py                 315
```

---

### O7a — Word 內容抽取器（4 檔／1,639 行）

```
domain/oscal/parser/docx_section_extractors.py                  892  ← 單檔最大
domain/oscal/parser/docx_parser_core.py                         555
domain/oscal/parser/docx_intermediate.py                         72
domain/oscal/parser/control_id_matcher.py                       120
```

### O7b — CMMC 轉接器與中介格式（3 檔／1,307 行）

```
domain/oscal/adapter/cmmc_ssp_adapter.py                        860
domain/oscal/parser/ssp_intermediate.py                         369
domain/oscal/adapter/i_ssp_docx_adapter.py                       78
```

---

### O8a — 合規框架 PDF 解析工作（6 檔／1,343 行）

```
api/oscal/routes/framework/framework_parse_job_route.py         178
app/oscal/service/framework_parse_job_service.py                674
api/oscal/serializers/framework_parse_job/framework_parse_job_schemas.py  201
domain/oscal/service/framework_parse_job_domain_service.py      123
infra/oscal/repository/framework_parse_job_repo_impl.py          91
domain/oscal/entity/framework_parse_job_entity.py                76
```

### O8b — 框架與版本增刪改查（9 檔／1,479 行）

```
api/oscal/routes/framework/oscal_framework_route.py             104
api/oscal/routes/framework/oscal_framework_version_route.py     157  ← 唯一有掛 authz 的 route 檔
api/oscal/routes/framework/framework_version_edit_route.py      132
app/oscal/service/framework_app_service.py                      247
app/oscal/service/framework_version_app_service.py              229
app/oscal/service/framework_version_edit_service.py             351
api/oscal/serializers/framework/oscal_framework.py              165
api/oscal/serializers/framework/oscal_framework_version.py       34
api/oscal/serializers/framework_version_edit/schemas.py          60
```

---

### O9 — 接線總裝、稽核階段掛鉤、人員對帳（18 檔／2,505 行 ⚠️ 派工前再切）

> 白話：把所有東西接起來的那一層——哪個網址對到哪支程式、哪個物件由誰建立；以及稽核流程推進／退回階段時會回頭改 OSCAL 資料的那三支。

```
api/oscal/__init__.py                                           201  ← 路由註冊總表，能看出哪些入口是關的
di_containers/oscal/oscal_containers.py                         509
di_containers/oscal/__init__.py                                   0
di_containers/dashboard_apis/oscal.py                            36
app/flow_control/service/oscal_stage_handlers.py                142
app/flow_control/service/oscal_stage_preconditions.py           129
app/flow_control/service/oscal_stage_rollback_handlers.py        71
domain/oscal/import_pipeline/bundle_restore.py                  208
domain/oscal/service/reconciliation/base.py                     107
domain/oscal/service/reconciliation/person_reconciler.py        101
domain/oscal/service/reconciliation/organization_reconciler.py  101
domain/oscal/service/reconciliation/_normalizers.py              51
domain/oscal/service/reconciliation/match_method.py              10
domain/oscal/service/party_reconciliation_service.py             30
infra/oscal/repository/ssp_catalog_title_query.py                93
infra/oscal/model/framework_parse_job.py                         36
infra/oscal/model/ssp_docx_parse_job.py                          30
infra/oscal/model/ssp_excel_parse_job.py                         30
app/oscal/service/import_adapter/_common.py                     620  ← Excel／Word 共用接縫檔
```
⚠️ **本棒 2,505 行超過上限，派工前再切**。`_common.py` 放這裡是因為它是兩條匯入線共用的，放進 O3a 或 O4 都會讓那一棒爆上限。可能的切法：人員對帳五支（約 370 行）或 `bundle_restore.py`（208 行）另放一棒。

---

### 不排入掃描（86 檔／約 1,400 行）

`__init__.py` 空檔、`infra/oscal/model|mapper`（純資料表定義與欄位對應，205 行）、`domain/oscal/entity|repository` 介面（329 行）、`api/oscal/serializers/oscal_common/`（21 支純格式定義，337 行）。這些是資料結構宣告，沒有判斷邏輯，**照 CLAUDE.md「純資料表不計」的精神排除**。若首腦要求全覆蓋，可合併成一棒約 900 行。

---

## 3. 每棒重點看什麼（讀碼後的實數與發現）

### 🔴 先更正一個關鍵數字：「122 支入口完全沒有權限檢查」**不成立**

總表 §0.5 寫「這部分共 122 支功能入口只驗證有沒有登入，完全沒有功能權限檢查（實際測試確認）」。我實際 grep 驗證，**實數與結論都要修正**：

| 實測項目 | 實數 |
|---|---|
| `api/oscal/routes` 底下的功能入口總數 | **89 支**（不是 122；122 應該是把 module_frame 的 62 支也算進去了，89＋62＝151 也對不上，來源不明） |
| 入口檔（route）掛了 `common.authz` 的 | **22 支檔裡只有 1 支**（`oscal_framework_version_route.py`） |
| `jwt_required`（只驗有沒有登入）出現次數 | **110 次** |
| **但是**：權限檢查下移到下一層（app service）的呼叫次數 | **54 次**（`require_manager` 33 次、`require_participant` 21 次），分布在 **12 支 app service 檔** |
| `app/oscal` 46 支檔中有掛權限判斷的 | **14 支** |

**真實情況是這樣**：SSP 那一支線（61 支入口）**權限是有守的，只是守在下一層**——入口只驗登入，真正判斷「你是不是這個專案的成員／負責人」寫在 app service 裡，呼叫 `SspPermissionChecker.require_participant()`（讀）或 `require_manager()`（寫）。這是 CLAUDE.md 明文允許的做法（「資源域守門維持 app service 層」）。框架那一支線（25 支入口）也有守，用的是 `require_platform_admin()`，共 18 處。

**所以這批掃描要問的真正問題不是「有沒有守」，而是「守的那 54 個點，是不是每一支公開方法都守到了」**——這是逐支對照題，不是全面缺口。

**真正沒守的確認缺口（實查）**：

1. **`resource_library_app_service.py` 530 行，`require_`／`authz`／`Forbidden` 一個字都沒有**（grep 全檔零命中），而且裡面有 **9 段手寫 SQL**（`sa.text(...)`）。對應的兩支入口 `/oscal/resource-libraries/list` 與 `/oscal/resource-libraries` 也沒守。**這正是總表第 67 項那條「租戶管理員可以改掉原廠公版範本」的現場**——第 67 項只描述了其中一條路徑（改控制項清單），這支檔裡還有另外 8 段 SQL 沒人看過。
2. **`module_frame_template_ssp_route.py`**（1 支入口）同樣零守門。

### 各棒重點（建議寫進卡片）

**O1（控制項實作）**
1. `ssp_control_implementation_service.py` 1,037 行裡有 12 次 `require_*` 呼叫，但這支檔的公開方法（`@transaction` 標的）有幾支？**逐支對照「每支公開方法是不是都有守門」**，找出漏掉的那幾支
2. `require_manager` 與 `require_participant` 有沒有用錯邊——寫入的操作卻只要求 participant（等於任何專案成員都能改）
3. `common/authz/ssp.py` 的 `require_manager` 內含 `_require_ap_editable`（評估計畫還能不能編輯），**`require_participant` 沒有這層**。有沒有寫入操作走了 participant 這條，結果繞過了「已定案不能再改」的限制
4. `SspPermissionChecker` 的依賴全靠依賴注入（`__init__` 五個參數預設都是 `None`），**若注入漏掉，`self._resolver` 是 None 會直接炸還是靜默放行**——讀 `_require_user_is_participant` 的 None 處理
5. `infra/readmodel/.../ssp_control_implementation_query.py` 257 行的查詢，有沒有靠資料庫隔離擋租戶、還是完全靠上層守門

**O2（資源庫與匯出）— 優先級最高**
1. **`resource_library_app_service.py` 全檔零守門**，逐支公開方法列出「誰能呼叫、能改到誰的資料」
2. **9 段手寫 SQL 逐段看**：參數是不是綁定的（有沒有字串拼接）、有沒有 `WHERE tenant_id`、資料庫隔離對這幾張表是否開啟（總表第 67 項已確認 `oscal.profile_imports` 與 `ssp_implemented_requirements` **連客戶歸屬欄位都沒有**）
3. **`ssp_libreoffice_converter.py` 是整批唯一呼叫外部程式的檔**——檔名／路徑有沒有可能被使用者控制、暫存檔放哪、轉完有沒有刪、同時多人匯出會不會互相踩到
4. 匯出端點 `/ssp/<ssp_uid>/export` 只有 1 支入口，`ssp_export_app_service.py` 只有 1 次 `require_*`——**匯出等同讀取整份文件，這 1 次守門守的是哪個動作**
5. 公版範本（原廠給的）與客戶自建範本的讀寫判斷是不是同一套——公版應該只有平台管理員能改

**O3a／O4（Excel 與 Word 上傳的權限線）**
1. `ssp_excel_import_app_service.py` 三支公開方法（`upload_and_parse`／`get_parse_result`／`confirm_import`），**只有 3 次 `require_*` 呼叫**——逐支確認是不是每支都守到。已讀到 `get_parse_result` 是「先查租戶再查專案成員」兩段式，`upload_and_parse` 的守門藏在 `_verify_source_exists` 裡面，**這種「守門藏在私有方法裡」的寫法最容易漏**
2. **`source_uid` 由使用者從表單送上來**（總表首腦讀碼時的疑點），有沒有檢查這個編號指到的東西是不是使用者自己的
3. 上傳檔案大小上限（`_EXCEL_MAX_SIZE` 有設）、**Word 那邊有沒有對應的上限**、擋在哪一層（讀完整份檔案之後才擋等於沒擋）
4. 檔名處理：有沒有用 `secure_filename`、上傳的檔存到哪、解析失敗的殘檔會不會留著
5. 解析工作（parse job）有存活時間機制，**過期後的資料還讀不讀得到**；`parse_uid` 是不是可以猜的（流水號還是隨機碼）
6. `_common.py`（620 行，兩條匯入線共用）**在 O9 掃**，本棒只看「呼叫它之前有沒有驗過權限」

**O3b（Excel 內容解析）— 惡意檔案題，與上面完全不同的清單**
1. `parser.py:42` 用 `load_workbook(file_path, data_only=True, read_only=False)`：`read_only=False` 代表整份檔載進記憶體（試算表炸彈），`data_only=True` 代表讀的是快取計算結果
2. **公式注入**：讀進來以 `=`／`+`／`-`／`@` 開頭的字串有沒有中和，這條要跟 O2 的匯出線合看（進來沒擋＋出去沒擋＝完整一條路）
3. **外部連結／外部參照**：載入時有沒有關掉，會不會讓伺服器主動去連使用者指定的網址（SSRF）
4. `sheet_handlers.py` 428 行：工作表數／列數／欄數有沒有上限；讀到預期外型態時是回報錯誤還是默默吞掉
5. `validators.py` 只有 66 行，對九張工作表的樣板少得可疑——驗出錯誤是擋住整份還是只記一筆繼續跑
6. `version_check.py` 拿使用者檔案裡的字串當版本號：直接信任嗎、版本不符是擋下還是照舊解析
7. `v2_bundle.py` 組裝時有沒有拿檔案內容裡的編號直接當系統內編號用

**O5（子物件增刪改查）**
1. 六組各 4 支入口共 22 支，對應的 app service 各有 2～4 次 `require_*`——**數字對不對得上**（例如元件有 4 支入口但只有 4 次守門，那刪除那支守到了嗎）
2. `ssp_project_resolver.py`「這份 SSP 屬於哪個專案」是整條權限鏈的起點，**解析不到的時候回什麼**（拋錯還是回 None 然後上層當作通過）
3. 子物件的編號（`item_uid`）在改／刪的時候，有沒有確認它確實屬於網址上那份 SSP（跨 SSP 改別人的元件）

**O6（文件池與比對引擎）**
1. 文件池 9 支入口對 9 次守門，看起來齊——**逐支核對是不是一對一**
2. 比對引擎 `ssp_diff_service.py` 751 行：**使用者選擇「這筆要／那筆不要」的決定，合併的時候有沒有再驗一次權限**，還是只在進入比對畫面時驗過一次
3. `decision_merge.py` 的合併決定是使用者送上來的資料結構，有沒有可能塞進不該改的欄位

**O7a／O7b（Word 解析器）**
1. 這兩棒是純解析、沒有權限問題，**重點在惡意檔案**：巢狀深度、超大表格、外部參照
2. `docx_section_extractors.py` 892 行的抽取邏輯，有沒有對段落數／表格數設上限

**O8a／O8b（合規框架）**
1. 框架這條線用 `require_platform_admin()` 守，共 18 處，**只守寫入不守讀取**（註解明寫「寫入僅 root 租戶」）——讀取是不是真的可以全開
2. 框架 PDF 解析工作（`framework_parse_job_service.py` 674 行）只有 3 次 `require_platform_admin`，但有 5 支入口

**O9（接線總裝）**
1. **`api/oscal/__init__.py` 是路由註冊總表，能直接看出哪些入口是關的**——已讀到 `SspScopedExcelImportRoute` 與 `Confirm` 兩支是註解掉的狀態（FR-038 2A 遺留）。**確認被註解掉的入口，其 app service 是不是還被別的地方呼叫得到**
2. 依賴注入設定 509 行：`SspPermissionChecker` 的五個依賴有沒有全部接上（接不上就是權限判斷失效）
3. 稽核階段掛鉤三支：推進／退回階段時回頭改 OSCAL 資料，**這條路徑繞不繞得過 SSP 的權限判斷**

---

## 4. 建議派工順序

| 順位 | 棒 | 理由 |
|---|---|---|
| **1** | **O2**（資源庫與匯出） | **已知有問題的地方，先掃**。總表第 67 項已經證實這裡能改到原廠公版；530 行零守門＋9 段手寫 SQL，是全批最可能有斬獲的一棒。而且這棒同時涵蓋唯一呼叫外部程式的檔 |
| **2** | **O1**（控制項實作＋權限核心） | 把 `common/authz/ssp.py` 先掃過，**後面五棒的判斷基準就有了**。若這支本身有洞，後面棒次的結論全要重評估 |
| **3** | **O3a**／**O3b**／**O4**（Excel 入口、Excel 解析、Word 上傳，可換序） | 檔案上傳是外部輸入進入系統的正門；三棒互不相干。⚠️ 全機一次只跑一支掃描，「可換序」不是「同時跑」 |
| **4** | **O9**（接線總裝） | 掃完前四棒後再掃，**能回頭驗證「依賴注入有沒有接對」對前面結論的影響**；也能確認被註解掉的入口是不是真的關了 |
| **5** | **O5**／**O6**（子物件、文件池比對） | 守門看起來齊，屬於逐支核對題，斬獲期待值中等 |
| **6** | **O8a**／**O8b**（合規框架） | 用 `require_platform_admin` 守得最嚴，期待值最低 |
| **7** | **O7a**／**O7b**（Word 解析器） | 純解析無權限議題，**檢查重點跟前面完全不同**（惡意檔案），建議跟套件本體那批（第 8 順位）排在一起掃，共用同一張檢查清單 |

**強烈建議**：O7a／O7b **不要跟前六棒混排**。它們的風險型態與 jedi-oscal-v2 套件本體（第 8 順位，364 檔）一樣是「檔案解析安全」，跟權限檢查是兩回事——這正是總表 §5 已經寫明的分離原則，只是當初沒注意到 domain 層的解析器住在主專案這一側。

---

## 5. 盤點時發現、但需要首腦裁決的事

1. **🔴 總表的「191 個檔案」與「122 支功能入口」兩個數字都跟實際對不上**。實數是 167 檔（建議範圍）／89 支入口。191 疑似跟 §3.1 第 43 項的「191 筆資料」抄串行。**要不要順手把總表 §0.5 與 §5 的數字改掉？**（我沒改，照指示不動檔）

2. **🔴 「完全沒有功能權限檢查」的描述會誤導研究員**。實際上 SSP 線有 54 個守門呼叫、框架線有 18 個，只是守在 app service 層而不是入口層——這是 CLAUDE.md 明文允許的正規做法。**若卡片照抄總表的說法，研究員會拿著「找沒守門的端點」去掃，掃到的全是誤報**。建議卡片改寫成「逐支公開方法核對守門是否齊全，並重點查 `resource_library_app_service.py` 這支確認零守門的檔」。

3. **module_frame 要不要現在排**。我的建議是不要（理由見 §1），但這 81 檔跟 oscal 有 12 處跨模組 import，**首腦若認為「接線」應該包含被接的那一側，可以另立 M1／M2 兩棒**（api 25＋app 31 約 10,044 行，要切 5～6 棒）。

4. ~~**`_common.py` 與 `bundle_restore.py` 的歸屬**~~ — **已裁定**：`_common.py`（620 行）掛 O9（重疊帶入兩棒會讓兩棒都爆上限），O3 另拆成 O3a／O3b。`bundle_restore.py`（208 行）留在 O9。O9 因此 2,505 行超標，排序在後、派工前再切。

5. **被註解掉的路由**（`api/oscal/__init__.py:110-111`，兩支 scoped Excel 匯入入口）。FR-038 2A 的遺留狀態。**這是不是已經永久廢棄？** 若是，對應的 app service 方法是死碼可以不掃；若只是暫時關閉，那它們是「隨時會被打開、但沒人掃過」的入口，風險更高。我不確定，**建議首腦查一下 FR-038 的 2B 進度再決定要不要特別標注**。

6. **`domain/oscal/parser` 與 `adapter` 該不該算「主專案側接線」**。這 7 支（O7a／O7b，2,868 行）做的是純檔案解析，性質上更像套件本體那一批。**若首腦認同，可以把 O7a／O7b 從本批移出、併進第 8 順位那批（套件本體）一起掃**，本批就縮成 10 棒（O3 拆 O3a／O3b 後）、界定範圍 160 檔、實際進掃描 92 檔／17,224 行。

7. **不排入掃描的 86 檔（約 1,400 行）**。我照「純資料表不計」的精神排除，但其中 `infra/oscal/model/` 那 4 支資料表定義**可能藏著「這張表有沒有客戶歸屬欄位」的答案**（總表第 67 項就是栽在這上面）。**建議至少讓 O9 那棒的研究員順手看一眼 model 目錄**，不必單獨開棒。

---

## 附：驗證用指令（首腦復核時可直接跑）

```bash
cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be

# 範圍 167 檔／21,364 行
git ls-files 'api/oscal/*.py' 'app/oscal/*.py' 'domain/oscal/*.py' 'infra/oscal/*.py' \
  'di_containers/oscal/*.py' 'infra/readmodel/oscal/*.py' 'common/authz/ssp.py' \
  'app/flow_control/service/oscal_*.py' 'di_containers/dashboard_apis/oscal.py' | xargs wc -l | tail -1

# 89 支功能入口
git ls-files 'api/oscal/routes/*.py' | xargs grep -c '^\s*def \(get\|post\|put\|patch\|delete\)' \
  | awk -F: '{s+=$2} END {print s}'

# 54 次 app service 層守門呼叫
git ls-files 'app/oscal/*.py' | xargs grep -ho \
  'require_participant\|require_manager\|require_read_access\|require_write_access' | sort | uniq -c

# resource_library 零守門（應該零輸出）
grep -n 'require_\|authz\|permission\|raise Forbidden' app/oscal/service/resource_library_app_service.py
```
