SSP Doc Parser v2 — Implementation Plan

對應 design-v2.md 預期執行模式:subagent-driven development(每 phase 結束會打 commit) 開工條件:使用者明確說「開工」

§1

階段切分總覽

Phase 範圍 預估 commits 阻擋下一階段?
0. 前置 既有 v1 程式碼盤點、cleanup 順序確認 0(純規劃)
1. BE Schema oscal_parties 加 4 欄 + index + GRANT 1 migration
2. BE Parser 分層 抽 ISspDocxAdapter interface + CmmcSspAdapter 殼 3-4 ❌(v1 並存)
3. BE Adapter 內容擴充 CmmcSspAdapter 抓 metadata / parties / leveraged 4-6
4. BE Write Strategy 擴充 寫入 parties / leveraged / metadata(不影響既有 controls 寫入) 3-4
5. BE Confirm flow 統一 接收 v2 payload;create + update 雙 mode 2-3
6. FE 獨立頁 shell route + ImportDocxPage.vue + Splitter 佈局 2
7. FE Section components 5 個 SectionComponent + PartyLinkDialog 5-7
8. FE LocalStorage 同步 useSspDocxDraft composable 1
9. FE 補連結 UI 編輯現況與程序書頁加「參與單位 / 人員」section 2-3
10. FE 入口替換 合規資源庫管理首頁加「從 docx 建立」按鈕 + 拆掉舊 dialog 2
11. v1 cleanup 廢棄 SspDocxImportDialog / PreselectSummary / chain confirm 2-3
12. 整合測試 smoke + e2e + i18n 補齊 2-3

預估總 commits:30-40 預估時間(subagent-driven,含 review):3-5 個工作天


§2

Phase 0:前置

0.1 v1 程式碼盤點(read-only)

確認下列 v1 元素的廢棄順序,避免 v2 寫到一半既有 user 的 docx 匯入流程壞掉:

v1 元素 v2 廢棄時機
SspDocxImportDialog.vue Phase 11
PreselectSummary.vue Phase 11
StepUpload/StepPreview/StepResult.vue Phase 11
ModuleFrame.vue「從 docx 預選」按鈕 Phase 10
ModuleFrameTemplateEditView 批次維護 docx 入口 Phase 10(換 route)
ProjectPlanningView 批次維護 docx 入口 Phase 10(換 route)
BE mode='full' chain confirm 邏輯 Phase 5
BE pending_parse_uid 邏輯 Phase 5

0.2 既有測試覆蓋盤點

pytest tests/ -k 'docx or write_strategy' -q --collect-only | head -30

確認 v2 開工前哪些既有 test 會被影響、哪些可繼續用、哪些要重寫。

0.3 確認 jedi-file-upload 的 PDF 轉檔可用

# 手動驗證
from jedi_file_upload.service.file_upload_service import FileUploadService
# upload 一份測試 docx → 驗 GET /upload-file/<uid>/preview-as-pdf 可正常 render

驗證內容:

  • 大檔(10MB+)是否會 timeout
  • 圖檔密集 docx 是否轉得出 PDF
  • 中文字型是否正常

Phase 0 不寫 code,只做盤點 + 確認決策


§3

Phase 1:BE Schema migration

1.1 寫 migration SQL

scripts/sql/2026-XX-XX-oscal-parties-extensions.sql

-- Date: 2026-XX-XX
-- 1. oscal_parties 加 4 欄供 SSP docx import 鉤稽 + 聯絡資料保留 (2026-XX-XX)
ALTER TABLE oscal.oscal_parties
  ADD COLUMN IF NOT EXISTS title varchar(255),
  ADD COLUMN IF NOT EXISTS email_address text,
  ADD COLUMN IF NOT EXISTS telephone_number varchar(50),
  ADD COLUMN IF NOT EXISTS address text,
  ADD COLUMN IF NOT EXISTS user_id integer,        -- soft ref public.members.id
  ADD COLUMN IF NOT EXISTS org_unit_id integer;    -- soft ref public.org_units.id

-- 2. 加 index (2026-XX-XX)
CREATE INDEX IF NOT EXISTS ix_oscal_parties_email
  ON oscal.oscal_parties(email_address);
CREATE INDEX IF NOT EXISTS ix_oscal_parties_user_id
  ON oscal.oscal_parties(user_id);
CREATE INDEX IF NOT EXISTS ix_oscal_parties_org_unit_id
  ON oscal.oscal_parties(org_unit_id);

1.2 ORM model + entity + mapper 同步更新

  • infra/oscal/models/... 加欄位
  • domain/oscal/entity/oscal_party_entity.py 加 fields
  • infra/oscal/mappers/... 加 mapping

1.3 changelog

docs/changelog/2026-XX-XX-feat-ssp-docx-v2-oscal-parties-extensions.md

驗收

  • migration 在 dev DB 跑成功,欄位存在
  • 既有測試全綠(schema 純加欄位不破壞)
  • ORM 可讀寫 4 個新欄位

§4

Phase 2:BE Parser 分層

2.1 新增 ISspDocxAdapter interface

domain/oscal/adapter/i_ssp_docx_adapter.py

from abc import ABC, abstractmethod

class ISspDocxAdapter(ABC):
    @abstractmethod
    def adapt(
        self,
        parsed: 'ParsedDocx',  # 既有 intermediate
        candidates: list,
    ) -> 'ParsedSsp':
        """Map raw parsed docx structure to OSCAL-aligned ParsedSsp."""

2.2 新 intermediate ParsedSsp

domain/oscal/parser/ssp_intermediate.py:定義 dataclasses:

  • ParsedMetadata
  • ParsedSystemCharacteristics
  • ParsedParty
  • ParsedLeveragedService
  • ParsedInformationType
  • ParsedSsp(容器)

2.3 CmmcSspAdapter

domain/oscal/adapter/cmmc_ssp_adapter.py

class CmmcSspAdapter(ISspDocxAdapter):
    def adapt(self, parsed_docx, candidates):
        # v2 Phase 2 殼版:先把既有 controls + AOs 直接轉過來
        # parties / leveraged / metadata 留 TODO 給 Phase 3
        return ParsedSsp(
            metadata=...,                       # TODO Phase 3
            parties=[],                         # TODO Phase 3
            leveraged_services=[],              # TODO Phase 3
            matched_controls=parsed_docx.matched_controls,
            unmatched_paragraphs=parsed_docx.unmatched_paragraphs,
            information_types=[],
            summary=parsed_docx.summary,
            framework_specific_props={},
        )

2.4 DI 註冊

di_containers/oscal/oscal_containers.py

cmmc_ssp_adapter = providers.Factory(CmmcSspAdapter)
adapter_registry = providers.Factory(
    AdapterRegistry,
    adapters={'cmmc-l1': cmmc_ssp_adapter, 'cmmc-l2': cmmc_ssp_adapter}
)

2.5 既有 parser 不動

DocxParserCore 維持原樣,只是它的輸出(ParsedDocx)會被 adapter 多一層轉換。

2.6 changelog + tests

  • tests/test_cmmc_ssp_adapter.py:覆蓋 controls + AOs 直接 passthrough 的情境
  • changelog 2026-XX-XX-feat-ssp-docx-v2-adapter-layer.md

驗收

  • DocxParserCore 既有 92 tests 全綠
  • 新 adapter 殼 tests 通過
  • DI container start-up 不爆

§5

Phase 3:BE Adapter 內容擴充

每個小 step 都有 RED → GREEN test。

3.1 metadata 抽取(封面 T0)

CmmcSspAdapter 加 _extract_metadata(structure_paragraphs, tables) -> ParsedMetadata

  • 文件編號(regex match Ser. NO[::]
  • Version
  • Issue Date
  • 組織名

3.2 parties 抽取(Introduction 5 張表 T1-T5)

CmmcSspAdapter 加 _extract_parties(structure_paragraphs, tables) -> list[ParsedParty]

  • 對應 5 個固定角色
  • 抽 Name / Title / Email / Phone / Address

3.3 leveraged services 抽取(T6 + T7)

CmmcSspAdapter 加 _extract_leveraged_services(tables) -> list[ParsedLeveragedService]

  • Leveraged FedRAMP
  • 找 External Systems / Services 表
  • 合併兩表內容

3.4 鉤稽(adapter 內呼叫 user / org_unit service)

需要新增兩個 helper(adapter 注入 dependency):

  • match_party_to_user(party, tenant_id) -> Optional[int]
  • match_party_to_org_unit(party, tenant_id) -> Optional[int]

放在 adapter 接收的 dependency 內,避免 adapter 直接 import infra repos。

3.5 information types 抽取(FCI 類型清單)

從 Introduction 段落抓條列項目:

  • 合約文件 / 行政資訊 / 指導文件 / etc.
  • 暫存到 framework_specific_props['cmmc:fci_types']

3.6 changelog + tests

每小步一個 commit + test:

  • 2026-XX-XX-feat-ssp-docx-v2-adapter-metadata.md
  • 2026-XX-XX-feat-ssp-docx-v2-adapter-parties.md
  • 2026-XX-XX-feat-ssp-docx-v2-adapter-leveraged.md
  • 2026-XX-XX-feat-ssp-docx-v2-adapter-info-types.md

驗收

  • ASIA-CMMC-SSP-DRAFT-202604.docx 跑 e2e parse + adapt:
    • metadata: 文件編號 / version / issue date 都抽出(即便是樣板字也回 None)
    • parties: 5 個角色物件,含 email/phone/title
    • leveraged: T6 + T7 內容合併
    • controls + AOs 與 v1 結果一致

§6

Phase 4:BE Write Strategy 擴充

4.1 ModuleFrameWriteStrategy 擴充

新增方法:

  • _write_metadata(parsed_metadata, ssp_id, user) 寫 oscal_metadatas
  • _write_parties(parsed_parties, context_type, context_id, user) 寫 oscal_parties + responsible_parties
  • _write_leveraged_services(parsed_services, ssp_id, user) 寫 ssp_system_implementations type='leveraged' + props
  • _write_information_types(parsed_types, ssp_id, user) 寫 oscal_props (cmmc:fci-type)

既有的 _upsert_control_default / _upsert_objective_default 不動。

4.2 SspWriteStrategy 同步擴充

對應 SSP 路徑:

  • 寫到 oscal_metadatas (跟 SSP 連結)
  • 寫到 oscal_parties + responsible_parties (context_type='ssp')
  • 寫到 SSP 自己的 system_implementations / props

4.3 changelog + tests

對寫入策略補測試:

  • tests/test_module_frame_write_strategy.py 補 parties / leveraged / metadata 寫入 case
  • tests/test_ssp_write_strategy.py 同上

驗收

  • 既有 92 tests 全綠
  • 新測試覆蓋 parties / leveraged / metadata 寫入
  • 寫入後 DB 可查到對應 row

§7

Phase 5:BE Confirm flow 統一

5.1 移除 chain confirm

app/oscal/service/ssp_docx_import_app_service.py

  • confirm_import() 接受新版 payload(含 ParsedSsp 完整 patch)
  • v1 的 mode='full' + chain confirm 路徑簡化:BE 同 transaction 內:
    1. 建 module_frame(新增 mode)
    2. 寫 baseline (oscal_profile.include_controls)
    3. 寫 controls + AOs
    4. 寫 parties
    5. 寫 leveraged services
    6. 寫 metadata / props

5.2 新增 dual-mode confirm payload schema

@dataclass
class ConfirmImportPayload:
    parse_uid: str
    mode: Literal['create', 'update']
    edits: dict  # localStorage 帶過來的 edits patch
    # 若 mode='create':附 framework_version_uid + module_frame_basic(name 等)
    # 若 mode='update':附 target_uid (module_frame uid 或 ssp uid)
    target_uid: Optional[str] = None
    framework_version_uid: Optional[str] = None
    module_frame_basic: Optional[dict] = None

5.3 既有 v1 confirm 路徑相容

保留既有 confirm_import signature 一段時間,標記 @deprecated,待 Phase 11 移除。

5.4 changelog + tests

  • 2026-XX-XX-feat-ssp-docx-v2-unified-confirm.md
  • 既有 test_ssp_docx_import_app_service.py 加 v2 dual-mode tests

驗收

  • create mode confirm:建立完整 module_frame + 所有 OSCAL 資料
  • update mode confirm:更新既有 module_frame / SSP,不動 baseline
  • 既有 v1 tests 全綠

§8

Phase 6:FE 獨立頁 shell

6.1 新 route 註冊

src/config/router/index.js

{
    path: '/compliance-resource/import-docx',
    name: 'compliance-resource-import-docx',
    component: () => import('@/views/compliance-framework/ImportDocxPage.vue'),
    props: { mode: 'create-mf' },
},
{
    path: '/compliance-resource/:uid/import-docx',
    name: 'compliance-resource-update-docx',
    component: () => import('@/views/compliance-framework/ImportDocxPage.vue'),
    props: route => ({ mode: 'update-mf', targetUid: route.params.uid }),
},
{
    path: '/project/:id/ssp-import-docx',
    ...
    props: route => ({ mode: 'update-ssp', projectId: route.params.id }),
},

6.2 ImportDocxPage.vue shell

最小可用:

  • Header(標題 + 取消 / 確認按鈕)
  • Splitter 上下/左右
  • Left iframe placeholder
  • Right Fieldset 容器(內部各 SectionComponent 在 Phase 7 實作)
  • 上傳 input(StepUpload 邏輯抽出)

6.3 changelog + 視覺確認

  • 2026-XX-XX-feat-ssp-docx-v2-import-page-shell.md
  • 開 dev server 進新 route 看到 layout

驗收

  • 新 route 可訪問
  • 上傳 docx 觸發既有 parse API
  • 拿到 parse_uid 後 iframe URL 正確指向 PDF preview endpoint
  • Splitter 可拖拉

§9

Phase 7:FE Section components

依序實作 5 個 SectionComponent + 1 個 dialog:

7.1 MetadataSection.vue

讀寫 parsed_result.metadata:4 欄 InputText + DatePicker。

7.2 PartiesSection.vue

5 個 role 各一卡片,狀態 ✓/⚠ 顯示,「補連結」按鈕呼叫 PartyLinkDialog。

7.3 PartyLinkDialog.vue

  • person 模式:搜 system users by email/name
  • organization 模式:搜 org_units by name/code
  • 確認 → 寫 user_id / org_unit_id 到當前編輯狀態

7.4 LeveragedSection.vue

DataTable 列出 leveraged services,每行可編輯。

7.5 ControlImplSection.vue

繼承 v1 PreselectSummary 大部分邏輯:

  • expand/collapse 工具列
  • 控制項清單 + AO preview
  • 可編輯 implementation_description / objectives

7.6 UnmatchedSection.vue

直接搬 v1 PreselectSummary 內的「疑似漏掉的控制項 + 雜訊計數」邏輯。

7.7 changelog + tests

每個 section 一個 commit。

驗收

  • 對真實 docx 跑 import:右側 Fieldset 都填得到資料
  • 編輯任一欄位生效(暫存到 component state,Phase 8 接 localStorage)

§10

Phase 8:FE LocalStorage 同步

8.1 useSspDocxDraft composable

src/composables/useSspDocxDraft.js

export function useSspDocxDraft(parseUid) {
    const KEY = `ssp_docx_draft:${parseUid}`
    const draft = reactive(JSON.parse(localStorage.getItem(KEY) ?? '{}'))

    watch(draft, (val) => {
        localStorage.setItem(KEY, JSON.stringify(val))
    }, { deep: true })

    function clear() {
        localStorage.removeItem(KEY)
    }

    return { draft, clear }
}

8.2 ImportDocxPage 整合

mount 時:

  • 從 BE 拿 parsed_result
  • merge localStorage edits → 給各 SectionComponent

unmount / confirm 後:

  • draft.clear()

8.3 changelog + tests

  • 手動驗證 F5 不丟編輯狀態
  • 2026-XX-XX-feat-ssp-docx-v2-localstorage-draft.md

驗收

  • F5 / 切 tab 回來編輯狀態還在
  • confirm 成功後 localStorage 清乾淨
  • 重新上傳(新 parse_uid)→ 舊 key 不存在

§11

Phase 9:FE 補連結 UI

9.1 編輯現況與程序書頁加「參與單位 / 人員」section

ModuleFrameTemplateEditView.vue 加新 tab/section:

  • 列出該 module_frame 連結的 oscal_parties
  • 顯示連結狀態
  • [補連結] 按鈕呼叫 PartyLinkDialog(沿用 Phase 7 的)

9.2 BE API:list parties by module_frame

新 endpoint:

  • GET /module-frame/<uid>/parties → list with link status
  • PATCH /oscal-party/<uid>/link → 改 user_id / org_unit_id

9.3 SSP 編輯頁同步加(如有 SSP 版本編輯頁)

9.4 changelog + tests

  • 2026-XX-XX-feat-ssp-docx-v2-party-relink-ui.md
  • BE API 測試

驗收

  • 從 docx 匯入未連結 party
  • 進編輯頁可看到 ⚠ 標記
  • 點補連結 → 搜尋 → 連結成功 → 重新整理顯示 ✓

§12

Phase 10:FE 入口替換

10.1 合規資源庫管理首頁加「從 docx 建立」按鈕

ModuleFrame.vue 工具列:

  • 既有「新增」按鈕保留(手動建立路徑)
  • 新增「從 docx 建立」按鈕 → 跳 import-docx route

10.2 ModuleFrame.vue 新增 dialog 內 docx 預選按鈕移除

<!-- 移除這整段 -->
<div v-if="!currFrame.uid" class="mb-3 pt-3 border-top-1 surface-border">
    <Button :label="t('lang.ssp_docx_import.btn_import_controls_from_docx')" ... />
</div>

10.3 編輯模板頁批次維護 docx 入口改 route

ModuleFrameTemplateEditView.vue 批次維護 menu 內「從 docx 匯入」改成 router.push({name: 'compliance-resource-update-docx', params: {uid: moduleFrameUid.value}}),不再開 dialog。

10.4 專案規劃頁同步改

ProjectPlanningView.vue 同上,跳到 SSP route。

10.5 changelog

  • 2026-XX-XX-feat-ssp-docx-v2-replace-entry-points.md

驗收

  • 三條入口都跳到新獨立頁
  • 舊 dialog 不再出現
  • create / update 兩 mode 行為正確

§13

Phase 11:v1 Cleanup

11.1 廢棄 dialog component

git rm src/components/grc/SspDocxImportDialog.vue
git rm src/components/grc/ssp-docx-import/PreselectSummary.vue
git rm src/components/grc/ssp-docx-import/StepUpload.vue
git rm src/components/grc/ssp-docx-import/StepPreview.vue
git rm src/components/grc/ssp-docx-import/StepResult.vue

11.2 廢棄 store

src/stores/sspDocxImport.js → 改用 v2 store 或直接刪。

11.3 BE 廢棄 chain confirm

app/oscal/service/ssp_docx_import_app_service.py

  • 移除 mode='full' 特殊 chain logic
  • 移除 v1 confirm fallback

11.4 i18n 清理

移除 wizard / preselect 相關 i18n key。

11.5 changelog

  • 2026-XX-XX-cleanup-ssp-docx-v1-deprecated.md

驗收

  • 全 codebase grep 沒有 v1 dialog component reference
  • 既有 module_frame / SSP 資料正常顯示

§14

Phase 12:整合測試

12.1 對真實 docx 跑 e2e

# scripts/e2e_test_ssp_docx_v2.py
# 1. 上傳 docx
# 2. 驗證 parse_result 內 metadata / parties / leveraged 都抽出
# 3. confirm create → 驗證 module_frame + 所有相關 row 都建好
# 4. 進編輯頁 → 驗證 parties section 顯示正確
# 5. 補連結 → 驗證 user_id / org_unit_id 寫入

12.2 i18n 補齊

中英文兩語系所有新 key。

12.3 release notes 更新

docs/features/FR-022-2604-ssp-doc-parser/release-notes-v2.md

12.4 changelog 收斂

整合 v2 changelog 索引。

驗收

  • 真實 docx e2e 通過
  • 所有測試(pytest)綠
  • 手動 smoke:create + update 兩條路都跑得通

§15

依賴關係圖

1 ─→ 2 ─→ 3 ─→ 4 ─→ 5 ─→ 10 ─→ 11
              ↓                    ↑
              └────→ 6 ─→ 7 ─→ 8 ─┘
                              ↓
                              9 ─→ 12

關鍵路徑:1 → 2 → 5 → 10 → 11,其他 phase 可平行。

§16

預期交付物

每個 phase 完成後產生:

  1. 對應 commit(s)
  2. changelog 在 docs/changelog/
  3. tests 全綠(pytest -k 'docx or write_strategy')
  4. 必要時 PR 描述

最終交付:docs/features/FR-022-2604-ssp-doc-parser/release-notes-v2.md 含完整上版 SOP。