# SSP Doc Parser — v2 Design

> 取代 v1 design（dialog-based + chain confirm hack）
> 設計討論完成日：2026-05-02
> v1 已上線 commit `565e8b5` (BE) / `1f66640` (FE)，本 v2 是針對下一輪重構

## 1. 目標

讓使用者把 SSP docx 範本一鍵建立 / 一鍵匯入到既有資源庫，
不再受 dialog 嵌套限制；解析範圍擴大到 metadata / parties / leveraged services。

## 2. 主要決策一覽

| # | 主題 | 決定 | 理由 |
|---|---|---|---|
| 1 | 文件預覽 | iframe `/upload-file/<uid>/preview-as-pdf` | 既有 endpoint 自動 docx→PDF，零新依賴 |
| 2 | 右側 panel | scrollable Fieldset + 每區獨立 SectionComponent | 跟合規框架預覽 UI 一致 |
| 3 | 左右同步滾動 | ❌ 不做（v1）| 第一版實作成本高，先觀察使用者需求 |
| 4 | 編輯持久化 | BE `parse_job.parsed_result` JSONB（7 天 TTL）+ FE localStorage（session 防丟）| BE 7 天足夠；localStorage 防 refresh |
| 5 | 重新上傳 | 新 parse_uid，舊的自然過期 | 簡單；user 確認多次上傳是罕見場景 |
| 6 | UI 流程 | 5-A revised：雙獨立入口（手動 / docx）| 取消 dialog 嵌套，docx 一條龍建立 |
| 7 | 帳號鉤稽 | email 完全相符（同 tenant）；soft reference `user_id integer` | 對齊既有 cross-schema FK 規範 |
| 8 | 未連結 party | 純文字保留（A 案）；編輯頁加「參與單位 / 人員」section 補連結 | 不污染 user 表，事後可補 |
| 9 | Org-unit 鉤稽 | name 完全相符；soft reference `org_unit_id integer` | 同 person 模式 |
| 10 | 多 framework | Parser core + framework adapter 兩層；CMMC 特例走 `oscal_props` | v1 只實作 CMMC，但不綁死 |
| 11 | UI 擴充性 | hard-coded Fieldset + SectionComponent 拆乾淨；不做 config-driven | v1 區塊有限，過度抽象不划算 |
| 12 | Storage 擴充性 | 核心走 column；偶爾擴充走 `oscal_props`；1:N 走新表 | 跟既有 OSCAL 設計對齊 |

## 3. UX 流程（5-A revised）

### 3.1 入口分布

```
[合規資源庫管理 首頁]
   ├─ 「手動新增」Button
   │   → Dialog (3 step: framework + baseline 勾選 + 確認)
   │   → 建立空殼 module_frame
   │   → redirect to 編輯現況與程序書頁
   │
   ├─ 「從 docx 建立」Button  ← 新增
   │   → /compliance-resource/import-docx (獨立頁，create mode)
   │
   └─ [既有資源庫] → 編輯現況與程序書頁
         └─ 批次維護 → 「從 docx 匯入」
             → /compliance-resource/<uid>/import-docx (獨立頁，update mode)

[專案規劃頁]
   └─ 批次維護 → 「從 docx 匯入」
       → /project/<id>/ssp-import-docx (獨立頁，SSP update mode)
```

### 3.2 獨立頁雙模式

| Mode | route | 可編輯範圍 |
|---|---|---|
| **create** | `/compliance-resource/import-docx` | framework / version / baseline / 全部內容 / parties |
| **update (MF)** | `/compliance-resource/<uid>/import-docx` | controls / AOs / parties；framework + baseline 唯讀 |
| **update (SSP)** | `/project/<id>/ssp-import-docx` | 同 update MF，目標寫入 SSP 而非 module_frame |

三條路共用同一個 Vue page component，差別只是 props（`mode` + 目標 uid）。

### 3.3 獨立頁佈局

```
┌─ Header ────────────────────────────────────────────┐
│ [從 docx 建立 / 匯入] [重新上傳] [取消] [確認]      │
└─────────────────────────────────────────────────────┘
┌─ Splitter ──────────────────────────────────────────┐
│ ┌─ Left (50%) ──────┐ ┌─ Right (50%) ─────────────┐ │
│ │                   │ │                            │ │
│ │ <iframe>          │ │ Fieldset 「基本資料」     │ │
│ │  原始 docx        │ │   <MetadataSection>       │ │
│ │  (PDF)            │ │ Fieldset 「參與單位/人員」│ │
│ │                   │ │   <PartiesSection>        │ │
│ │  scrollable       │ │ Fieldset 「外部利用服務」 │ │
│ │                   │ │   <LeveragedSection>      │ │
│ │                   │ │ Fieldset 「控制項實作」   │ │
│ │                   │ │   <ControlImplSection>    │ │
│ │                   │ │ Fieldset 「未匹配段落」   │ │
│ │                   │ │   <UnmatchedSection>      │ │
│ │                   │ │   scrollable              │ │
│ └───────────────────┘ └────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
```

### 3.4 操作流程

```
1. 點「從 docx 建立」/「從 docx 匯入」
   → 跳到獨立頁
2. user 上傳 docx
   → BE upload + parse → 拿 parse_uid + parsed_result
   → 左 iframe 顯示 PDF
   → 右各 Section 自動填入 parsed 資料
3. user 編輯任一欄位
   → 立即更新 localStorage[ssp_docx_draft:<parse_uid>]
4. user F5 / 切 tab 回來
   → mount 時從 localStorage 還原編輯狀態
5. user 重新上傳
   → 棄置舊 parse_uid（BE 7 天後過期）+ 清掉 localStorage key
   → 新 parse_uid → 從 step 2 重來
6. user 點「確認」
   → POST confirm with merged payload (BE parsed + localStorage edits)
   → BE 寫入 SSP/MF (含 parties / leveraged / controls / AOs)
   → 清 BE parse_job + FE localStorage
   → redirect to 編輯現況與程序書頁
7. 在編輯頁的「參與單位 / 人員」section 可補連結未連結的 party
```

## 4. 架構

### 4.1 Parser 分層（多 framework 預備）

```
┌─ DocxParserCore (既有，純結構抽取，framework-agnostic) ──┐
│   _extract_structure(doc) -> StructureParagraph[]/Table[] │
│   _is_list_item(p), _promote_pseudo_h3, _heading_level    │
└──────────────────────────────────────────────────────────┘
                          ↓
              StructureBundle (intermediate)
                          ↓
┌─ ISspDocxAdapter (新增 interface) ────────────────────────┐
│   adapt(structure, candidates) -> ParsedSsp               │
└──────────────────────────────────────────────────────────┘
                          ↓
              ┌───────────┴───────────┐
              ↓                       ↓
   CmmcSspAdapter (v1)        Iso27001SspAdapter (未來)
   - 抓 5 個固定角色            - ...
   - FCI 類型 → props
   - FedRAMP → leveraged
                          ↓
              ParsedSsp (OSCAL-aligned schema)
                          ↓
┌─ WriteStrategy (既有，已涵蓋 controls + AOs) ─────────────┐
│   ModuleFrameWriteStrategy / SspWriteStrategy             │
│   v2 擴充：寫 parties / leveraged-auth / metadata         │
└──────────────────────────────────────────────────────────┘
```

### 4.2 ParsedSsp Intermediate Schema

```python
@dataclass
class ParsedSsp:
    metadata: ParsedMetadata
    system_characteristics: ParsedSystemCharacteristics
    parties: list[ParsedParty]
    leveraged_services: list[ParsedLeveragedService]
    matched_controls: list[MatchedControl]      # 既有
    information_types: list[ParsedInformationType]
    unmatched_paragraphs: list[UnmatchedParagraph]  # 既有
    summary: dict
    framework_specific_props: dict[str, Any]    # CMMC FCI 等特例

@dataclass
class ParsedParty:
    name: str
    party_type: Literal['person', 'organization']
    title: str | None         # person 的職稱
    email_address: str | None
    telephone_number: str | None
    address: str | None
    role: str                  # 'system-owner' / 'security-officer' / ...
    matched_user_id: int | None        # email exact match → user_id
    matched_org_unit_id: int | None    # name exact match → org_unit_id
```

### 4.3 Storage

#### 4.3.1 oscal_parties schema 擴充

```sql
ALTER TABLE oscal.oscal_parties
  ADD COLUMN title varchar(255),
  ADD COLUMN email_address text,
  ADD COLUMN telephone_number varchar(50),
  ADD COLUMN address text,
  ADD COLUMN user_id integer,        -- soft ref to public.members.id
  ADD COLUMN org_unit_id integer;    -- soft ref to public.org_units.id

CREATE INDEX ix_oscal_parties_email ON oscal.oscal_parties(email_address);
CREATE INDEX ix_oscal_parties_user_id ON oscal.oscal_parties(user_id);
CREATE INDEX ix_oscal_parties_org_unit_id ON oscal.oscal_parties(org_unit_id);

GRANT SELECT, INSERT, UPDATE, DELETE ON oscal.oscal_parties TO cm_app;
```

> `user_id` / `org_unit_id` 二擇一 nullable：
> - `party_type='person'` → 用 `user_id`，`org_unit_id` 必為 NULL
> - `party_type='organization'` → 用 `org_unit_id`，`user_id` 必為 NULL

#### 4.3.2 parse_job 行為

```
ssp_docx_parse_jobs (既有表)
  ├─ uid
  ├─ source_type / source_uid    (update mode 用)
  ├─ framework / mode            (mode='full' 為 create / 'statement_only' 為 update)
  ├─ parsed_result JSONB         (immutable parser 輸出)
  ├─ status                      (awaiting_review / completed / discarded)
  ├─ created_at / created_user
  └─ ...

TTL: 7 天（cron 清 status='awaiting_review' 且 created_at < NOW() - 7d）
```

#### 4.3.3 CMMC 特例走 `oscal_props`

```
oscal_props (既有表，key-value)
  ├─ name='cmmc:fci-type'
  ├─ value='合約文件，包括與政府代表進行溝通...'
  ├─ prop_class='information_type_extension'
  └─ ns='http://cmmc.example.com'

oscal_metadata_props (M2M 既有)
  → 連結到對應 OSCAL 主節點
```

不新增 `fci_*` 欄位污染 OSCAL 通用 schema。

### 4.4 FE 結構

```
src/
├─ views/compliance-framework/
│   └─ ImportDocxPage.vue          ← 新增！create + update 共用
├─ views/project/
│   └─ ProjectSspImportDocxPage.vue ← create-style export 用 SSP 路徑
├─ components/grc/ssp-docx-import/
│   ├─ sections/                    ← v2 新增子目錄
│   │   ├─ MetadataSection.vue
│   │   ├─ PartiesSection.vue
│   │   ├─ LeveragedSection.vue
│   │   ├─ ControlImplSection.vue
│   │   ├─ UnmatchedSection.vue
│   │   └─ PartyLinkDialog.vue      ← 補連結 dialog
│   └─ ImportPageHeader.vue         ← header bar (按鈕區)
├─ stores/sspDocxImportV2.js        ← 重寫 store（捨棄 v1 wizard 邏輯）
└─ composables/useSspDocxDraft.js   ← localStorage 同步 helper
```

### 4.5 補連結 UI（編輯現況與程序書頁）

新增 tab/section「參與單位 / 人員」：

```
┌─ Tab 「參與單位 / 人員」──────────────────────────┐
│                                                    │
│ 角色：System Owner                                 │
│   王大明 (wang@inner.com)  ✓ 已連結 [User] [改連結] │
│                                                    │
│ 角色：Security Officer                             │
│   李小美 (lee@external.com)  ⚠ 未連結 [補連結]    │
│                                                    │
│ 角色：Responsible Organization                     │
│   AIR ASIA Co., Ltd.  ⚠ 未連結 [補連結]          │
│                                                    │
└────────────────────────────────────────────────────┘
```

點「補連結」→ `PartyLinkDialog`：
- 對 person：搜尋 system users by email/name
- 對 organization：搜尋 org_units by name/code

## 5. 各 Section 內容（右側 fieldset 詳細）

### 5.1 MetadataSection

| 欄位 | 來源 docx | OSCAL 欄位 | 編輯方式 |
|---|---|---|---|
| 文件編號 | 封面 T0 | `oscal_props (cmmc:document-serial-no)` | InputText |
| Version | 封面 T0 | `oscal_metadatas.version` | InputText |
| Issue Date | 封面 T0 | `oscal_metadatas.published` | DatePicker |
| 修訂紀錄 | Appendix 2 T24 | 用 `oscal_props` 暫存 list（v1）| 唯讀表 |

### 5.2 PartiesSection

每個角色顯示：
- Name / Title / Email / Phone / Address
- 連結狀態：✓ 已連結 / ⚠ 未連結
- 「補連結」按鈕（內嵌 PartyLinkDialog）

5 個固定角色（CMMC adapter 抽出）：
1. Responsible Organization (organization)
2. Information Provider (organization)
3. Information Receiver (person)
4. System Owner (person)
5. System Security Officer (person)

未來 framework 可不同（adapter 決定）。

### 5.3 LeveragedSection

T6 + T7 的內容合併顯示：
- 服務名稱
- 提供商
- 用途
- 協定 / Port
- FedRAMP Package ID（CMMC 特例，存 `oscal_props`）
- Agreement Type

每行可編輯。

### 5.4 ControlImplSection

跟現有 PreselectSummary 邏輯類似：
- 列出 matched_controls（含 score、impl preview、AO 列表）
- create mode：可勾選 / 取消（控制 baseline）
- update mode：唯讀控制項清單，只編輯內容
- 控制項點開可編輯 `parsed_implementation_description` markdown
- AO 子列每筆可編輯 `parsed_description`

### 5.5 UnmatchedSection

跟現有 PreselectSummary 一樣的「疑似漏掉的控制項 + 雜訊計數」分段呈現。

### 5.6 SystemCharacteristicsSection（如果獨立）

| 欄位 | OSCAL 欄位 |
|---|---|
| System Name | `system_security_plans_system_characteristics.name` |
| Description | `description` |
| Sensitivity Level | `security_sensitivity_level` |
| C / I / A Impact | `compliance.information_systems.security_objective_*` |
| Authorization Boundary | `compliance.information_systems.authorization_boundary` |
| Deployment Model | `compliance.information_systems.deployment_model` |

## 6. 鉤稽演算法

### 6.1 Person → user_id

```python
def match_person(party: ParsedParty, tenant_id: int) -> Optional[int]:
    if not party.email_address:
        return None
    user = users.get_one_by(
        email=party.email_address.lower().strip(),
        tenant_id=tenant_id,  # 同 tenant 才連
        is_active=True,
    )
    return user.id if user else None
```

### 6.2 Organization → org_unit_id

```python
def match_organization(party: ParsedParty, tenant_id: int) -> Optional[int]:
    if not party.name:
        return None
    org = org_units.get_one_by(
        name=party.name.strip(),  # exact, case-insensitive 由 DB 端 collation 處理
        tenant_id=tenant_id,
    )
    return org.id if org else None
```

兩者無 fuzzy / partial 比對；找不到就回 None，store 為純文字 party。

## 7. Out of Scope（v1 不做）

- 左右同步滾動 / 高亮聯動
- Multi-day cross-device 編輯（user 確認不需要）
- 全域「未連結 party 管理」頁
- 新建 user 時 auto-suggest 連結 parties
- 多 user 協作鎖定
- 修訂紀錄獨立表（`metadata_revisions`）
- `system_information_types` 獨立 1:N 表
- `leveraged_authorizations` 獨立表（先用 `system_implementation` 之 `implementation_type` + props）
- `system_implementation_components` 結構化清單表
- ISO 27001 / ISMS adapter（架構預備好，實作未來再做）
- Plugin / dynamic adapter loading
- Config-driven UI section
- Network architecture 圖示自動抽取（圖檔留在原 docx 內，不擷取）

## 8. 廢棄項

從 v1 廢棄：

| 項目 | 廢棄原因 |
|---|---|
| `SspDocxImportDialog.vue` | dialog 模式不再使用 |
| `PreselectSummary.vue` | 整合到新 SectionComponent 內 |
| `StepUpload.vue` / `StepPreview.vue` / `StepResult.vue` | wizard 模式不再使用 |
| ModuleFrame.vue「從 docx 預選」按鈕 | 入口改為合規資源庫管理首頁 |
| `pendingParseUid` / `pendingPredictedControls` 暫存 | state 改放 import page 內 |
| `bindingFrameworkVersionTreeData` 防洗 guard | flow 重新設計後不需要 |
| chain confirm 邏輯 | confirm 直接帶 create payload，BE 一筆 transaction 完成 |

## 9. 風險與對策

| 風險 | 對策 |
|---|---|
| docx PDF 轉檔失敗（複雜版面）| iframe fallback：`onerror` 顯示「PDF 預覽不可用，請檢查右側結構化資料」|
| localStorage 配額用滿 | 一個 parse_uid 一個 key，confirm 後即清；異常 capture 警告 user |
| Adapter 介面設計不夠抽象（未來加 ISO 才發現）| 先寫 CmmcSspAdapter 時刻意挑「最 generic 的 method signature」；ISO 加入時容許小幅 adjust interface |
| Email 鉤稽到別 tenant 的 user | match 強制帶 `tenant_id` filter |
| 既有 v1 用戶的 module_frame 怎辦 | v2 不影響既有資料，只改新建 / 更新流程；既有 module_frame 仍可正常編輯 |

## 10. 上線策略

v2 規劃為 **fresh ship**：
- v1 已上線運作；v2 完成後直接覆蓋舊路徑
- v1 的 dialog 入口在 v2 release 時拿掉
- `parse_jobs` 表 schema 不變，向後相容；既有資料保留
- 新 schema migration（oscal_parties 4 欄）安全（純加欄位）

詳細 deployment 流程見 `release-notes-v2.md`（v2 ship 前產生）。
