# SSP 文件解析器 API 規格書

> **文件版本**：1.0
> **撰寫日期**：2026-04-30
> **文件性質**：SA — API 介面契約
> **依據**：[`raw-requirement/requirement.md`](./raw-requirement/requirement.md) + Phase 1 brainstorming 9 項決策
>
> **狀態**：草稿，等使用者 review 簽字後才會撰寫 SD（design.md）

---

## 1. 模組概述

### 1.1 功能定位

SSP 文件解析器（SSP Docx Parser）讓使用者上傳 Word 格式的 System Security Plan（SSP），系統用**純規則層**抽取每個合規控制項的實作描述（implementation description）與評估目標（assessment objectives），自動填入 `ssp_control_implementation` / `module_frame_control_defaults` 等對應表，省去人工逐條 copy-paste。

### 1.2 v1 範圍

✅ **做：**
- 純規則 + python-docx + regex + fuzzy match 解析 docx
- 抽取控制項實作描述、評估目標（(a)(b)(c) 表格第 2 欄）
- 抽取 SSP 整體 metadata 寫到 `information_systems`
- 三個入口（合規資源庫 Step 2 / Step 3 / 專案規劃端）共用同一套 API
- 三步驟 wizard：上傳 → 預覽編輯（含拖拉指派 + diff preview）→ 結果摘要
- 同步處理（5MB docx 預估 5 秒內）
- 失敗段落拖拉到任意控制項主述或特定 (a)(b)(c) objective
- 一份 docx 涉及單一 baseline / framework

❌ **不做（v2 候選）：**
- LLM fallback（規則打不到時不自動猜，全靠拖拉）
- OSCAL JSON 匯出
- Leveraged External Systems table 結構化抽取
- 系統元件 / 軟體元件清單抽取
- 嵌入圖片抽取
- Rich text 格式保留（與既有 plain Textarea UI 一致）
- docx 範本下載
- 多 baseline 跨 framework 解析
- 中途離開後 resume（中途離開作廢）
- async / cron / worker queue

### 1.3 端點總覽

| # | Method | URL | Route Class | 說明 |
|---|--------|-----|-------------|------|
| 1 | POST | `/api/1.0/ssp-docx-imports/parse` | `SspDocxImportParseRoute` | 上傳 docx + 解析（同步） |
| 2 | GET | `/api/1.0/ssp-docx-import/<parse_uid>` | `SspDocxImportRoute` | 取得 parse job 狀態與預覽結果（Step 1 用） |
| 3 | POST | `/api/1.0/ssp-docx-import/<parse_uid>/confirm` | `SspDocxImportConfirmRoute` | 確認匯入，寫入目標表（Step 2） |
| 4 | DELETE | `/api/1.0/ssp-docx-import/<parse_uid>` | `SspDocxImportRoute` | 放棄此次 parse job（清檔） |

---

## 2. Acceptance Criteria（驗收條件）

依優先順序：

| AC | 描述 | 涵蓋的 brainstorm 決策 |
|----|------|----------------------|
| **AC-1** | 使用者在「專案規劃 / SSP 控制項現況頁」可看到「匯入 docx」按鈕（與既有「匯入 Excel」並排），點擊後開啟 3 步驟 wizard | D-Q1 D2, D-Q2 α |
| **AC-2** | 使用者在「合規資源庫 / 新增 module_frame」第 2 步驟可選擇「從 docx 匯入控制項」啟動解析（Full mode） | D Q1 → A1 |
| **AC-3** | 使用者在「合規資源庫 / 新增 module_frame」第 3 步驟可選擇「從 docx 匯入現況」啟動解析（Statement-only mode） | D Q1 → A2 |
| **AC-4** | 上傳 docx 時必須先選 framework（CMMC L1 / NIST 800-171 / ISO27001），未選不能繼續 | C-Q1 = C2 |
| **AC-5** | 5MB / 273 段 / 25 控制項範例 docx 同步解析時間 < 10 秒 | F → v1A 同步 |
| **AC-6** | 規則層命中（信心 ≥ 0.8）的控制項自動配對到 baseline 對應的 control_id | A → 純規則 |
| **AC-7** | 規則層失敗（信心 < 0.8）的段落出現在「待人工確認」清單，使用者可拖拉到任意控制項的主述或特定 (a)/(b)/(c) objective | G-Q1 β |
| **AC-8** | 同一段落可拖拉到多個控制項（複製到每個） | G-Q2 β |
| **AC-9** | 拖拉到已有 docx 內容的目標時，跳出 conflict modal 讓使用者選 append / replace | G-Q3 β |
| **AC-10** | Re-parse 時 Step 1 預覽顯示「現有值 vs docx 值」，使用者可逐筆選擇 keep_current / use_docx / skip；空欄位預設 use_docx，衝突欄位預設 keep_current | E-Q1 E3, E-Q2 |
| **AC-11** | 評估目標 (a)(b)(c) 每筆獨立顯示一行，獨立選 keep/use_docx/skip | E-Q4 α |
| **AC-12** | Step 1 提供批次操作：「全部使用 docx」/「全部保留現有」/「只匯入空白項」 | E-Q3 |
| **AC-13** | docx 解析至 baseline 內找不到的控制項時，留 implementation_description 為空，UI 提示使用者後續手動補（不阻擋匯入） | C-Q3 α |
| **AC-14** | 使用者選定的 framework 與 docx 內主要控制項 ID 模式不符時（例如選 CMMC L1 但 docx 全是 NIST 800-53 ID），標 FATAL 拒絕匯入 | G-Q4 |
| **AC-15** | docx 內找不到任何 heading / 控制項 ID 時，標 FATAL 拒絕匯入 | G-Q4 |
| **AC-16** | 規則命中率 < 50% 或 baseline 缺漏 > 30% 控制項時，Step 1 頂部顯示 PARTIAL warning banner，使用者可選擇繼續或放棄 | G-Q4 |
| **AC-17** | parse job 創建後 1 小時內若 status 仍非 'completed' 或 'failed'，自動標 'expired'，使用者必須重新上傳 | G-Q5 α + 短 expire |
| **AC-18** | Step 2 確認匯入時，使用者標「不匯入」的段落記在 `parsed_result.skipped_paragraphs`（JSONB 欄位） | G-Q6 β |
| **AC-19** | docx 內所有 rich text 格式（粗體、斜體、底線）抽取時轉成純文字；表格內容用換行分隔 row、`: ` 或 ` \| ` 分隔 cell | I-Q1 α |
| **AC-20** | docx 內嵌入圖片（網路架構圖、資料流）完全忽略不抽取 | I-Q2 α |
| **AC-21** | 評估目標 table 第 2 欄當 `description`，第 1 欄忽略（objective_text 由 baseline 提供） | I-Q3 γ |
| **AC-22** | 入口 A 操作權限：tenant admin OR project manager；入口 B 操作權限：project manager only | F-Q2, F-Q3 α |
| **AC-23** | docx 抽出的 SSP metadata（Introduction tables）寫入 `information_systems` 對應欄位（name, description, security_sensitivity_level, deployment_model, authorization_boundary 等），與專案 `project_information_systems` 關聯 | B → metadata 寫 information_systems |

---

## 3. 名詞定義

| 詞彙 | 定義 |
|------|------|
| **SSP** | System Security Plan，OSCAL 規範的系統安全計畫文件 |
| **module_frame** | 「合規資源庫」table — 租戶層級的合規範本（如：「我們公司的 CMMC L1 SSP 範本」），給專案啟動時 copy 用 |
| **baseline** | 一個合規 framework + level 的控制項清單（如 CMMC L1 = 17 個控制項） |
| **framework** | 合規規範（CMMC / NIST 800-171 / ISO27001） |
| **catalog** | 一個 framework 的控制項目錄（OSCAL 概念） |
| **profile** | 一個 catalog 的 subset，用於某個 baseline（OSCAL 概念） |
| **assessment objective** | 控制項下的 (a)(b)(c) 評估細項 |
| **implementation_description** | 控制項實作現況的描述文字 |
| **Full mode** | docx 解析模式：抽出控制項清單 + 現況描述（用於合規資源庫 Step 2） |
| **Statement-only mode** | docx 解析模式：只抽出現況描述（用於合規資源庫 Step 3 + 專案規劃端） |
| **parse job** | 一次完整的 parse 任務（從上傳到匯入完成），對應 `ssp_docx_parse_jobs` 表的一 row |

---

## 4. API 端點規格

### 4.1 POST `/api/1.0/ssp-docx-imports/parse` — 上傳 docx + 解析（同步）

- **Route Class**：`SspDocxImportParseRoute`
- **認證**：JWT Required
- **權限**：依 `source_type` 分支驗證
  - `source_type = 'module_frame'` → tenant admin OR manager
  - `source_type = 'project_ssp'` → project manager
- **Request**：`multipart/form-data`

| 欄位 | 型別 | 必填 | 說明 |
|------|------|------|------|
| `file` | File | Y | docx 檔（≤ 10MB，副檔名必須 `.docx`） |
| `framework` | string | Y | `cmmc-l1` \| `cmmc-l2` \| `nist-800-171` \| `iso27001`（C-Q1 = C2 手動選） |
| `source_type` | string | Y | `module_frame` \| `project_ssp` |
| `source_uid` | string | Y | 對應的 `module_frames.uid` 或 SSP 的 uid（取決於 source_type） |
| `mode` | string | Y | `full`（限 source_type=module_frame）\| `statement_only` |

- **Response (200)**（成功，parse job 創建並同步解析完成）：

```json
{
  "status": true,
  "data": {
    "parse_uid": "uuid-string",
    "status": "awaiting_review",
    "summary": {
      "total_paragraphs": 273,
      "matched_controls_count": 22,
      "unmatched_paragraphs_count": 8,
      "missing_baseline_controls_count": 3,
      "framework_detected_id_pattern": "AC.L1-b.1.x",
      "warning_level": null
    }
  }
}
```

- **Response (200)**（PARTIAL warning，仍可繼續但需確認）：

```json
{
  "status": true,
  "data": {
    "parse_uid": "uuid-string",
    "status": "awaiting_review",
    "summary": {
      "total_paragraphs": 273,
      "matched_controls_count": 8,
      "unmatched_paragraphs_count": 50,
      "missing_baseline_controls_count": 12,
      "warning_level": "partial",
      "warnings": [
        "規則命中率 < 50% — 建議檢查 framework 選擇是否正確",
        "Baseline 中 12/17 控制項在 docx 找不到對應描述"
      ]
    }
  }
}
```

- **Response (422)**（FATAL 失敗）：

```json
{
  "status": false,
  "msg": "上傳的 docx 不包含任何可辨識的控制項 ID（GRC_422001）",
  "data": {
    "parse_uid": null,
    "fatal_reason": "no_control_id_found"
  }
}
```

- **Error codes**：
  - `GRC_400005` 上傳檔案不是 docx 格式
  - `GRC_400006` 上傳檔案超過大小限制（10MB）
  - `GRC_400007` framework 必填
  - `GRC_400008` source_type 必填且必須為 `module_frame` / `project_ssp`
  - `GRC_400009` mode 為 `full` 時 source_type 必須為 `module_frame`
  - `GRC_403002` 無權限操作此 source_uid
  - `GRC_404021` source_uid 對應的資源不存在
  - `GRC_422001` docx 內無法辨識任何控制項 ID（FATAL）
  - `GRC_422002` docx framework 偵測與使用者選擇不符（FATAL）
  - `GRC_422003` docx 結構損壞 / 解析失敗

### 4.2 GET `/api/1.0/ssp-docx-import/<parse_uid>` — 取得 parse 結果（Step 1 用）

- **Route Class**：`SspDocxImportRoute`
- **認證**：JWT Required
- **權限**：建立此 parse job 的使用者本人 OR 同 source_uid 的有權使用者
- **Path Parameters**：
  - `parse_uid` — `ssp_docx_parse_jobs.uid`

- **Response (200)**（status=`awaiting_review`，給 Step 1 預覽 UI 用）：

```json
{
  "status": true,
  "data": {
    "parse_uid": "uuid",
    "status": "awaiting_review",
    "source_type": "project_ssp",
    "source_uid": "ssp-uid",
    "framework": "cmmc-l1",
    "mode": "statement_only",
    "summary": {
      "total_paragraphs": 273,
      "matched_controls_count": 22,
      "unmatched_paragraphs_count": 8,
      "missing_baseline_controls_count": 3,
      "warning_level": null
    },
    "matched_controls": [
      {
        "control_id": "AC.L1-b.1.i",
        "control_name": "Authorized Access Control",
        "score": 0.95,
        "score_reason": "H3 標題完整 ID 命中",
        "current_implementation_description": "我們透過 AD 控制使用者群組...",
        "parsed_implementation_description": "我們透過 AD + Okta 控制使用者群組，並...",
        "default_action": "use_docx",
        "objectives": [
          {
            "objective_key": "a",
            "objective_text": "authorized users are identified",
            "current_description": "已透過 SOP-001 識別",
            "parsed_description": "已透過 SOP-001 識別並由 IT 部審核",
            "default_action": "use_docx"
          }
        ]
      }
    ],
    "unmatched_paragraphs": [
      {
        "paragraph_idx": 142,
        "text": "我們的安全事件處理由 SOC team 24x7 監控...",
        "context": {
          "preceding_h1": "Control Implementation",
          "preceding_h2": null,
          "preceding_h3": null
        },
        "rule_score": 0.45,
        "rule_guess_control_id": "IR.L2-...",
        "rule_guess_confidence": 0.45
      }
    ],
    "missing_baseline_controls": [
      {
        "control_id": "SC.L1-b.1.x",
        "control_name": "Boundary Protection",
        "reason": "baseline 中存在但 docx 沒提到"
      }
    ],
    "predicted_module_frame_controls": [
      // 僅在 mode=full 時提供 — D-Q5 β 預選清單，給 Step 2 勾選 UI
      { "control_id": "AC.L1-b.1.i", "preselected": true },
      { "control_id": "AC.L1-b.1.ii", "preselected": true }
    ],
    "created_at": "2026-04-30 10:00:00",
    "expires_at": "2026-04-30 11:00:00"
  }
}
```

- **Response (200)**（status=`completed`，已匯入完成的 job 查詢）：

```json
{
  "status": true,
  "data": {
    "parse_uid": "uuid",
    "status": "completed",
    "import_summary": { "created": 22, "updated": 5, "skipped": 3 },
    "completed_at": "2026-04-30 10:05:00"
  }
}
```

- **Response (200)**（status=`failed` / `expired`）：

```json
{
  "status": true,
  "data": {
    "parse_uid": "uuid",
    "status": "failed",
    "error_code": "GRC_422001",
    "error_message": "docx 內無法辨識任何控制項 ID"
  }
}
```

- **Error codes**：
  - `GRC_404022` parse_uid 不存在
  - `GRC_403003` 無權限存取此 parse job

### 4.3 POST `/api/1.0/ssp-docx-import/<parse_uid>/confirm` — 確認匯入（Step 2）

- **Route Class**：`SspDocxImportConfirmRoute`
- **認證**：JWT Required
- **權限**：建立此 parse job 的使用者本人；且 status 必須為 `awaiting_review`
- **Path Parameters**：`parse_uid`

- **Request Body**：

```json
{
  "decisions": [
    {
      "control_id": "AC.L1-b.1.i",
      "action": "use_docx | keep_current | skip",
      "objectives": [
        { "objective_key": "a", "action": "use_docx" },
        { "objective_key": "b", "action": "keep_current" }
      ]
    }
  ],
  "manual_assignments": [
    {
      "paragraph_idx": 142,
      "targets": [
        { "control_id": "IR.L2-x", "level": "implementation", "merge_action": "append" },
        { "control_id": "AC.L1-b.1.i", "level": "objective", "objective_key": "c", "merge_action": "replace" }
      ]
    }
  ],
  "skipped_paragraph_idxs": [85, 142, 200],
  "predicted_controls_user_selection": [
    "AC.L1-b.1.i", "AC.L1-b.1.ii", "AC.L1-b.1.iv"
    // 僅在 mode=full 時必填 — 使用者在 Step 1 微調過的最終控制項勾選清單
  ]
}
```

| 欄位 | 必填 | 說明 |
|------|------|------|
| `decisions[]` | Y | 每個 matched_control 一筆，action 決定 keep/use_docx/skip |
| `decisions[].objectives[]` | N | objective 層級的決策，未列出的 objective 沿用 control 層的 default_action |
| `manual_assignments[]` | N | 使用者拖拉指派的段落（unmatched_paragraphs） |
| `manual_assignments[].targets[]` | Y（若有 manual_assignment） | 段落可拖拉到多個 target（G-Q2 β） |
| `manual_assignments[].targets[].merge_action` | Y | `append` \| `replace`（G-Q3 β） |
| `skipped_paragraph_idxs[]` | N | 使用者明確選「不匯入」的段落 idx，記錄供審計（AC-18 / G-Q6 β） |
| `predicted_controls_user_selection[]` | Y（mode=full）/ N（其他） | 合規資源庫 Step 2 使用者最終勾選的控制項清單 |

- **Response (200)**：

```json
{
  "status": true,
  "data": {
    "parse_uid": "uuid",
    "status": "completed",
    "import_summary": {
      "created": 22,
      "updated": 5,
      "skipped": 3,
      "manual_assigned": 8,
      "manual_skipped": 3,
      "missing_left_blank": 3
    },
    "redirect_url": "/projects/<uid>/ssp/control-implementations"
  }
}
```

- **Error codes**：
  - `GRC_404022` parse_uid 不存在
  - `GRC_412009` parse job 狀態不是 awaiting_review，無法確認匯入
  - `GRC_412010` parse job 已 expired
  - `GRC_400010` decisions 缺漏（matched_controls 中有未決策的 control_id）
  - `GRC_400011` predicted_controls_user_selection 缺漏（mode=full 時必填）

### 4.4 DELETE `/api/1.0/ssp-docx-import/<parse_uid>` — 放棄此次 parse job

- **Route Class**：`SspDocxImportRoute`
- **認證**：JWT Required
- **權限**：建立此 parse job 的使用者本人

- **Response (200)**：

```json
{
  "status": true,
  "data": { "parse_uid": "uuid", "status": "discarded" }
}
```

- 副作用：MinIO 上的 file 同步刪除；DB row 軟刪除（is_active=False）。

- **Error codes**：
  - `GRC_404022` parse_uid 不存在
  - `GRC_412011` 已完成的 job 無法再放棄（status=completed）

---

## 5. Error Codes（新增）

依 CLAUDE.md 命名規則 `GRC_<HTTP狀態碼><3位序號>`，序號接續既有 `common/code/grc_error_code.py`。

| Constant Name | 訊息 | Code |
|---------------|------|------|
| `GRC_DOCX_INVALID_FILE` | 上傳檔案不是 docx 格式 | GRC_400005 |
| `GRC_DOCX_FILE_TOO_LARGE` | 上傳 docx 超過大小限制（10MB） | GRC_400006 |
| `GRC_DOCX_FRAMEWORK_REQUIRED` | 必須選擇合規 framework | GRC_400007 |
| `GRC_DOCX_SOURCE_TYPE_INVALID` | source_type 必須為 module_frame 或 project_ssp | GRC_400008 |
| `GRC_DOCX_MODE_FULL_REQUIRES_MODULE_FRAME` | full mode 僅限 source_type=module_frame | GRC_400009 |
| `GRC_DOCX_DECISIONS_INCOMPLETE` | decisions 缺漏，請對所有匹配控制項做選擇 | GRC_400010 |
| `GRC_DOCX_PREDICTED_CONTROLS_REQUIRED` | full mode 必須提供 predicted_controls_user_selection | GRC_400011 |
| `GRC_DOCX_NO_PERMISSION_FOR_SOURCE` | 無權限操作此 source_uid | GRC_403002 |
| `GRC_DOCX_PARSE_JOB_NO_PERMISSION` | 無權限存取此 parse job | GRC_403003 |
| `GRC_DOCX_SOURCE_NOT_FOUND` | source_uid 對應的資源不存在 | GRC_404021 |
| `GRC_DOCX_PARSE_JOB_NOT_FOUND` | parse job 不存在 | GRC_404022 |
| `GRC_DOCX_PARSE_JOB_NOT_AWAITING` | parse job 狀態不是 awaiting_review，無法確認匯入 | GRC_412009 |
| `GRC_DOCX_PARSE_JOB_EXPIRED` | parse job 已過期，請重新上傳 | GRC_412010 |
| `GRC_DOCX_PARSE_JOB_ALREADY_COMPLETED` | 已完成的 job 無法放棄 | GRC_412011 |
| `GRC_DOCX_NO_CONTROL_ID_FOUND` | docx 內無法辨識任何控制項 ID | GRC_422001 |
| `GRC_DOCX_FRAMEWORK_MISMATCH` | docx 主要控制項 ID 模式與選擇的 framework 不符 | GRC_422002 |
| `GRC_DOCX_PARSE_FAILED` | docx 結構損壞，解析失敗 | GRC_422003 |

---

## 6. Permission Matrix

| 操作 | 入口 | 角色 |
|------|------|------|
| Upload + Parse（POST /parse） | A 合規資源庫 | tenant admin OR manager |
| Upload + Parse（POST /parse） | B 專案規劃 | project manager |
| GET preview（GET /\<parse_uid\>） | 任一 | parse job 建立者 |
| Confirm import（POST /confirm） | A 合規資源庫 | tenant admin OR manager（建立者本人） |
| Confirm import（POST /confirm） | B 專案規劃 | project manager（建立者本人） |
| Discard（DELETE） | 任一 | parse job 建立者 |

備註：
- 角色驗證在 app service 層透過 domain service（依 CLAUDE.md DDD 規範）
- `project manager` = `project_participants.role == 'manager'`
- `tenant admin` = `users.is_admin == True`
- `auditor` 角色**不可**進行任何 docx import 操作（F-Q3 α 嚴格）

---

## 7. UI 整合點（high-level，細節留 frontend-spec.md）

### 7.1 入口 A1 — 合規資源庫新增 Step 2

- 位置：`ModuleFrame.vue` 編輯/新增 wizard 第 2 步（選控制項）
- 新增按鈕：「從 docx 匯入控制項」（與既有手動勾選並排）
- 點擊後開啟 `SspDocxImportDialog` 子 wizard
- mode = `full`，source_type = `module_frame`
- 子 wizard 完成後：使用者選定的控制項 ID 清單回傳父 wizard，父 wizard 跳轉至 Step 3

### 7.2 入口 A2 — 合規資源庫新增 Step 3

- 位置：`ModuleFrame.vue` 編輯/新增 wizard 第 3 步（填現況）
- 新增按鈕：「從 docx 匯入現況」（與既有手動填寫並排）
- mode = `statement_only`，source_type = `module_frame`
- 子 wizard 完成後：使用者選定的現況資料回傳父 wizard，父 wizard 顯示已填內容

### 7.3 入口 B — 專案規劃 SSP 控制項現況頁

- 位置：SSP 控制項現況頁，與既有「匯入 Excel」按鈕並排
- 新增按鈕：「匯入 docx」
- mode = `statement_only`，source_type = `project_ssp`
- 子 wizard 完成後：頁面 reload 顯示更新後的現況

### 7.4 共用：SspDocxImportDialog 三步驟

- **Step 0 — 上傳**：framework dropdown + FileUpload 元件 → 點「下一步」呼叫 `POST /parse`
- **Step 1 — 預覽編輯**（最複雜的 UI）：
  - 自動匹配清單（matched_controls）— diff preview + 每筆 keep/use_docx/skip
  - 待人工確認清單（unmatched_paragraphs）— 拖拉到任一控制項主述或 objective
  - Baseline 缺漏清單（missing_baseline_controls）— 純 warning 訊息
  - 批次操作 toolbar：全部使用 docx / 全部保留現有 / 只匯入空白項
  - PARTIAL warning banner（若有）
  - 拖拉到已有內容時跳 Conflict modal（append / replace 二選一）
- **Step 2 — 結果**：import_summary + 「完成」按鈕

---

## 8. 待解決 / 後續討論

| 編號 | 問題 | 可能影響 |
|------|------|---------|
| **OPEN-1** | `information_systems` 寫入時：使用者已手動建了該專案的 information_system 怎麼辦？docx 匯入要 merge / 新建 / 報錯讓使用者選？brainstorm 沒展開細節 | AC-23 細節 |
| **OPEN-2** | 多個使用者同時操作同一個 source_uid（如多人匯入同一份 SSP）時的 race condition：兩個 parse_uid 同時 confirm，後者覆蓋前者？要不要加 lock | F → 多人協作 v1 不展開但需有對策 |
| **OPEN-3** | docx 解析的 framework 偵測邏輯：用什麼比例 / 邏輯判定「framework mismatch」？例如 80% H3 ID 不符選擇的 framework 才報錯？ | AC-14 / GRC_422002 細節 |
| **OPEN-4** | `predicted_controls_user_selection` 的 default 行為：若使用者完全沒勾選任何（清空），是不是該擋下？或當作「全不選」 | mode=full 邊界條件 |
| **OPEN-5** | 拖拉指派到 objective 層時，objective_key 是否驗證屬於該 control？(e.g., 防止拖到不存在的 objective) | AC-7 / AC-8 細節 |
| **OPEN-6** | 1 小時 expire 是否合適？對顧問處理 100+ 控制項可能不夠 | AC-17 細節 |

這些 OPEN 不影響 SA 主結構，會在 SD（design.md）階段細化解決方案。

---

## 9. 風險與假設

### 9.1 假設

- 既有 jedi-oscal 套件的 `ssp_control_implementation` / `ssp_control_implementation_objective` 現況可直接寫入（API 已存在）
- 既有 `module_frame_control_defaults` / `module_frame_control_objective_defaults` 表結構穩定
- `information_systems` + `project_information_systems` 的 CRUD service 已可呼叫
- python-docx 套件已存在（既有 docs/scripts 已使用）

### 9.2 風險

- **R1**：客戶提供的 docx 結構過於不規則 → 規則命中率 < 50% → 大量人工拖拉，使用者體驗差 → 觸發 PARTIAL warning，但 v1 沒有 LLM 救援，使用者可能放棄
- **R2**：`information_systems` 寫入 / merge 邏輯（OPEN-1）若沒 set 好，可能造成多 row 髒資料
- **R3**：5MB docx 同步解析超時：若實測超過 5 秒，前端 UX 受影響 → 需要在 SD 階段加 timeout 監控

---

## 10. 參考資料

- 原始白話需求：[`raw-requirement/requirement.md`](./raw-requirement/requirement.md)
- 客戶範例 docx：[`raw-requirement/reference/ASIA-CMMC-SSP-DRAFT-202604.docx`](./raw-requirement/reference/ASIA-CMMC-SSP-DRAFT-202604.docx)
- 既有合規資源庫文件：`docs/api/module-frame/`
- 既有 SSP 控制項現況設計：`api/oscal/serializers/ssp/ssp_control_implementation.py`
- jedi-oscal 套件 SSP models：`~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/model/ssp/`
- jedi_information_system 套件：`jedi_information_system/`（主專案內）
- CLAUDE.md（DDD / 權限 / error code 規範）

---

**Sign-off 區（Phase 2.1 結束時填寫）**

| 角色 | 名字 | 日期 | 備註 |
|------|------|------|------|
| Product / 需求 | | | |
| Tech Lead | | | |
| FE Lead | | | |

簽完後進 Phase 2.2（design.md / SD 撰寫）。

---

## 11. Post-launch 新增 endpoints / 欄位 (2026-05-03)

跟 docx 匯入相關的編輯介面（編輯頁的「參與單位 / 人員」對話框）需要 BE 提供 CRUD endpoints 直接管理某個 module_frame 的 oscal_parties，不再透過 docx import 流程。

### 11.1 `GET /module-frame/<uid>/parties`

列出該 module_frame 連結的所有 parties。

**Request**：無 body

**Response 200**：
```json
{
  "status": true,
  "data": [
    {
      "uid": "uuid",
      "party_type": "person | organization",
      "name": "Pan Pan",
      "role": "system-security-officer",
      "title": "Security Manager",
      "email_address": "bls.tech.pan@gmail.com",
      "telephone_number": null,
      "address": null,
      "matched_user_id": 23,
      "matched_org_unit_id": null,
      "matched_user_name": "Pan Pan",
      "matched_org_unit_name": null
    }
  ]
}
```

### 11.2 `POST /module-frame/<uid>/parties`

新增 party + 連結到該 module_frame。

**Request body**：
```json
{
  "party_type": "person | organization",
  "name": "string",
  "role": "system-owner | ...",
  "title": "string?",
  "email_address": "string?",
  "telephone_number": "string?",
  "address": "string?",
  "matched_user_id": 23,
  "matched_org_unit_id": null
}
```

`role` 為 5 個固定 role 之一（responsible-organization / information-provider / information-receiver / system-owner / system-security-officer）。

### 11.3 `PUT /module-frame/<uid>/parties/<party_uid>`

編輯 party 內容 / 重新連結。`party_type` 與 `name` **不可改**（保 dedupe key 穩定，re-import 時才能找到同一筆）。`role` 改變時自動 re-point responsible_party 連結。

### 11.4 `DELETE /module-frame/<uid>/parties/<party_uid>`

只移除「該 module_frame 跟此 party 的連結」（responsible_party row），party row 本身保留供其他 module_frame 重用。

### 11.5 Error codes

| Code | HTTP | 說明 |
|------|------|------|
| `GRC_INVALID_PARTY_TYPE` | 400 | party_type 不是 person / organization |
| `GRC_PARTY_NAME_REQUIRED` | 400 | name 缺漏 |
| `GRC_PARTY_ROLE_REQUIRED` | 400 | role 缺漏 |
| `GRC_PARTY_NOT_FOUND` | 404 | party_uid 不存在 |
| `GRC_MODULE_FRAME_NOT_FOUND` | 404 | module_frame uid 不存在 |

### 11.6 Confirm (`/ssp-docx-import/<uid>/confirm`) 新欄位

| 欄位 | 型別 | 用途 |
|------|------|------|
| `content_overrides` | dict | FE inline 編輯 overlay：`{"<control_id>": {"implementation": "...", "objectives": {"(a)": "..."}}}`，BE 在 strategy.write 前 mutate `parsed.matched_controls` 對應欄位 |

### 11.7 Parse 回傳新增欄位

`GET /ssp-docx-import/<uid>` 回傳的 `parsed_result` 多了：

| 欄位 | 用途 |
|------|------|
| `baseline_controls[].catalog_control_uid` | catalog_control UUID — FE pre-create module_frame 必須送這個給 `add_profile`（不是 control_id 字串）|
| `matched_controls[].catalog_objective_descriptions` | catalog 來的 AO 題目（dict: AO key → 題目文字），跟 `current_objectives_descriptions`（user 答案）刻意分開 |
