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


§1

完成範圍

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

§2

產出物總覽

新增檔案

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 掛入

§3

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 已確認)

§4

架構關鍵決策(本 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)。


§5

資料流

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)

§6

已知 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 完成後接

§7

未完成的 Track B

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

§8

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


§9

下個 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