# FR-038 import-ssp — 資源庫範本 SSP 匯入（docx + excel，落 v2 schema）設計

> 狀態：設計定稿（2026-06-15，與 user 互動拍板）。本檔為 brainstorm 收斂結果。
> 上游：requirement-analysis / design §4.2、§4.4 / api-contract §2 / 2A handoff §10（import-ssp defer）。
> FR-038 Wave 2 收尾項：把唯一還沒接 v2 的 BE 功能（SSP 匯入）補上。

---

## 1. 問題（白話）

受評公司 / 顧問常常**已經有一份寫好的 SSP 文件（Word 或 Excel）**。目前要把內容搬進系統，只能
人工一條條打。import-ssp = 上傳該文件 → 系統自動解析 → 填進「答案卷（SSP）」。

**落點 = 合規資源庫的「範本 SSP」**（公版範本）：顧問/管理員建一份標準範本 SSP 放資源庫，之後
每個新專案成立時 clone 這份範本當起點（B2 已有 clone 機制）。不是填某個專案自己的 SSP。

UI 掛在合規資源庫編輯頁（FE `ModuleFrameTemplateEditView.vue`，已是 v2：`module-frame-import-docx-update`
/ `module-frame-import-excel-update` 路由 + `ssp-docx-import-v2` 頁）。

### 兩種匯入模型（user 2026-06-15 拍板：兩種都做）
v1 confirm 以 `source_type` 分派三條（`_VALID_SOURCE_TYPES={framework_version, module_frame, ssp}`）：

| source_type | v1 flow | 模型 | 本期 |
|-------------|---------|------|------|
| `framework_version` | `_confirm_superset_flow` | **Model B：從文件建新資源庫**（選框架版本 → 建 module_frame + profile + 填範本 SSP）| ✅ 做 |
| `module_frame` | `_confirm_update_flow` | **Model A：更新既有資源庫**的範本 SSP（FE `...-update` 路由）| ✅ 做 |
| `ssp` | `_confirm_ssp_update_flow` | 專案 SSP（Project Planning）| ❌ 範圍外 |

- **兩條都靠 P1 `OscalIoService.import_ssp` 填範本 SSP**（共同核心）。差別：Model B 多一步「先建殼」
  —— 從 framework_version clone catalog→profile→空範本 SSP（**= B1 既有 clone_resource_library 能力**），
  再 import_ssp 填；Model A 直接解析既有 module_frame 的 `template_ssp_id` 來填/更新。
- **本質是 update 流程**：FE 路由名為 `...-update`，程式碼註解「ensure_shell 後填入」。**首次匯入空殼
  = create（P1~P3 涵蓋）；再次匯入既有內容 = update-diff（P4，核心非附加）**。Model B 每次建新資源庫
  =永遠 create；Model A 才會碰 update-diff。

### 為什麼現在卡住
- v1 匯入管線**完整但 dark**：route（docx / excel / ssp-scoped excel）、app service、parser
  （`DocxParserCore` / `ExcelParser`，OSCAL-agnostic）、parse-job 表、reconciliation、
  Phase 2 update-diff、8+ 測試 —— 都還在，但被 `config/di_modules.py:EXCLUDE_MODULES` 擋掉。
- 擋掉原因：confirm 的**寫入核心**（`domain/oscal/strategy/ssp_write_strategy.py`、
  `module_frame_write_strategy.py`）還 import 舊 `jedi_oscal` v1 entity（`ControlImplementationEntity`
  / `ControlImplementationObjectiveEntity` / `PartyEntity` / `ResponsiblePartyEntity`），與 v2
  撞同一 SQLAlchemy MetaData，wire 進去 BE 就炸。
- 套件 `jedi_oscal_v2` 的 `OscalIoService` 只有 `export_oscal`（已 ship），**import 整個不存在**。

### 已驗證可重用 vs 須重建
| 區塊 | 狀態 | 處置 |
|------|------|------|
| `DocxParserCore` / `ExcelParser`（解析） | OSCAL-agnostic，不依賴 v1/v2 | **重用** |
| parse-job 表 + domain service（暫存解析結果 + 狀態機） | 主專案、與 v1 OSCAL 無關 | **重用** |
| route + app service 的 preview / confirm / discard 編排 | 主專案；confirm 內呼 v1 strategy | **重用編排，換寫入** |
| party reconciliation（解析出的人 ↔ DB user 配對） | 主專案 | **重用** |
| Phase 2 update-diff 標注（parsed vs current） | 主專案；current 端讀 v1 entity | **重用編排，current 端改讀 v2** |
| **寫入核心**（control-impl / SoA / statements / parties / SC） | v1 `jedi_oscal` strategy | **重建於 v2（套件）** |
| `SspImportConfirmService`（v2-bundle confirm） | 只寫 LA / component / inventory（FR-032 era） | 併入新寫入路徑，補齊核心 |

---

## 2. 已驗證地基（2026-06-15 讀 live code）

- 套件 `jedi_oscal_v2` 寫入機具齊全：14 個 SSP repo/entity（`SspControlImplementation` /
  `SspImplementedRequirement` / `SspStatement` / `SspByComponent` / `SspSystemCharacteristics` /
  `SspComponent` / `SspLeveragedAuthorization` / `SspInventoryItem` 等）+ `SspCloneService.deep_clone_ssp`
  已示範 12 子表完整寫入 pattern（含 by-component 的 `component_id` 軟指標 remap）。
- 套件 `OscalIoService.export_oscal` → `_export_ssp` / `_build_metadata` / `_build_system_characteristics`
  / `_build_system_implementation` / `_build_control_implementation`：import 是其精確反向，照同一結構鏡像。
- 資源庫範本 SSP 由 B1 建殼：`module_frames.template_ssp_id` 指該 SSP；import 解析 resource-library
  `{uid}` → `template_ssp_id` → 寫入該 ssp_id。

> ⚠️ 實作前 pre-flight（per memory `feedback_preflight_before_touching_pipeline`）：每 phase 開工
> 必先讀關鍵檔本身（parser 輸出 dataclass 欄位、export `_build_*` 實際結構、SspCloneService 寫入
> 順序、B1 建殼後 template SSP 是否已有 implemented_requirements），不憑本設計摘要刻。欄位級
> 對照在 implementation-plan 各 phase pre-flight 段釘死。

---

## 3. 架構與邊界（locked）

```
┌─ 主專案（compliance-manager-be）────────────────────────────────┐
│  route（re-enable）→ app service（preview / confirm / discard 編排）│
│    ├─ DocxParserCore / ExcelParser     → 解析成「正規化 SSP 中介」   │
│    ├─ party reconciliation             → 解析人 ↔ DB user            │
│    ├─ update-diff 標注                  → 中介 vs 套件 snapshot       │
│    └─ confirm 寫入  ── delegate ──▶                                  │
└──────────────────────────────────────────┼──────────────────────────┘
                                            ▼
┌─ 套件（jedi-oscal-v2 OscalIoService）──────────────────────────────┐
│  import_ssp(intermediate, *, target_ssp_id, catalog_id, mode, user) │
│    → 把正規化中介寫成 v2 SSP entity 樹（鏡像 export / clone pattern）│
│  build_ssp_snapshot(ssp_id) → 可比對 dict（給主專案 diff 用）        │
└─────────────────────────────────────────────────────────────────────┘
```

### 三條邊界決策
| # | 決策 | 值 |
|---|------|----|
| D-IS-1 | 落點 | 合規資源庫範本 SSP（`module_frames.template_ssp_id`）。**兩種模型都做**：Model A（`module_frame` source，填/更新既有）+ Model B（`framework_version` source，先建殼=clone_resource_library 再填）。`ssp` source（專案）範圍外 |
| D-IS-2 | 寫入 mapper 位置 | **套件 `OscalIoService.import_ssp`**（與 export 對稱，套件擁有 OSCAL↔entity 知識）|
| D-IS-3 | 正規化中介契約 | **主專案 parse 端定義、套件 import 端 consume**；穩定 dataclass（OSCAL-shaped、不綁 docx/excel）|

### 正規化中介契約（altitude；欄位級在 plan 釘）
一個 OSCAL-shaped、與來源格式無關的中介物，承載：
- **control implementations**：以 control-id 為鍵，每條含實作描述 + SoA（applicability / inclusion-justification
  / implementation-status，落 `props`）+ statements + by-components。
- **parties / roles**：解析出的人/單位（已經 reconciliation 配對 DB user / org）。
- **system-characteristics**：系統說明、邊界、資料流、資訊類型、圖。
- **system-implementation 子表**：components / leveraged-authorizations / inventory-items / system-users。

docx 與 excel parser 各自把自己的解析結果**正規化成此中介**（docx adapter / excel adapter 各一），
之後 confirm + diff + 套件 import 全部只認中介 → 兩格式共用下游，避免兩套寫入。

---

## 4. 階段拆解（逐段 ship，每段 smoke + 顯式 git add commit；照 Q1 cadence）

| Phase | 範圍 | 驗收 |
|-------|------|------|
| **P1 套件寫入核心 ✅ DONE** | `OscalIoService.import_ssp(oscal_ssp, *, target_ssp_id, curr_user, mode='create')`：OSCAL ssp dict（中介=export 同形狀）→ v2 SSP 樹。**涵蓋 export round-trip 面**：metadata roles/parties + system-characteristics + system-implementation/components + control-implementation/implemented-requirements（SoA props）。**statements / by-components / LA / inventory 不在 P1**（export 也不 round-trip；LA/component/inventory 另有 FR-032 v2-bundle 路徑）。create-only（空殼；非空丟錯）。pkg test `tests/io_export/test_oscal_import.py`。 | io_export 10 passed（含 round-trip + 非空 guard）；BE BOOT OK；BE pytest 持平 |
| **P2 Excel（兩 source）** | excel parsed → OSCAL ssp dict adapter；`SspExcelImportAppService` confirm 改 delegate `import_ssp`（丟 v1 strategy）；re-enable excel route。**Model A**（`module_frame` 解析 template_ssp_id 填）+ **Model B**（`framework_version` → 先 clone_resource_library 建殼[=B1 能力] → 再 import_ssp 填）。create 模式（首次/空殼）。 | e2e：(A) 既有資源庫上傳 excel → 範本 SSP 填好；(B) 選框架+上傳 → 建新資源庫且範本 SSP 填好；BOOT OK + pytest 不多紅 |
| **P3 Docx（兩 source）** | docx parsed → OSCAL ssp dict adapter；`SspDocxImportAppService` 同 P2（A+B）；re-enable docx route。 | e2e：docx → 兩 source 範本 SSP 填好 |
| **P4 update-diff（Model A 再匯入）✅ 實作（接回 v2，W2；smoke 待驗收）** | import_ssp 加 `mode='update'` + 套件 `build_ssp_snapshot`；current 端改讀 v2；移植 Phase 2 diff 標注（parsed vs current）；preview 逐項 diff_status；confirm 依 decision 在 OSCAL-dict 層 merge（W2，單一 writer）後寫入。**主要服務 Model A 再匯入**（Model B 永遠建新=不需 diff），並擴及專案 SSP（Model C）。 | 既有資源庫重複匯入 → side-by-side 差異 + 逐項決定生效。**單元/整合測試 + BOOT + baseline 零回歸已過；real-DB HTTP smoke 待 user 驗收（2026-06-16）** |

> P1 已完成（套件 commit `a930bfd`，OSCAL-dict 即中介、無獨立 dataclass、create-only）。中介契約
> = OSCAL ssp dict（與 export 同形狀），故 §3「正規化中介契約=dataclass」改為「= OSCAL ssp dict」；
> docx/excel adapter 產出此 dict。
> 每 phase 開工先 pre-flight 驗 live 路徑（§2 ⚠️）。改套件走 dev path-dep，不 commit pyproject 改動；
> 整弧完 + user 明示才發 Nexus + pin。FE 已是 v2（多數情況不需動 FE，BE 契約對齊既有頁面）。

---

## 5. 資料流（P2/P3 confirm，create 模式）

1. 上傳 docx/excel → 存檔（MinIO）→ 建 parse-job（status=pending）。
2. parser 解析 → adapter 正規化成中介 → party reconciliation → 存 parse-job.parsed_result（JSONB，status=awaiting_review）。
3. FE preview（GET parse-job）。
4. confirm：解析 resource-library `{uid}` → `template_ssp_id` → 主專案在 `@transaction` 內呼
   `OscalIoService.import_ssp(中介, target_ssp_id=template_ssp_id, catalog_id, mode='create', user)`
   → 套件寫 v2 SSP 樹 → 回 count 摘要 → parse-job status=completed。
5. （P4）confirm 前先 diff：中介 vs `build_ssp_snapshot(template_ssp_id)` → 標 diff_status → FE 逐項決定 → confirm 依 decision 寫。

---

## 6. 錯誤處理

- 沿用 `jedi_common.handler.exception` + `GrcErrorCode`（新碼 grep `common/code/grc_error_code.py` 最大值續編）：
  resource-library 不存在 / template_ssp_id 為空 / 解析失敗（落 parse-job.error_code+message，不 500）/
  control-id 對不到專案 catalog（warning 不擋，記 import_warnings）。
- 寫入在主專案 `@transaction` 內（套件 import_ssp 不自開 scope，caller 控制；mirror confirm_service 慣例）。
- reconciler / 非核心子項失敗 → logger.warning 不擋主流程（沿用 confirm_service Step 3 慣例，不 silent pass）。

## 7. 測試

- **套件**（P1）：單元測 import_ssp 各子樹寫入 + by-component remap + round-trip（export(import(x)) ≈ x）。
- **主專案**（P2/P3）：app service 測（logger patch fixture，per memory `feedback_test_logger_patch_db_handler`）；
  重用既有 8+ 匯入測試檔當回歸基準，逐一改成 v2 斷言。
- **e2e**（測試專案 compliance-manager-test）：上傳真 docx/excel → 資源庫範本 SSP 內容正確。
- 每 phase：BOOT OK + `python -m pytest test/` 不多於 baseline 紅（零新回歸）。

## 8. Scope / 非目標

- **本弧只做資源庫落點**；專案 SSP 落點機制可推廣但不在本弧 re-enable（D-IS-1）。
- ISO / NIST parser adapter 維持現狀（CMMC 焦點）；component-definition 不碰。
- 不重寫 parser（OSCAL-agnostic，直接重用）；不重做 FE preview/confirm UX（欄位形狀盡量不變）。
- 套件發 Nexus + pin 還原 → 整弧完 + user 明示（dev 走 path-dep）。
- v1 `ssp_write_strategy` / `module_frame_write_strategy` 退役（不刪檔，停用；確認無 live consumer 後另開 cleanup）。

## 9. 待 pre-flight 釘死的假設（plan 各 phase 開工驗）

- ⭐ **最高槓桿（P1 寫 mapper 前必先答）**：B1 建殼後 template SSP 是否已有
  `ssp_control_implementations` / `implemented_requirements` —— **決定 create 是純 insert 還是
  upsert + remap**，直接改變 P1 mapper 形狀。必須在 P1 寫 code 前驗，不可邊做邊發現。
- ⭐ **SoA prop key 名**（P1 pre-flight 釘）：從套件 `_build_control_implementation` 抓
  applicability / inclusion-justification / implementation-status 的**精確 prop name**，讓 docx
  與 excel adapter 對齊同一組鍵（否則兩格式寫出不同 props，下游 SoA 讀不到）。
- 正規化中介的精確欄位（讀 `ParsedExcel` / `ParsedDocx` / `ParsedSsp` dataclass 實體）。
- 套件 export `_build_*` 的精確 OSCAL 結構（import 鏡像）。
- resource-library `{uid}` → `template_ssp_id` 的解析路徑與權限（manager 守門）。
- adapter registry 現況（docx/excel adapter 怎麼註冊、CMMC adapter 是否可直接產中介）。
