# SSP Export B1~B4 完工 SUMMARY
> 日期：2026-05-22
> Session：B1 generator 骨架 → B2 content loader → B3 export API → B4 LibreOffice converter
> 分支：`feature/ssp-import-export-phase2`

---

## 完成範圍

| Phase | 主題 | 狀態 |
|-------|------|------|
| B1 | docxtpl 模板 + `SspDocxGenerator` 骨架 | ✅ shipped |
| B2 | `MfSspContentLoader` + `SspVersionContentLoader` | ✅ shipped |
| B3 | `SspExportAppService` + 2 routes + DI wiring | ✅ shipped |
| B4 | `SspLibreOfficeConverter` subprocess wrapper | ✅ shipped |

---

## 產出物總覽

### 新增檔案

```
app/oscal/service/export/
├── __init__.py                      (B1 同步建立)
├── ssp_export_model.py              (B1) 中間層 dataclass
├── ssp_docx_generator.py            (B1) docxtpl generator
├── ssp_mf_content_loader.py         (B2) MF 來源 loader
├── ssp_version_content_loader.py    (B2) SSP 版本來源 loader
├── ssp_export_app_service.py        (B3) 匯出 facade
└── ssp_libreoffice_converter.py     (B4) LibreOffice subprocess wrapper

app/oscal/templates/ssp/
└── ssp_cmmc_template.docx           (B1) docxtpl 模板（進版控）

scripts/
└── generate_ssp_docx_template.py    (B1) 模板產生腳本

api/module_frame/routes/
└── mf_ssp_export_route.py           (B3) GET /module-frame/<uid>/ssp-export

api/oscal/routes/ssp/
└── ssp_export_route.py              (B3) GET /ssp/<ssp_uid>/export
```

### 修改檔案

```
pyproject.toml                        docxtpl 相依新增
common/code/grc_error_code.py        GRC_SSP_EXPORT_INVALID_FORMAT (GRC_400075)
di_containers/oscal/oscal_containers.py  6 個 SSP export providers
api/module_frame/__init__.py          route 掛入
api/oscal/__init__.py                 route 掛入
```

---

## API 端點

```
GET /api/1.0/module-frame/<uid>/ssp-export?format=docx|pdf|odt
GET /api/1.0/ssp/<ssp_uid>/export?format=docx|pdf|odt
```

- 回傳 `send_file`（非 envelope）
- `ssp_uid` = `system_security_plans.uid`（SSP 版本 UID）
- 預設 `format=docx`
- pdf / odt 需 LibreOffice 已安裝（dev server 已確認）

---

## 架構關鍵決策（本 session 落地）

### D2：SspExportDataModel 中間層
兩個來源（MF / SSP 版本）都組裝成同一 dataclass，generator 不知道來源差異。

### D3：docxtpl（Jinja2-in-Word）
模板 `.docx` 進版控；改版面直接在 Word 編輯，不需改 Python code；
腳本 `scripts/generate_ssp_docx_template.py` 重跑可重建。

### docxtpl 關鍵限制（已解決）
`{%tr for %}` 是 row-level preprocessor，不可嵌套在段落層 `{% for %}` 內。
解法：所有迴圈移進 table row；`all_objectives` 在 context 層展平（含 `control_id`）。

---

## 資料流

```
MF 來源：
  module_frame_service.get_module_frame(uid)
  module_frame_party_service.list_parties(uid)
  module_frame_ssp_resources_service.list_resources(uid)  → devices / info_systems
  module_frame_control_default_service.list_all(uid)
  module_frame_control_objective_default_service.list_all(uid)
  module_frame_reference_document_service.list_pool(uid)
  → MfSspContentLoader.load(uid) → SspExportDataModel
  → SspDocxGenerator.generate(model) → BytesIO
  → SspLibreOfficeConverter.convert(buf, "pdf") → BytesIO（B4）

SSP 版本來源：
  ssp_domain_service.get_by_uid(ssp_uid)      → ssp.system_characteristic / oscal_metadata.parties
  ctrl_domain_service.get_all_by_ssp_id(id)   → ControlImplementationEntity[](含 objectives)
  → SspVersionContentLoader.load(ssp_uid) → SspExportDataModel
  → (同上 generator)
```

---

## 已知 Follow-up

| 項目 | 說明 |
|------|------|
| MF leveraged 空清單 | MF 沒有 leveraged_authorizations 資料路徑，目前回傳空清單。若需要，要在 `ssp_system_implementation_items` 補 MF scope leveraged 的查詢 |
| SSP version 程序書空清單 | `ssp_reference_documents` 路徑留給 B5 補；目前 `SspVersionContentLoader` 回傳空清單 |
| `libreoffice` 路徑 | DI 預設 `"libreoffice"`；若 prod server 路徑不同，需調整 `SspLibreOfficeConverter(libreoffice_cmd=...)` |
| B5 OSCAL JSON/XML/YAML | 重型 phase，需修改 jedi-oscal `ssp_yaml_mapper.py`（補 parties / AO statements / back-matter）；下個 session 獨立處理 |
| B6 FE UI | export 按鈕 + 格式 dialog，等 B5 完成後接 |

---

## 未完成的 Track B

| Phase | 主題 | 說明 |
|-------|------|------|
| B5 | OSCAL JSON/XML/YAML | 重型，需修 jedi-oscal；見下方交接 prompt |
| B6 | FE export 按鈕 | 等 B5 完成 |

---

## Smoke Test 結果（B1 驗收）

```
✅ Docx generated: 38,645 bytes
✅ No unrendered Jinja2 blocks
✅ 'Guidant AI Platform' in output
✅ 'Raymond Chou' in output
✅ 'AC.1.001' in output  (control)
✅ 'AC.1.001.a' in output (AO)
✅ 'AWS GovCloud' in output (leveraged)
✅ 'SEC-001' in output (reference doc)
```

B2~B4 為純 service 層組裝，無法在本 session 做 E2E smoke（需 DB context）。
建議 B5 開工前手動測試：用 dev 環境的 MF uid 呼叫 `GET /api/1.0/module-frame/<uid>/ssp-export`。

---

## 下個 Session 交接 Prompt

下個 session 貼以下內容：

```
接手 SSP Import/Export Phase 2 Track B — B5 OSCAL 序列化

已完成：B1（docxtpl generator）、B2（content loaders）、B3（export API）、B4（LibreOffice）
目前 API 支援 docx / pdf / odt，B5 要加 json / xml / yaml 格式。

B5 工作範圍：

1. jedi-oscal ssp_yaml_mapper.py 補 4 個缺口：
   - AO statements：ci.objectives → OSCAL statements[]
     路徑：ssp_control_implementation_objectives，ControlImplementationObjectiveEntity
     欄位：statement_identifier, implementation_description, implementation_status, remarks
   - parties + responsible-parties：metadata.parties（PartyEntity 已有 email_address/telephone_number/address）
   - back-matter resources：ssp_reference_documents（context_type='ssp'）
     目前 SspVersionContentLoader.reference_documents 回傳空清單，需補路徑
   - （system-characteristics 缺口 4 已確認不需補：DB 無 CIA / network-architecture 欄位）

2. SspExportAppService 加 format=json|yaml|xml 支援：
   - 先用 ssp_domain_service.get_by_uid(ssp_uid) 取 entity
   - 呼叫 jedi-oscal yaml_mapper 或新的序列化方法
   - JSON: json.dumps(entity_to_dict(...))
   - YAML: yaml.safe_dump(entity_to_dict(...))
   - XML: xmltodict.unparse + xmlns namespace patch

3. Routes 加 json/yaml/xml mimetype + filename

開發規範提醒：
- jedi-oscal 修改期間在 pyproject.toml 用 path dependency（取消 tool.poetry.dev-dependencies 的 jedi-oscal 那行註解）
- 完成才發版，不提前 bump
- 每個 phase ship 後更新 docs/features/FR-011.2-2605-ssp-import-export-phase2/README.md

關鍵檔案座標：
- tracker: docs/features/FR-011.2-2605-ssp-import-export-phase2/README.md
- B phase 分析: docs/features/FR-011.2-2605-ssp-import-export-phase2/handoff/2026-05-21-b-phase-analysis.md
- yaml_mapper: ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/app/services/ssp/ssp_yaml_mapper.py（路徑待確認）
- content loaders: app/oscal/service/export/ssp_*_content_loader.py
- export app service: app/oscal/service/export/ssp_export_app_service.py
```
