# SSP Doc Parser v2 — Implementation Plan

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

## 階段切分總覽

| 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 個工作天

---

## 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 既有測試覆蓋盤點

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

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

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

```python
# 手動驗證
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，只做盤點 + 確認決策**

---

## Phase 1：BE Schema migration

### 1.1 寫 migration SQL

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

```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 個新欄位

---

## Phase 2：BE Parser 分層

### 2.1 新增 `ISspDocxAdapter` interface

`domain/oscal/adapter/i_ssp_docx_adapter.py`：

```python
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`：

```python
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`：

```python
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 不爆

---

## 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 結果一致

---

## 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

---

## 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

```python
@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 全綠

---

## Phase 6：FE 獨立頁 shell

### 6.1 新 route 註冊

`src/config/router/index.js`：

```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 可拖拉

---

## 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）

---

## Phase 8：FE LocalStorage 同步

### 8.1 useSspDocxDraft composable

`src/composables/useSspDocxDraft.js`：

```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 不存在

---

## 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
- 進編輯頁可看到 ⚠ 標記
- 點補連結 → 搜尋 → 連結成功 → 重新整理顯示 ✓

---

## Phase 10：FE 入口替換

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

`ModuleFrame.vue` 工具列：
- 既有「新增」按鈕保留（手動建立路徑）
- 新增「從 docx 建立」按鈕 → 跳 import-docx route

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

```vue
<!-- 移除這整段 -->
<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 行為正確

---

## Phase 11：v1 Cleanup

### 11.1 廢棄 dialog component

```bash
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 資料正常顯示

---

## Phase 12：整合測試

### 12.1 對真實 docx 跑 e2e

```python
# 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 兩條路都跑得通

---

## 依賴關係圖

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

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

## 預期交付物

每個 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。
