# Phase A5 Design — 預覽 UI + Confirm 寫入（含 inline 新建）

> **Phase**：A5（Track A：Excel 匯入第五 / 收口階段）
> **級別**：中型（design + plan，不獨立 brainstorm）
> **狀態**：design draft（2026-05-20，A4 ship 後接續開工）
> **前置**：A1（樣板下載）+ A2（Parser + 解析 API）+ A3（共用 matcher）+ A4（5 reconciler + 5 WriteStrategy + 8-step pipeline）皆 BE shipped
> **本 phase 解鎖**：Track A Excel 匯入 user-facing 端到端完整可用

---

## 1. 為什麼要做 A5

### 1.1 既有現況（A4 ship 後）

A4 phase 完成 5 個新 reconciler（device / info_system / leveraged / catalog_control / AO）+ 5 個 WriteStrategy + SSP shell + 8-step pipeline + DI 完整化。Excel 匯入 confirm flow 從 A2 skeleton（只寫 parties）升級到完整寫入（parties + SSP shell + control_implementations + control_impl_objectives + 3 種 ssp_system_implementation_items）。

A4 Step 7（`_update_parsed_result_with_reconcile`）已把 reconcile 結果（`matched_*_id` / `match_method` / `match_confidence`）by-key 對齊回填到 parsed_result JSONB。A5 預覽 UI 只要讀這份 JSONB 即可呈現完整鉤稽結果。

**A4 留下的 5 個 stub / 缺口給 A5 補**：

| 區塊 | A4 ship 後 | A5 待補 |
|------|----------|---------|
| Confirm payload schema | `SspExcelImportConfirmRequestSchema.decisions` 是 `List[Dict]`、`overrides` 是 `Dict`（彈性，A5 定稿）| 結構化 schema：`decisions[]` per entity + `inline_creates[]` per entity + `content_overrides{}` |
| Unmatched 處理路徑 | BE 寫純文字 (FK=null) 一條 path | 三種路徑：(a) 選現有 entity / (b) inline 新建 tenant 資料 / (c) 純文字保留 |
| Inline 新建 | 不存在 | confirm `@transaction` 內 BE 先寫 `public.devices` / `compliance.information_systems` / `org_units` / `users` → 拿 id → 寫 SSP item with FK |
| DELETE endpoint | 不存在 | `DELETE /ssp-excel-import/<parse_uid>` 對齊 docx `discard_parse` pattern |
| FE 預覽頁 | 不存在 | `/module-frame/import-excel` 路由 + Vue 元件 + Pinia store + localStorage 暫存 |

### 1.2 A5 三個目標

1. **BE confirm schema 定稿** — 處理 unmatched 三種路徑 + content_overrides + decisions per entity
2. **BE inline 新建 handling** — confirm `@transaction` 內 atomically 寫 tenant 表 → 拿 id → 寫 SSP item with FK
3. **FE 預覽頁 + 編輯介面** — 9 sheet tab 展示 + unmatched 三種 UI 路徑 + fuzzy badge + localStorage 暫存 + i18n

### 1.3 與其他 phase 關係

- **依賴 A4**：parsed_result JSONB 已含 `matched_*_id` / `match_method` / `match_confidence`；A5 FE 直接讀
- **依賴 A2**：`GET /ssp-excel-import/<parse_uid>` endpoint + `parsed_result` 結構；A5 不改 endpoint shape，只 polish response
- **依賴 jedi-oscal**：不動（A4 ship 後 path-dep 不再變動）
- **解鎖 Track A 完整**：A5 ship 後 Excel 匯入端到端可用，user 從上傳 → 預覽 → 拍板 → confirm → SSP 寫入完整

---

## 2. 範圍 / 不在範圍

### 2.1 In Scope（A5 task arc 內）

**BE**：
- `SspExcelImportConfirmRequestSchema` 完整 schema 定稿（decisions / inline_creates / content_overrides）
- `SspExcelImportAppService.confirm_import` 增加 inline_creates handling（在 `_write_all_data` 之前先寫 tenant 表 + 回填 matched_*_id）
- `DELETE /ssp-excel-import/<parse_uid>` endpoint（soft delete parse job）
- `_update_parsed_result_with_overrides` helper — confirm 時把 user 編輯後的 content_overrides 蓋掉 parsed_result 對應欄位
- Inline 新建可寫的 4 種 tenant 表：`public.devices` / `compliance.information_systems` / `org_units` / `users`（最後 2 個透過 jedi-auth domain service）

**FE**：
- `/module-frame/import-excel` 路由 + Vue 元件骨架
- 預覽元件：9 sheet tab 展示（00_說明 + 01-08 資料 sheet）
- Unmatched UI 三路徑：
  - (a) 從下拉選現有 entity（候選來自 GET tenant 表）
  - (b) Inline 新建 dialog（fill in tenant 表必填欄位）
  - (c) 保留純文字（不配對，FK=null）
- Fuzzy badge UI：confidence < 1.0 顯示「模糊匹配」+ confidence 數值 + override 按鈕
- localStorage dirty state：user 編輯後 confirm 前不丟失
- Confirm flow + discard flow + i18n（zh-tw / en）

**Test**：
- BE pytest：confirm schema validation / inline_creates path / DELETE endpoint / content_overrides 套用
- 不寫 cucumber（沿 F-A4-cucumber pattern；env 配齊那天一次補）

### 2.2 Out of Scope（列 follow-up）

| # | 項目 | 為什麼不做 / 何時做 |
|---|------|----------------|
| F-A5-cucumber | E2E cucumber regression | env 配齊時一次性補 A1-A5 共 5 phase |
| F-A5-preview-export | 預覽頁直接匯出 partial Excel | 跟 Track B SSP 匯出有重疊，留 B6 統一處理 |
| F-A5-batch-import | 批次上傳多個 Excel | Track A 目前單檔模式，批次另起 phase 評估 |
| F-A5-template-version-warn | 上傳舊版 template 警告 | 既有 `template_version` 已存在 parsed_result，FE 可後 polish |

### 2.3 不在 scope 的事（明確排除）

- ❌ 不改 A4 ship 的 reconciler / WriteStrategy / pipeline 行為
- ❌ 不擴 A4 5 entity 以外的 inline 新建（如 catalog_control / assessment_objective — 這些不存 tenant 表，UNMATCHED 直接 skip 是 A4 拍板）
- ❌ 不改 jedi-oscal 套件
- ❌ 不在 BE 主專案內加 e2e（一律走 compliance-manager-test repo）

---

## 3. Architecture

### 3.1 BE 改動 file 清單

```
api/oscal/routes/ssp/
└── ssp_excel_import_route.py            # M：加 DELETE method 到 SspExcelImportRoute

api/oscal/serializers/ssp/
└── ssp_excel_import.py                  # M：擴 ConfirmRequestSchema（decisions / inline_creates / content_overrides）

app/oscal/service/
└── ssp_excel_import_app_service.py      # M：加 confirm_import payload 處理 + _apply_inline_creates + _apply_content_overrides + discard_parse method

common/code/
└── grc_error_code.py                    # M：加 inline 新建相關 error code（如 GRC_400xxx 必填缺漏 / GRC_409xxx 重複 name）
```

**不新建 file**（中型 phase，全 in-place 擴）。

### 3.2 FE 改動 file 清單

```
src/views/module-frame/
├── ImportExcel.vue                       # 新：路由 entrypoint
└── components/import-excel/              # 新：本 phase 元件子目錄
    ├── ImportExcelPreview.vue            # 9 sheet tab shell
    ├── SheetPreviewBasic.vue             # 01_基本資料
    ├── SheetPreviewParties.vue           # 02_單位 + 03_參與人員（共用）
    ├── SheetPreviewDevices.vue           # 04_設備
    ├── SheetPreviewInfoSystems.vue       # 05_資訊系統
    ├── SheetPreviewLeveraged.vue         # 06_外部利用服務
    ├── SheetPreviewControls.vue          # 07_控制項與 AO
    ├── SheetPreviewRefDocs.vue           # 08_程序書
    ├── UnmatchedRow.vue                  # 共用：unmatched 三路徑 UI
    ├── InlineCreateDialog.vue            # 共用：新建 dialog（4 種 entity）
    └── FuzzyBadge.vue                    # 共用：fuzzy 標示 + override 按鈕

src/store/
└── modules/sspExcelImport.js             # 新：Pinia store（parsed_result / localStorage / inline_creates 暫存）

src/service/oscal/
└── SspExcelImportService.js              # 新：BaseService extension（parse / get / update / confirm / discard）

src/router/
└── routes.js                             # M：加 /module-frame/import-excel 路由

src/locales/
├── zh-tw/module-frame.js                 # M：i18n
└── en/module-frame.js                    # M：i18n
```

### 3.3 BE App service 內部結構（confirm_import 新流程）

A5 後 `SspExcelImportAppService.confirm_import` 流程：

```
confirm_import(parse_uid, payload, user_context)
│
├── 1. Validate payload (decisions / inline_creates / content_overrides schema)
├── 2. Load parse job + parsed_result
├── 3. _apply_content_overrides(parsed_result, content_overrides)
│       └── 蓋掉 parsed_result 對應欄位（基本資料 / parties / controls 內容）
├── 4. _apply_inline_creates(inline_creates, user_context, tenant_id)   ← A5 新增
│       ├── For each inline_create.entity_type:
│       │   ├── device      → device_domain_service.add(...)
│       │   ├── info_system → information_system_domain_service.add(...)
│       │   ├── org_unit    → jedi-auth org_unit_service.add(...)
│       │   └── user        → jedi-auth user_service.add(...) + send_invite
│       └── 回 dict {row_idx: new_entity_id}
├── 5. _apply_decisions(parsed_result, decisions)                       ← A5 新增
│       └── 把 decisions[].matched_*_id 蓋掉 parsed_result（user 預覽頁手動改的）
│       └── 把 decisions[].skip=True 標 unmatched（純文字保留）
├── 6. _write_all_data(...)   ← A4 ship，A5 不改
│       └── 8-step pipeline 內部走 A4 reconciler + WriteStrategy
└── 7. Return import_summary (含 inline_created_counts / decision_applied_count)
```

整段在 `@transaction` 內 — inline 新建 + SSP 寫入要 atomic（user 取消預覽不留髒資料）。

### 3.4 DDD 規範

- Route 層只解 payload + dispatch app service（不查 DB）
- App service `confirm_import` 用 `@transaction`
- Inline 新建走 `domain service.add()`，不直接 import ORM model
- 權限檢查：寫入 confirm endpoint 必有 manager / auditor 角色檢查（沿用既有 ssp_excel_import 入口的權限規則 — A2 已實作 + 沿用）

---

## 4. Data Structures

### 4.1 Confirm Request Schema（A5 定稿）

```python
class InlineCreateSchema(Schema):
    """user 在預覽頁點 '新建' 後 BE 要寫的 tenant entity"""
    row_idx = fields.Int(required=True)           # parsed_result 內第幾筆（給回填用）
    entity_type = fields.Str(required=True, validate=validate.OneOf([
        "device", "information_system", "org_unit", "user"
    ]))
    payload = fields.Dict(required=True)          # tenant 表必填欄位（依 entity_type 不同）

class DecisionSchema(Schema):
    """user 對單筆 parsed entity 的最終決定"""
    sheet = fields.Str(required=True)             # devices / info_systems / leveraged / parties / controls
    row_idx = fields.Int(required=True)
    action = fields.Str(required=True, validate=validate.OneOf([
        "use_matched",      # 用 reconciler 配到的 matched_*_id（或 user 改過的）
        "use_existing",     # user 從下拉選了另一個 existing entity
        "use_inline_new",   # user 新建（透過 inline_creates 流程）
        "skip_as_text"      # 純文字保留（FK=null）
    ]))
    matched_id_override = fields.Int(load_default=None)   # use_existing 時帶
    inline_create_row_idx = fields.Int(load_default=None) # use_inline_new 時對應 inline_creates 內第幾筆

class SspExcelImportConfirmRequestSchema(Schema):
    decisions = fields.List(fields.Nested(DecisionSchema), load_default=list)
    inline_creates = fields.List(fields.Nested(InlineCreateSchema), load_default=list)
    content_overrides = fields.Dict(load_default=dict)
```

**`content_overrides` shape**（沿 docx pattern）：

```json
{
  "metadata": {"name": "<new>", "abbreviation": "<new>", ...},
  "controls": {
    "AC-1": {
      "implementation_description": "<new>",
      "objectives": {"(a)": "<new>", "(b)": "<new>"}
    }
  }
}
```

### 4.2 Confirm Response Schema 擴張

```python
class SspExcelImportConfirmResponseSchema(Schema):
    parse_uid = fields.Str()
    status = fields.Str()                                 # "success" / "partial" / "failed"
    module_frame_uid = fields.Str(allow_none=True)
    profile_uid = fields.Str(allow_none=True)
    import_summary = fields.Dict()                        # A4 ship 既有 + 加 inline_created_* 計數
```

`import_summary` 新增欄位（A4 ship 既有計數之外）：

```json
{
  "parties_written": 5,
  "controls_written": 32,
  ...
  "inline_created_devices": 2,
  "inline_created_info_systems": 1,
  "inline_created_org_units": 0,
  "inline_created_users": 0,
  "decision_applied_count": 3,
  "skipped_as_text_count": 1
}
```

### 4.3 FE parsed_result 編輯 state（localStorage）

```js
// key: `ssp-excel-import:${parse_uid}`
{
  "version": 1,
  "saved_at": "2026-05-20T10:00:00Z",
  "parsed_result_overlay": {
    "metadata": { ... },
    "controls": { ... }
  },
  "decisions": [
    { "sheet": "devices", "row_idx": 0, "action": "use_existing", "matched_id_override": 42 }
  ],
  "inline_creates": [
    { "row_idx": 1, "entity_type": "device", "payload": { "name": "Web-02", "ip": "10.0.0.1" } }
  ]
}
```

TTL：跟 BE parse job TTL 對齊（7 天）。discard parse 時清掉。

---

## 5. User Mental Walkthrough（強制段，per SOP）

### 5.1 主流程（happy path）

```
1. user 進入 /module-frame/import-excel 頁面
   → 看到 "上傳 Excel" 按鈕 + 既有 framework + version 兩層 picker
2. user 選 framework + version → 上傳 .xlsx
   → POST /ssp-excel-imports/parse
   → BE 回 parse_uid + status='success' (or 'failed' + error_code)
   → 自動 redirect /module-frame/import-excel/<parse_uid>
3. user 看到 9 個 sheet tab（00_說明 + 01-08）
   → tab badge 顯示 unmatched 數量（如 04_設備 [3 unmatched]）
   → 預設停在 01_基本資料
4. user 編輯欄位（基本資料 / 程序書 / 控制項 impl 文字）
   → 即時存 localStorage（無 BE 呼叫，避免 race）
   → 重新整理頁面 → state 恢復
5. user 進 04_設備 tab
   → 看到 5 列：3 列已配對（綠勾 + matched device 名稱）/ 2 列 unmatched（紅 badge）
   → 2 列 fuzzy（confidence 0.7 黃 badge "模糊匹配" + override 按鈕）
6. user 對 unmatched device 點「選現有」
   → opens dropdown (autocomplete from tenant.devices)
   → user 選 → 該列 matched_id_override 寫 localStorage decisions
7. user 對另一 unmatched device 點「新建」
   → opens InlineCreateDialog (form: name / ip / os / device_type / status)
   → user 填完 → 暫存到 localStorage inline_creates
   → 該列顯示 "已新建 + Web-02"（淡藍 badge，confirm 後才真的寫 DB）
8. user 對 fuzzy match 點「override」
   → 可選 "改選現有" / "改新建" / "純文字保留" / "確認此模糊匹配"
   → default action: 確認此模糊匹配 (use_matched)
9. user 巡完 9 個 sheet → 點 "確認匯入" 按鈕
   → opens 確認 modal: 顯示總計（X 配對 / Y 新建 / Z 純文字 / W 跳過）
   → user 點 "確定"
   → POST /ssp-excel-import/<parse_uid>/confirm with decisions + inline_creates + content_overrides
   → BE @transaction:
     1. 套 content_overrides
     2. 寫 inline_creates 到 tenant 表 → 拿 id
     3. 套 decisions（matched_id_override / inline_create_row_idx 對應）
     4. _write_all_data (A4 8-step)
     5. 返 import_summary
   → FE 收到 success → 清 localStorage → redirect /module-frame/<module_frame_uid> 顯示成功 toast
```

### 5.2 錯誤路徑

| 情境 | UI 行為 |
|------|---------|
| 上傳 .xlsx 結構錯誤（缺 sheet / 缺欄位）| `parse` 回 status='failed' + error_code GRC_400xxx；FE 顯示「樣板版本不符」+ 重新上傳按鈕 |
| Inline 新建 device.name 重複 | confirm 整段 rollback；FE 跳 modal 顯示「設備名稱 'Web-02' 已存在」+ 引導 user 選現有 |
| Confirm 撞 SSP 已存在（update flow 缺 SSP）| BE raise `GRC_412026`；FE 跳 modal「對應的 MF 尚未建立 SSP，請先 ...」|
| User 離開頁面（瀏覽器關閉）| localStorage 保留；下次回到此 parse_uid 自動恢復 + 顯示「上次編輯於 <time>」 |
| Parse job TTL 過期 | `GET /ssp-excel-import/<parse_uid>` 回 404；FE 顯示「解析資料已過期，請重新上傳」+ 清 localStorage |

### 5.3 跨角色情境

- **Manager 上傳 + 編輯 + confirm**：完整流程
- **Auditor 上傳 + 編輯 + confirm**：同 manager 流程（A2 既有權限規則）
- **Viewer / 無權限**：route 入口直接 403（沿 A2 既有 jwt + role check）

---

## 6. Acceptance Criteria

### 6.1 BE Acceptance

| # | AC | 驗證方式 |
|---|----|--------|
| BE-1 | Confirm payload schema 接 decisions / inline_creates / content_overrides；錯誤格式回 400 + error_code | pytest `test_a5_confirm_schema.py` |
| BE-2 | Inline 新建 device 在 @transaction 內寫 `public.devices` → 寫 SSP item 帶 `device_id` FK | pytest `test_a5_inline_create_device.py`（mock + real domain service） |
| BE-3 | Inline 新建 info_system 同上對 `compliance.information_systems` + SSP item 帶 `system_characteristic_id` FK | pytest `test_a5_inline_create_info_system.py` |
| BE-4 | Inline 新建 org_unit / user 透過 jedi-auth domain service | pytest `test_a5_inline_create_party.py` |
| BE-5 | Content_overrides 蓋掉 parsed_result 對應欄位（basic / controls.impl / controls.objectives）後再寫入 | pytest `test_a5_content_overrides.py` |
| BE-6 | Decisions skip_as_text → SSP item 寫純文字，FK=null | pytest `test_a5_decision_skip_as_text.py` |
| BE-7 | Inline 新建撞 duplicate / validation 錯誤 → 整段 @transaction rollback；無髒資料 | pytest `test_a5_inline_create_rollback.py` |
| BE-8 | `DELETE /ssp-excel-import/<parse_uid>` soft delete parse job，後續 GET 回 404 | pytest `test_a5_discard_endpoint.py` |
| BE-9 | import_summary 含 `inline_created_*` + `decision_applied_count` + `skipped_as_text_count` 計數 | pytest 範圍同 BE-2 / BE-3 |
| BE-10 | 0 regression — a2/a3/a4/a5 ssp_excel 全綠 | `pytest tests/test_a*_*.py tests/test_ssp_excel_import_app_service.py` |

### 6.2 FE Acceptance

| # | AC | 驗證方式 |
|---|----|--------|
| FE-1 | `/module-frame/import-excel` 路由 + 上傳頁開得起來 | manual smoke |
| FE-2 | 9 sheet tab 全部能展示 parsed_result（基本資料 / parties / devices / info_systems / leveraged / controls / refdocs） | manual smoke |
| FE-3 | Unmatched device 點「選現有」彈 dropdown，autocomplete tenant.devices | manual smoke |
| FE-4 | Unmatched device 點「新建」彈 InlineCreateDialog，填完 confirm 後該列顯示淡藍 badge | manual smoke |
| FE-5 | Fuzzy match 顯示 confidence + override 按鈕；override 可改 4 種 action | manual smoke |
| FE-6 | 編輯欄位 / 切 tab / 重整頁面 → state 由 localStorage 恢復 | manual smoke |
| FE-7 | 確認匯入 modal 顯示總計 + 點 "確定" 觸發 POST confirm | manual smoke |
| FE-8 | Confirm 成功 → 清 localStorage + redirect MF 詳細頁 + success toast | manual smoke |
| FE-9 | i18n zh-tw + en | manual smoke 切語言 |
| FE-10 | 上傳錯誤 / parse 過期 / confirm 失敗 各種錯誤路徑顯示對應錯誤訊息 | manual smoke |

---

## 7. 風險與緩解

| 風險 | 緩解 |
|------|------|
| Inline 新建 user / org_unit 撞 jedi-auth domain service 簽章變動 | 開工前 T0 verify：`UserDomainService.add` / `OrgUnitDomainService.add` 簽章；落地偏差進 §11 |
| Confirm `@transaction` 過長（5 entity write + N inline create + content_overrides）超時 | 假設 N ≤ 10（單次匯入小）；若 prod 大量 inline create 再評估批次 |
| localStorage 容量限制（parsed_result 大型 SSP 過大）| 預估 ~500KB；瀏覽器 5-10MB 限制夠用。若超量再評估 IndexedDB |
| FE inline 新建 dialog form fields 跟 tenant 表 schema 不一致 | T0 verify：4 種 entity 必填欄位列在 design § 4.1 InlineCreateSchema.payload 子欄位附註 |
| User confirm 後發現錯誤想 undo | 沒做 — confirm 是 final commit；user 要 undo 走另外的 MF / SSP 編輯路徑（非本 phase 範圍）|

---

## 8. 跨 repo 工作

| Repo | 範圍 | branch |
|------|------|--------|
| compliance-manager-be | BE confirm schema + inline_creates handling + DELETE endpoint + pytest | `feature/ssp-import-export-phase2`（沿用）|
| compliance-manager-fe | `/module-frame/import-excel` 路由 + 11 個 Vue 元件 + Pinia store + service + i18n | 新建 `feature/ssp-import-export-phase2` 對齊 BE 命名 |
| compliance-manager-test | cucumber 拍板跳過（F-A5-cucumber follow-up） | — |
| jedi-* | 不動 | — |

---

## 9. Pre-flight Verification（T0 必驗）

開工前 T0 verify 以下假設，落地偏差進 §11：

1. **既有 `SspExcelImportConfirmRequestSchema.decisions / overrides` 彈性 dict** → 改成結構化 schema 後 A2 既有 test 是否 break
2. **jedi-auth `UserDomainService.add` / `OrgUnitDomainService.add` 簽章** → 確認可在 BE 主專案 `@transaction` 內 invoke
3. **`DeviceDomainService.add` / `InformationSystemDomainService.add` 簽章** → 同上
4. **既有 `_dict_to_parsed_*` helper 是否易於套 content_overrides**（A4 ship 已寫，T8 內）
5. **FE 既有 BaseService extension pattern**（如 `MfTemplateService.js`）
6. **PrimeVue Dialog / Dropdown / Autocomplete 既有用法 reference**

---

## 10. Implementation Reality / Reconciliation

### 10.1 T0 Pre-flight verify 偏差（Session B 開工前 — 2026-05-20）

#### 10.1.1 — Domain service `.add()` 方法名 3/4 不一致（plan §0.2 假設破裂）

Plan 假設 4 個 inline create domain service 都用 `.add(entity)` 統一簽章。實際：

| Domain Service | Plan 假設 | 實際 method |
|---------------|----------|------------|
| `DeviceDomainService` | `.add(entity, user_context)` | `.add_device(entity)` |
| `InformationSystemDomainService` | `.add(entity, user_context)` | `.add(entity)` ✓ |
| `OrgUnitDomainService` (jedi-auth) | `.add(entity, user_context)` | `.add_org_unit(entity)` |
| `UserDomainService` (jedi-auth) | `.add(entity, send_invite_flag)` | `.add_user(entity, is_admin=False)` |

**對應**：`_apply_inline_creates` 改成 entity_type → method-name dispatch（不能用統一 `.add`）。Entity 內 `created_user / updated_user / tenant_id` 在 dispatch 前 populate（method 簽章不收 user_context）。

#### 10.1.2 — `UserDomainService.add_user` 無 `send_invite` 機制

Plan 設計 inline-create user 走 `+ send_invite=True` 自動寄邀請信。實際 `add_user(entity, is_admin=False)` 只 hash password + insert，不發信。

**對應**：A5 phase 範圍**不發邀請信**。Inline 新建的 user 帳號由 admin 後續手動處理（最簡 path；user 拍板 2026-05-20）。若要發信走 follow-up（建議名稱：`F-A5-user-invite-on-inline-create`）。

#### 10.1.3 — DELETE endpoint + `discard_parse` 已 ship（A2 階段）

Plan T5 假設要新加 DELETE route + app service `discard_parse` method。實際 A2 phase 已 ship 兩者：

- Route：`api/oscal/routes/ssp/ssp_excel_import_route.py:85-93` (`SspExcelImportRoute.delete()`)
- App service：`app/oscal/service/ssp_excel_import_app_service.py:256-264` (`discard_parse`)
- Schema：`SspExcelImportDiscardResponseSchema` 已存在

**對應**：T5 縮為「補 ~3 個 discard pytest」（併入 T7 範圍處理）。T5 commit 取消。

#### 10.1.4 — content_overrides 命名沿 docx pattern（`implementation` + `objectives[<key>]`）

Design §4.1 原寫 `controls["AC-1"]["implementation_description"]`。但 parsed_result 實際用 `controls_with_aos[i]["statement"]` 同欄位給 control + AO。為避免 mapping 層，content_overrides shape **沿 docx pattern**（已有先例，見 `api/oscal/serializers/ssp/ssp_docx_import.py:55`）：

```json
{
  "metadata": {"target_mf_name": "<new>", "target_system_name": "<new>", ...},
  "controls": {
    "AC-1": {
      "implementation": "<new control 層 statement>",
      "objectives": {"(a)": "<new>", "(b)": "<new>"}
    }
  }
}
```

`_apply_content_overrides` 把 `implementation` 蓋到 parsed_result 對應父 control row 的 `statement` 欄位；`objectives[<key>]` 蓋到對應 AO row 的 `statement` 欄位。

#### 10.1.5 — Error code 起始序號

T0.5 verify 結果：

- 400 last=070 → A5 新增從 `GRC_400071` 開始
- 409 last=030 → A5 新增從 `GRC_409031` 開始
- 412 last=026 → A5 新增從 `GRC_412027` 開始

#### 10.1.6 — Tests 跳過 schema 驗證

T0.1 verify：既有 a2/a3/a4 confirm test 全部直接 `svc.confirm_import("u", {}, user_context)` 或 `{"decisions": [], "overrides": {}}` — 跳過 marshmallow schema layer。

**對應**：A5 app service `confirm_import` 內讀 payload 必用 `.get("decisions") or []` / `.get("inline_creates") or []` / `.get("content_overrides") or {}` defensive 模式 — 確保既有 tests 不破壞，schema 結構化只在 route layer 生效。

### 10.2 BE 實作偏差

#### 10.2.1 — Decisions 對 write-time FK 不直接 propagate（已知限制）

**問題**：A5 `_apply_decisions` 蓋掉 `parsed_result[<sheet>][row_idx]["matched_*_id"]` （給 FE preview 一致性），但 `_write_all_data` 8-step pipeline 內 Step 3 `_dict_to_parsed_*` 不讀回 `matched_*_id`，Step 4 orchestrator reconciler 重新算 ParsedDevice 等 typed entity 的 `matched_*_id`，Step 7 再 by-key 對齊寫回 dict — 結果 A5 user decision 被 orchestrator 結果覆寫。

**影響**：
- ✅ `inline_creates` 正確寫入 4 種 tenant entity 並於 transaction rollback 時清除（atomicity 保證）
- ✅ `import_summary` A5 stats 計數正確（inline_created_*, decision_applied_count, skipped_as_text_count）
- ✅ `parsed_result` mutation 套用後可由 FE 重讀 `GET /ssp-excel-import/<uid>` 看到
- ⚠ 但實際 DB 寫入的 SSP item FK 仍由 orchestrator 決定 — `use_existing` 改 ID / `use_inline_new` 對應到 new_id 都不會反映在 control_implementations / ssp_system_implementation_items 的 FK 上（除非剛好 orchestrator 也選同一個）

**為什麼 Session B 不修**：
- 修正需動 A4 `domain/oscal/service/reconciliation/base.py` 加 `_already_resolved_by_decision()` hook（讓 reconciler 跳過已有 user decision 的 row），跨 A4 / A5 邊界
- A5 design §2.3 明確排除「不改 A4 ship 的 reconciler / WriteStrategy / pipeline 行為」
- Session C FE 完成後可確認真實 user-facing 影響再決定是否升級為 follow-up bug fix

**Follow-up**：`F-A5-decision-write-propagation`
- 在 `_reconcile_one()` 開頭加 `if self._already_resolved_by_decision(parsed): return`
- `_dict_to_parsed_*` helper 把 A5 decision 標記寫到 ParsedDevice / ParsedInfoSystem / etc.（例如 `_user_decision_locked=True` 屬性）
- Session C FE smoke 完後評估 priority

### 10.3 FE 實作偏差

#### 10.3.1 — FE 檔案結構跟 design §3.2 不一致（FE 既有慣例 override）

design §3.2 列：
- `src/service/oscal/SspExcelImportService.js`
- `src/store/modules/sspExcelImport.js`
- `src/router/routes.js`
- `src/locales/{zh-tw,en}/module-frame.js`
- `src/views/module-frame/ImportExcel.vue`

實際 FE 慣例：
- `src/service/SspExcelImportService.js`（service 平鋪，無 oscal/ 子目錄；mirror SspDocxImportService.js）
- `src/stores/sspExcelImportStore.js`（複數 `stores/` + Store 後綴；mirror sspDocxImportStore.js）
- `src/config/router/index.js`（routes 集中於 config/）
- `src/config/locales/i18n/{zh-tw,en}/ssp-excel-import.json`（JSON 格式 + 各 feature 一檔；spread 到 lang.*）
- `src/views/module_frame/ImportExcelPage.vue`（underscore `module_frame` 不是 hyphen）

**對應**：全部 FE 改動依 FE 既有 docx import pattern 對齊 — 路徑跟 design 不一致是 design 寫得太早未驗 FE repo。

#### 10.3.2 — 5 sheet 而非 9 sheet（BE schema 限制）

design §3.2 plan 7 個 sheet preview Vue component（covering 00_說明 + 01-08 共 9 sheet）。實際：

- BE `DecisionSchema.sheet` OneOf 只 5 個：`devices / info_systems / leveraged / parties / controls`
- 其他 4 sheet（00_說明 / 01_基本資料 / 02_程序書 / 控制項 AO 文字）走 `content_overrides`，不需 per-row DataTable

**對應**：T11 ship 5 個 decision sheet preview（Devices / InfoSystems / Leveraged / Parties / Controls）+ MatchStatusCell 共用元件。SheetPreviewBasic + SheetPreviewRefDocs 留 follow-up `F-A5-content-overrides-ui`（T14 polish 範圍）。

#### 10.3.3 — Upload + Preview 拆 2 個 route（跟 design §5.1 / docx pattern 不同）

design §5.1 step 2 預期 `/module-frame/import-excel/<parse_uid>` 路由直接 redirect 到同 page；docx 既有 ImportDocxPage 是「單 page 同時做 upload + preview」。

實際：
- `/module-frame/import-excel` (create) → ImportExcelPage.vue（純上傳）
- `/module-frame/:uid/import-excel` (update) → 同上
- `/module-frame/import-excel/preview/:parseUid` → ImportExcelPreviewPage.vue（純預覽）

**對應**：拆 page 換得 page-reload 跨 navigation 恢復 + share link（user 把 preview URL 給同事接手匯入）。Trade-off：增加 1 個 route + 1 個 view file，但 store / service / i18n 全共用。

#### 10.3.4 — RowActionMenu / 不寫 FuzzyBadge 元件

design §3.2 plan 列 `UnmatchedRow + InlineCreateDialog + FuzzyBadge` 三個共用元件。實際：

- `RowActionMenu.vue` — 替代 UnmatchedRow，用 PrimeVue Menu popup 顯示 4 action（pick_existing / inline_new / skip_as_text / reset）
- `InlineCreateDialog.vue` — 4 entity form（device / information_system / org_unit / user）
- `EntityPickerDialog.vue` — 新增（從 menuStore 拿對應 menu + PrimeVue Dropdown filter）
- ~~`FuzzyBadge.vue`~~ — 不另寫；fuzzy 標示 + confidence % 整合進 `MatchStatusCell.vue`（T11 ship）

**對應**：fuzzy / unmatched / matched 都在同個 status cell 一致顯示，user override 走 RowActionMenu。少寫一個 component，職責更清楚。

#### 10.3.5 — Leveraged sheet RowActionMenu 不接（BE 不支援）

BE `InlineCreateSchema.entity_type` OneOf：`device / information_system / org_unit / user` — **不含 leveraged party**。leveraged 的 `matched_party_uuid` 對應 OSCAL Party (org / person)，但無對應 party menu API 給 FE 用 picker。

**對應**：
- T12 RowActionMenu 未接 leveraged sheet，action column 為 `─` placeholder
- leveraged 仍能透過 BE A3/A4 reconciler 自動配對（不影響 happy path）
- Follow-up `F-A5-leveraged-action`：T14 polish or 後續評估是否值得補 party menu API + inline-create leveraged 流程

#### 10.3.6 — localStorage shape 跟 §4.3 example 不一致

§4.3 示例：

```js
{ decisions: [...flat array...], inline_creates: [...flat array...] }
```

實際 store 持久化 shape：

```js
{
  version: 1,
  saved_at: "<iso>",
  decisions: { devices: { 0: { action, matched_id_override, inline_create_local_id } }, ... },
  inlineCreates: { ic_xxx_1: { sheet, row_idx, entity_type, payload }, ... },
  contentOverrides: { metadata: {...}, controls: {...} },
  filters: { devices: 'all', ... }
}
```

**差別**：
- `decisions` 用 `sheet → rowIdx` nested dict 而非 flat array — DataTable per-row lookup O(1)
- `inlineCreates` 用 Map<localId> 而非 array — 避免 user 取消某筆 inline create 後 splice 引發其他 decisions 的 `inline_create_row_idx` shift
- 加了 `contentOverrides` + `filters` 進持久化（reload 後保留編輯狀態 + 已選 filter）
- `buildConfirmPayload()` 才把 Map 重新 enumerate 成 BE schema array 並 remap localId → array index

**對應**：localStorage 是 FE 私有格式（BE 不 touch），internal shape 自由設計。Trade-off 已寫在 `src/stores/sspExcelImportStore.js` 開頭 comment（"Why a Map for inlineCreates"）。

#### 10.3.7 — T14 manual smoke 拋給 user 跑（agent 無 GUI）

plan §2.T14 寫「manual smoke：上傳 → 預覽 → 編輯 → unmatched 三路徑 → fuzzy override → confirm → 驗 DB」隱含 agent 跑完整流程。實際 agent 無 browser interactive 能力。

**對應**：
- T14 agent 完成的是 `npm run build:DEV` build verify（compile / lint / import 解析全過 — ImportExcelPreviewPage 30.32 kB chunk ship 進 dist）
- Manual smoke 寫成 8 段 checklist 給 user 自跑：`docs/features/FR-011.2-2605-ssp-import-export-phase2/handoff/2026-05-21-a5-smoke-checklist.md`
- T15 收口完成；user 跑 smoke 撞到 bug → 走 docs/issues/ 流程（`2026-05-21-a5-smoke-to-bugfix.md` handoff prompt 已備）

#### 10.3.8 — T11 從 7 個獨立 component 變 5 sheet + 1 共用 cell

plan §2.T11 列「7 個 sheet preview 元件」（含 SheetPreviewBasic / Parties / Devices / InfoSystems / Leveraged / Controls / RefDocs）。實際 ship：

- 5 個 decision sheet preview (Devices / InfoSystems / Leveraged / Parties / Controls)
- 1 個共用 `MatchStatusCell.vue`（matched / fuzzy / unmatched / inline_new / override / skip 共 6 種狀態）
- 0 個 Basic + RefDocs preview（content_overrides 路徑，留 `F-A5-content-overrides-ui` follow-up）

**對應**：
- 共用 MatchStatusCell 替代 plan 隱含的 per-sheet status 重複碼
- Basic + RefDocs 跟 §10.3.2 (5 sheet 而非 9 sheet) 一致 — 走 content_overrides 不需要 DataTable
- 實際 component 數 = 5 sheet preview + 3 共用元件（MatchStatusCell / RowActionMenu / 2 dialogs）= 9 個 .vue 檔（比 plan 7 個多 — 因為拆共用元件）

### 10.4 跨 session 偏差總覽

| § | 偏差類 | 嚴重度 |
|---|------|------|
| 10.1.1-10.1.6 | T0 pre-flight verify 6 條（domain service signature / DELETE 已 ship / content_overrides 命名 / error code 序號 / test 跳 schema） | medium — 不阻塞、改寫 helper 即可 |
| 10.2.1 | Decisions 對 write-time FK 不直接 propagate；F-A5-decision-write-propagation 待 smoke 評估 | medium-high — user 改 decision 可能不反映到 DB |
| 10.3.1 | FE 檔案結構照 FE 慣例改 | low — 對齊 docx pattern |
| 10.3.2 | 5 sheet 而非 9 sheet | low — BE schema 本來就 5 個 |
| 10.3.3 | Upload + Preview 拆 2 route | low — UX 偏好 |
| 10.3.4 | 共用元件命名 + 不寫 FuzzyBadge | low — 職責整合 |
| 10.3.5 | Leveraged action column 空 | medium — UX 缺洞，留 follow-up |
| 10.3.6 | localStorage shape 跟 §4.3 example 不一致（dict + Map 而非 flat array） | low — FE 私有格式；trade-off 在 store comment |
| 10.3.7 | T14 manual smoke 拋給 user 跑（agent 無 GUI） | low — checklist 已備 |
| 10.3.8 | T11 從 7 個獨立 sheet preview 變 5 sheet + MatchStatusCell 共用元件 | low — 拆共用元件比每 sheet 重寫好維護 |

---

## 11. 執行階段切分（建議 3 session）

| Session | 範圍 | Task | 預估 | Commit checkpoint |
|---------|------|------|------|-------------------|
| **A**（當前）| Design + Plan | design-A5.md + implementation-plan-A5.md + T0 verify | 0.5d | `docs(ssp-import-export-phase2): A5 design + plan` |
| **B** | BE 實作 | T1 schema 定稿 + T2 inline_creates handling + T3 DELETE endpoint + T4 content_overrides + T5 pytest | 1.5d | `feat(oscal): A5 BE confirm schema + inline_creates` |
| **C** | FE 實作 + 收尾 | T6 路由 + Vue 元件 + Pinia store + i18n + manual smoke + SUMMARY | 2d | `feat(fe): A5 import-excel preview page` + `docs(ssp-import-export-phase2): A5 收尾` |

**總計**：3 session、~4d（含 design / BE / FE / 收尾）

**注意**：4 天略超 spec sizing rule 4 (3-day cap)。理由：A5 BE + FE 不能拆兩個 spec（拆掉就破壞「ship 後 user 能用」規則 #1）。Mitigation：partial ship pattern — BE 先 ship 後可獨立 smoke test（FE 還沒做時可用 curl 驗證）；FE ship 後才是完整 user-facing 完成。

### 11.1 換 session 收尾規範

按 CLAUDE.md「做 summary 觸發完整收尾」段：
1. 盤點 commits + working tree 乾淨
2. 規範文件齊全度檢查（changelog / analysis / issue / design §10 reconciliation）
3. 產 handoff prompt → `docs/features/FR-011.2-2605-ssp-import-export-phase2/handoff/2026-05-XX-a5-<session 字母>-next.md`
4. Session C 是 task arc 收口 → 對話歷史 dump 到 `docs/conversation-history/<date>/ssp-import-export-phase2-A5/`

---

## 12. 設計決策溯源（user 拍板）

### 12.1 主要拍板（2026-05-20，A4 ship 後）

| # | 議題 | 拍板 | 理由 |
|---|------|------|------|
| Q1 | Inline 新建時機 | Confirm 階段 BE 一次處理 | 原子性最強；user 取消預覽不留髒資料 |
| Q2 | Fuzzy match UI 行為 | 預設 auto-accept + fuzzy badge，允許 user override | 沿 A3/A4 既有行為；balance UX 流暢度 vs 準確度 |
| Q3 | FE 路由結構 | 新建 `/module-frame/import-excel` 獨立頁面 | docx 跟 Excel 資料結構差異大（iframe PDF vs 9 sheet 表格），共用元件度低；獨立路由 maintenance 更清楚 |

### 12.2 預設拍板（沿 A1-A4 慣例，不額外問）

- DDD 嚴格分層（Route 不查 DB / app service @transaction / domain service 走 add()）
- Error code 命名沿 `GRC_<status><3 位序號>` 規則
- 跨 repo branch 名稱對齊 `feature/ssp-import-export-phase2`
- jedi-* 不動，feature 全部完工才 bump version
- Cucumber 拍板跳過，留 F-A5-cucumber follow-up
- localStorage 暫存策略沿 docx import 既有 pattern
