# FR-038 — 合規資源庫「適用控制項」SoA 頁 v2 遷移 進度 SUMMARY

> 狀態 **2026-06-16**。本檔是「FE 逐項套到 v2 BE」arc 中**資源庫 §5 決策1（適用控制項改走範本 SSP SoA）**這條的進度紀錄。
> ⚠️ **本輪所有改動都還沒 commit**（working tree；`pyproject.toml` dev path-dep 不 commit）。這是「進度追蹤」文件，非收尾 — changelog / Notion / 對話歸檔 / 橫向文件**都還沒做**。

## 🧭 這條 arc 在解什麼

FR-038 把 BE 翻成 jedi_oscal_v2，資源庫範本編輯頁（`module-frame/<uid>/template-edit`）整套建在舊 jedi_oscal 上、2A 後被 stub/disable。本輪把這頁**逐功能接回 v2**，原則：換底層、FE 盡量不動、保留原功能。

**典範**：資源庫 = clone catalog + profile + 範本 SSP 三件組（Model A）；「適用控制項」= 編範本 SSP 的 control-implementation（IR）/ AO（statement）SoA。

## ✅ 已完成且 curl 驗證通過

| # | 項目 | 做法 | 驗證 |
|---|------|------|------|
| 1 | 詳情樹顯示（group→control→AO）| `module_frame detail` enrich `oscal_profile.include_groups`（新 `resource_library_app_service.get_profile_control_tree`，profile→cloned catalog→parts）| ✅ 回 group/control/AO |
| 2 | profile id 對映 | create 把 include_controls 數字 id → **control_id 字串**（profile resolve 以 control_id 比對、跨 clone 穩定）| ✅ resolved_control_count 正確 |
| 3 | template-ssp 端點 | 從 EXCLUDE_MODULES re-enable + 註冊 + 改走 `get_template_ssp_ref` | ✅ 回 `{ssp_uid,exists}` |
| 4 | 控制項層可編（control-defaults）| service 重寫走 v2 `ssp_service`（IR.props 存 status/desc/role/origination，繞範本 SSP 無 project 的權限層）| ✅ PUT 寫入+讀回+暱稱 |
| 5 | AO 層可編（objective-defaults）| service 重寫走 v2 statement（IR 下 statement.props）| ✅ PUT AO 值+讀回 |
| 6 | AO 初始化基礎 BPMN | create 時每 AO 生 BPMN（Camunda7，含 Evidence upload 任務）+ workflow_template_uid 寫 part.props；樹讀回 | ✅ 6 AO→6 workflow_template |
| 7 | 編輯適用控制項（增減）| `update_module_frame` 重寫 + `update_applicable_controls`（更新 profile with-ids + 移除被拔控制項 IR〔statements CASCADE〕+ 新控制項補 AO BPMN）；FE dialog 改吃 catalog-tree + 用 control_id pre-select | BE 邏輯已寫；待 FE 實點增減 |
| 8 | 程序書掛載 control/AO | `module_frame_reference_document_service` 改走 v2 ssp_service（context_id = IR.id / statement.id）| ✅ FE 實點驗過(2026-06-16) |
| 9 | 程序書 picker 顯示/reload | FE：顯示 fallback `file_name`、搜尋含 file_name、開啟時 reload pool | ✅ FE hot-reload |

## ❌ 未完成 — 批次維護「匯入匯出」（另一塊獨立大工）

`template-edit` 頁「批次維護」選單全失效,**多個不同根因**,需獨立 task：

| 功能 | 根因 |
|------|------|
| Excel 樣板下載 / 匯出現況 / 匯入現況 | DI 炸：`module_frame_template_import_service` + `SspImportTemplateAppService` 引用 oscal_container **未定義**的 `control_implementation_domain_service`（di_containers/module_frame/module_frame_containers.py:203-204, 251-252）→ 需 v2 重寫 + DI |
| 從文件匯入(docx)/ 從 Excel 匯入 | **`ssp_docx_parse_jobs` / `ssp_excel_parse_jobs` 表在 v2 DB 不存在** → 需 DB migration |
| SSP 文件匯出(docx/pdf/odt) | 待確認（log 那兩個錯是 import parse_jobs，非 export 本身）|

> 這塊是 handoff 早標的「control-defaults 匯入匯出依賴 stubbed → 恐死」+ import parse-job 子系統,非本輪改壞。

---

## 2026-06-16 續：docx 匯入逐項修復（同日後半段，全部 UNCOMMITTED）

把「從文件匯入(docx)」整條打通到「匯入內容顯示在範本編輯頁」。逐項根因都是 v2 換底層後某條路徑沒跟上。

| # | 症狀 | 根因 | 修法 | 狀態 |
|---|------|------|------|------|
| A | docx/excel 匯入 500 | `oscal.ssp_docx_parse_jobs` / `ssp_excel_parse_jobs` 表在 v2 DB 不存在（oscal schema DROP 重建時漏掉主專案自有表）| migration `scripts/sql/2026-06-16-restore-ssp-parse-jobs-v2.sql`（建 2 表 + RLS + GRANT），已套 DEV | ✅ user 驗過 |
| B | 預覽頁整片空白 | `get_parse_result` 把資料包進 `parsed_result` key，但 FE 讀最上層（v1 是 `**parsed_result` 攤平）| 還原攤平 | ✅ user 驗過 |
| C | 人員/單位 0 筆 | docx 用「修訂模式」填，值在 `<w:ins>`，python-docx 讀不到；FR-038 P3 重寫 app service 時刪掉了 `_normalize_docx_revisions` | 還原 `_normalize_docx_revisions`（接受修訂後再解析）| ✅ user 驗過 |
| D | 新建資源庫「沒勾控制項」誤報 + 建立打到停用端點 | FE 抓 v2 永遠 null 的 `catalog_control_uid`；且 POST `/module-frame`（add_module_frame 在 v2 已 return None）| FE 改抓 `control_id` + 改打 `RESOURCE_LIBRARY_CREATE`（對齊 ModuleFrame.vue）| ✅ user 驗過 |
| E | 評估目標 0 / AO 現況沒帶 | parser ao_map 用字母 key `(a)`，v2 candidate.objective_keys 是 part_id `..._obj.1` → `key in ao_map` 永不命中 | parser `_map_ao_snippets`：用 catalog 描述開頭 `[a]` 把 part_id 橋接回字母 | ✅ user 驗過 |
| F | 發布資源庫「建立專案」灰掉 | list dto 漏回 `enable`（FE `disabled: !rowData.enable`）| dto 補 `enable` | ✅ user 驗過 |
| G | 匯入內容編輯頁讀不到（控制項/AO 現況空白）| 匯入寫 by-component.description（OSCAL 標準），但編輯頁讀 IR.props 自訂 key | **採 A**：匯入也寫 IR.props `implementation-description` + statement.props（letter key `(a)` 對齊 `aoLetterKey`）。by-component 全遷移列大修正延後（見 analysis doc）| ✅ 控制項/AO 現況 user 驗過 |
| H | 實作狀態沒帶出來 | docx 無「狀態」欄,parser 解不出 → soa_props 沒東西寫 | 有現況描述者預設 `implemented`（之後編輯頁可改）| ✅ user 初步驗過(2026-06-16) |
| I | 既有資源庫重匯報錯 / 變 append | confirm 對既有非空範本送 `mode="create"` → already-has-a-body 報錯;改 update 後 v3 實體(元件/人員 by title/name)又「只加不刪」累積 | **Replace**：套件加 `OscalIoService.clear_ssp_body`,confirm 既有路徑改 decision-merge → clear → create | ✅ user 初步驗過(2026-06-16) |

### 架構決策（已歸檔）
- **SoA 實作值存放 IR.props vs by-component**：本次採 **A（維持 IR.props 全系統慣例，FedRAMP SoA pattern）**；by-component 全 OSCAL-native 遷移列**獨立大修正延後**（牽動範本+專案編輯頁+匯出+啟動專案+匯入 ~6 service）。決策軌跡 / 反悔條件 / 落點清單見 `docs/analysis/2026-06-16-soa-implementation-storage-ir-props-vs-by-component.md`。

### 本段改動檔（UNCOMMITTED）
- BE：`app/oscal/service/ssp_docx_import_app_service.py`（get_parse_result 攤平 / _normalize_docx_revisions 還原 / confirm Replace）、`domain/oscal/parser/docx_parser_core.py`（_map_ao_snippets）、`app/oscal/service/import_adapter/docx_to_oscal_ssp.py`（IR.props+statement.props+letter key+status 預設）、`app/oscal/service/resource_library_app_service.py`（dto enable）
- 套件 jedi_oscal_v2（dev path-dep）：`OscalIoService.clear_ssp_body`
- FE：`src/components/grc/ssp-docx-import-v2/SspDocxImportPage.vue`
- migration：`scripts/sql/2026-06-16-restore-ssp-parse-jobs-v2.sql`（已套 DEV）

### 仍未做
- **Excel 匯入**：同一套寫法,推測有相同問題（IR.props / statement_id / 狀態預設）—— 未驗未修
- **SSP 文件匯出（docx/pdf/odt）**：未驗
- **by-component 全遷移**（大修正,延後,見 analysis + Notion）
- 收尾未做：commit / changelog / 對話歸檔 / 橫向文件

## 改動檔清單（全部 UNCOMMITTED）

**BE（12）**：
- `app/oscal/service/resource_library_app_service.py`（get_profile_control_tree / get_template_ssp_ref / _init_ao_workflows / update_applicable_controls / create id 對映 + 保留 group/version/frequency/provider / list enrich）
- `app/module_frame/service/module_frame_service.py`（detail enrich oscal_profile / update_module_frame 重寫）
- `app/module_frame/service/module_frame_control_default_service.py`（重寫走 ssp_service）
- `app/module_frame/service/module_frame_control_objective_default_service.py`（重寫走 ssp_service statement）
- `app/module_frame/service/module_frame_reference_document_service.py`（mapping context 改 v2 IR/statement）
- `api/oscal/__init__.py`（註冊 template-ssp）/ `api/oscal/routes/module_frame_template_ssp_route.py`（改走 resource_library）/ `api/oscal/routes/resource_library_route.py`（create 收 group/version/frequency/provider）
- `config/di_modules.py`（template-ssp route re-enable）
- `di_containers/containers.py`（oscal_container 接 workflow_execution_container）/ `di_containers/oscal/oscal_containers.py`（workflow_execution_container DependenciesContainer + resource_library 注入 workflow_template_service）/ `di_containers/module_frame/module_frame_containers.py`（control_default / objective_default / reference_document service 改注 ssp_service）

**FE（4）**：
- `src/config/api/api.js`（RESOURCE_LIBRARIES_LIST / RESOURCE_LIBRARY_CREATE / RESOURCE_LIBRARY 常數）
- `src/views/module_frame/ModuleFrame.vue`（列表/新增改打 resource-library 端點 + 控制項 picker 改 catalog-tree + 框架欄防 null）
- `src/views/module_frame/ModuleFrameTemplateEditView.vue`（程序書 picker 顯示/搜尋/reload）
- `src/components/grc/EditIncludeControlsDialog.vue`（catalog-tree + control_id pre-select）

## 待辦 follow-up

1. **重啟 BE 驗 #7（編輯適用控制項增減）+ #8（程序書掛載）** —— 本輪最後兩項只 curl/邏輯驗到一半。
2. **批次維護匯入匯出**：獨立 task（建 2 張 parse-job 表 migration + 2 service v2 重寫 + ssp-export 驗證）。
3. 收尾未做：commit / changelog / 橫向文件（fe-apply-comparison.md 的資源庫列該翻成「核心 SoA done / 匯入匯出 pending」）/ Notion。

## 測試資料（DEV 188/guidant_ai_dev）

本輪建的 verify 庫：`verify-soa`(57f82ee7) / `verify-bpmn`(f364552b) / `測試匯入合規資料庫`(fb1bc3aa,13 程序書) 等;清理時可一併刪。
