# Bug O Implementation Plan — Docx 重 import diff stepper 4 key 擴展 + TabView refactor

> **For agentic workers:** 跟著本 plan 一個 task 一個 task 跑；每個 phase 完成跑 test → commit → 進下一 phase。

**Goal:** Docx 重 import (update mode) 進 diff stepper 時，補完 4 個缺漏 key (system_characteristic / components / leveraged_authorizations / inventory_items) 的 per-row decision 機制，並把 DiffResolutionStep 改成 TabView 容器。

**Architecture:**
- BE：`SspDocxDiffService` 加 4 個 annotation method + `build_diff_summary` 擴展。`SspDocxImportAppService.confirm_import` 加 `_apply_v3_decisions` filter 在 `_run_v2_bundle_confirm` 之前把 decision 套到 parsed_result（含 overwrite caveat — `keep_current` 必須 inject current row 防 delete）。
- FE：`sspDocxImportStore.decisions` 加 4 sub-map + `buildConfirmPayload` 擴展。新增 4 個 `*DiffSection.vue` + 4 個 `*DiffCard.vue`（mirror `PartyDiffCard` pattern）。`DiffResolutionStep.vue` 重構成 6-tab TabView。

**Tech Stack:** Python 3.11 / SQLAlchemy / pytest / Vue 3 / Pinia / PrimeVue 3.53 (TabView / DataTable / Dropdown / SelectButton)

---

## ⚠️ 關鍵設計 caveat（開工前必看）

### Caveat 1：3 strategy 是 overwrite，不是 incremental upsert

`LeveragedWriteStrategy.write` / `ComponentWriteStrategy.write` / `InventoryItemWriteStrategy.write` 三者都是「delete all by ssp_id → insert from parsed list」（見 `domain/oscal/service/write_strategy/{leveraged,component,inventory_item}_write_strategy.py` 各自的 "Overwrite semantics" docstring）。

**後果**：對 docx 重 import 的 decision filter，正確語意是：

| diff_status × decision | 動作 |
|---|---|
| changed × use_docx | parsed_row 進 final list |
| changed × keep_current | **current_row 進 final list**（不然被 delete） |
| changed × skip | 同 keep_current（保留現值不被 delete） |
| added × use_docx | parsed_row 進 final list |
| added × skip | drop（不寫） |
| gone × keep_current | **current_row 進 final list**（防 delete） |
| gone × use_docx | drop（accept delete） |
| unchanged × * | current_row 進 final list（沒 decision 但要保留） |

所以 `_apply_v3_decisions` 結尾要 set `parsed_result[list_key] = final_list`，覆寫掉原本 parsed_result 內的 list。

### Caveat 2：current entity → dict 的 shape 要對齊 parsed dict

`ParsedComponent` / `ParsedLeveragedAuthorization` / `ParsedInventoryItem` dataclass field name 跟 DB entity field name **不完全一樣**。Inject current row 進 final list 時要 mapper：

| Parsed dataclass field | Current entity 來源 | 備註 |
|---|---|---|
| `ParsedComponent.title` | `ComponentEntity.title` | 對齊 |
| `ParsedComponent.component_type` | 對齊 | |
| `ParsedComponent.leveraged_authorization_ref` | **反查** `entity.leveraged_authorization_uid` → LA.title | 用 `(uid → title)` map reverse-lookup |
| `ParsedComponent.protocol/port_ranges/security_auth` | `entity.props["..."]` | from JSONB |
| `ParsedLeveragedAuthorization.fedramp_package_id/impact_level/...` | `entity.props["..."]` | from JSONB |
| `ParsedInventoryItem.asset_id/asset_tag/ipv4_address/...` | `entity.props["..."]` | from JSONB |
| `ParsedInventoryItem.implemented_component_refs` | 反查 `entity.implemented_component_uids` → component.title | 用 `(uid → title)` map |

寫 entity → ParsedXxx dict 的 helper（推薦 stateless static method 在 diff service 或 import app service），複用 mapper logic。

### Caveat 3：Match key 設計

| Key | Primary match | Tiebreaker | 防呆 |
|---|---|---|---|
| Components | `(title.strip().lower(), component_type)` | 同 (title, type) 重複 → 用第一個（log warning） | title + type 為空 → skip pair |
| LA | `title.strip().lower()` | 同 title 重複 → 用第一個 | title 為空 → skip pair |
| Inventory | `description.strip().lower()` | 同 description 重複 → 用第一個 | description 為空 → skip pair |
| SC | 單筆 by ssp_id — 不 match | N/A | N/A |

Mirror `SspDocxDiffService.match_parties` 的 dual-index pattern。

### Caveat 4：System Characteristic 是單筆 dict 不是 list

- `decisions` 是 **單一 action**（不是 per-row map）：`system_characteristic_decision: "use_docx" | "keep_current" | "skip"`
- annotation 結構：`annotated["system_characteristic"] = {current_values, parsed_values, diff_status, default_action}`，沒 row_uid 也沒 list
- `_apply_v3_decisions` 對 SC 是「決定要不要呼叫 `_sc_write_strategy.write`」 — `use_docx` → 寫 parsed；`keep_current` 或 `skip` 或 `unchanged` → 不寫（DB 保持現值 — SC 是 upsert by ssp_id 不是 overwrite，不寫等於保留）

### Caveat 5：FE TabView 用 `:active-step` 還是 `:active-index`?

PrimeVue 3.53 `TabView` prop 是 `:active-index="..."` （per 既有 docs），但 PrimeVue **`Steps`** 才用 `:active-step`（per memory `feedback_primevue_quirks`）。**Bug O 用的是 TabView 不是 Steps，所以 `:active-index` 才對**。實作前先 read `~/Projects/Billows/Audit-Manager/compliance-manager-fe/CLAUDE.md` 對 TabView 的描述。

### Caveat 6：BE service 改完必提醒 user 重啟

每 phase BE 改完跑 pytest pass 後，commit 訊息加 「⚠️ 需重啟 BE pid <X>」（per memory `feedback_be_restart_after_service_change`）。

---

## Phase O-A：BE diff service 擴展（4 key annotation + summary）

**Files:**
- Modify: `app/oscal/service/ssp_docx_diff_service.py`（既有 290 行，加 ~350 行）
- Test: `tests/test_ssp_docx_diff_service.py`（加 ~200 行）

### A.0 Pre-flight verify

- [ ] **A.0.1 Read 既有 diff service 完整 source**：`app/oscal/service/ssp_docx_diff_service.py` 1~290 line（已讀，pattern 在前述 caveat 對齊）
- [ ] **A.0.2 Read parsed dataclass**：`domain/oscal/parser/ssp_intermediate.py:158-265`（ParsedComponent / LA / Inventory / SC）— 確認 field name + type
- [ ] **A.0.3 Read 3 entity model**：`jedi_oscal/infra/model/base/{oscal_component,oscal_leveraged_authorization,oscal_inventory_item}.py` 看 entity → dict 要 cover 哪些 column
- [ ] **A.0.4 確認 SC entity 是否有 `update_one_by_ssp_id`**：grep `SystemCharacteristicEntity` `update_by_ssp_id` `upsert` — verify SC 寫入是 upsert by ssp_id 而非 overwrite（per Caveat 4）

### A.1 Helper: entity → parsed dict mapper

- [ ] **A.1.1 寫 TDD test**：`tests/test_ssp_docx_diff_service.py` 加 `test_component_entity_to_parsed_dict_roundtrip` — 給 mock ComponentEntity（含 props JSONB），call helper，assert 出來的 dict 含 `title / component_type / description / purpose / status / leveraged_authorization_ref / protocol / port_ranges / security_auth`

```python
def test_component_entity_to_parsed_dict_roundtrip():
    from unittest.mock import Mock
    from app.oscal.service.ssp_docx_diff_service import SspDocxDiffService
    svc = SspDocxDiffService()
    entity = Mock(
        title="X", component_type="service", description=None, purpose=None,
        status="operational", leveraged_authorization_uid=None,
        props={"protocol": "TCP", "port_ranges": "443", "security_auth": "OAuth"},
    )
    out = svc._component_entity_to_parsed_dict(entity, la_uid_to_title={})
    assert out["title"] == "X"
    assert out["component_type"] == "service"
    assert out["protocol"] == "TCP"
    assert out["leveraged_authorization_ref"] is None
```

- [ ] **A.1.2 Run test → expected FAIL**：`poetry run pytest tests/test_ssp_docx_diff_service.py::test_component_entity_to_parsed_dict_roundtrip -v` → method not exist
- [ ] **A.1.3 實作 `_component_entity_to_parsed_dict(entity, la_uid_to_title) -> dict`** — 在 diff service 加 static method，從 entity build dict，props JSONB key 攤平到 top-level
- [ ] **A.1.4 Run test → PASS**
- [ ] **A.1.5 同樣 TDD 寫 `_leveraged_entity_to_parsed_dict(entity)` + impl + pass**（含 props 攤平：fedramp_package_id / impact_level / data_types / nature_of_agreement / authorized_users + LA 直接 column：title / date_authorized / remarks）
- [ ] **A.1.6 同樣 TDD `_inventory_entity_to_parsed_dict(entity, component_uid_to_title)`**（props 攤平：asset_id / asset_tag / ipv4_address / mac_address / fqdn / hostname / software_name / os_name；description 直接從 column；implemented_component_refs 從 `entity.implemented_component_uids` 反查 component title list）
- [ ] **A.1.7 同樣 TDD `_sc_entity_to_parsed_dict(entity)`**（system_name / security_sensitivity_level / status / target_type / scope_description / system_identifier / description；owner_login_name 從 owner_uid 反查 user.login_name — 或先 leave None Bug O 不解）

### A.2 Match function (component / LA / inventory)

- [ ] **A.2.1 TDD `test_match_components_by_title_and_type`** — current 2 row（不同 type），parsed 2 row 同 title 不同 type → pair 2 對；無 unmatched
- [ ] **A.2.2 實作 `match_components(current, parsed) -> tuple[pairs, only_current, only_parsed]`** mirror `match_parties` dual-index pattern，key = `(title.strip().lower(), component_type)`
- [ ] **A.2.3 同樣 TDD + impl `match_leveraged_authorizations`**（key = `title.strip().lower()`）
- [ ] **A.2.4 同樣 TDD + impl `match_inventory_items`**（key = `description.strip().lower()`）

### A.3 Per-row diff compute

- [ ] **A.3.1 TDD `test_compute_component_diff_changed`** — current `{title:X, status:operational}`, parsed `{title:X, status:disposition}` → (changed, keep_current)
- [ ] **A.3.2 實作 `compute_component_diff(current, parsed) -> (DiffStatus, DefaultAction)`** — 比較 fields list（title / component_type / description / purpose / status / leveraged_authorization_ref / protocol / port_ranges / security_auth），任一不同 → changed
- [ ] **A.3.3 同樣 TDD + impl `compute_leveraged_diff`** — fields: title / date_authorized / fedramp_package_id / impact_level / data_types / nature_of_agreement / authorized_users / remarks
- [ ] **A.3.4 同樣 TDD + impl `compute_inventory_diff`** — fields: description / asset_id / asset_tag / ipv4_address / mac_address / fqdn / hostname / software_name / os_name / implemented_component_refs（list 比對 set equal）
- [ ] **A.3.5 同樣 TDD + impl `compute_sc_diff`** — fields: system_name / security_sensitivity_level / status / target_type / scope_description / system_identifier / description（owner_login_name Bug O 不比）

### A.4 Annotation methods (annotate 4 key into result dict)

- [ ] **A.4.1 TDD `test_annotate_components_pairs_changed_added_gone`** — 給 current_list + parsed_list 各 2 個（1 same, 1 different, 1 only current, 1 only parsed）→ annotated 4 entry (1 unchanged, 1 changed, 1 gone, 1 added)
- [ ] **A.4.2 實作 `_annotate_components(parsed_result, current_components, la_uid_to_title)`** — 把 annotated list set 到 `parsed_result["components"]`，每 entry shape:
  ```python
  {
    "row_uid": str,  # current entity uid (matched/gone) or "new-<sha1(title+type)>" (added)
    "title": str,    # display name
    "diff_status": "changed" | "added" | "gone" | "unchanged",
    "default_action": "keep_current" | "use_docx" | None,
    "current_values": dict | None,  # entity → parsed dict
    "parsed_values": dict | None,
  }
  ```
- [ ] **A.4.3 同樣 TDD + impl `_annotate_leveraged_authorizations`**
- [ ] **A.4.4 同樣 TDD + impl `_annotate_inventory_items`**
- [ ] **A.4.5 同樣 TDD + impl `_annotate_system_characteristic(parsed_result, current_sc)`** — 單筆，set 到 `parsed_result["system_characteristic_diff"]`（不覆蓋原 `parsed_result["system_characteristic"]` dict — 為了 `_run_v2_bundle_confirm` 仍能讀），shape:
  ```python
  {
    "diff_status": str,
    "default_action": str | None,
    "current_values": dict | None,
    "parsed_values": dict | None,
  }
  ```

### A.5 build_diff_summary 擴展

- [ ] **A.5.1 改 `build_diff_summary`** 加 4 個 key counts (components / leveraged_authorizations / inventory_items / system_characteristic)
- [ ] **A.5.2 TDD `test_build_diff_summary_includes_4_v3_keys`** — annotated 內全 7 key 都有 entry → summary 含 7 個 key
- [ ] **A.5.3 改 `annotate_parse_result(parse_result, current_parties, current_components=None, current_las=None, current_inventory=None, current_sc=None)`** — 加 4 個新 optional param，None 時 skip 對應 annotation（backward-compat — 既有 caller 沒給就 skip）
- [ ] **A.5.4 改 `has_diff` 計算** — 加 4 個 key total_with_diff 也算進去（SC 用 total_with_diff = 0 if status==unchanged else 1）

### A.6 Run all diff service tests + commit

- [ ] **A.6.1 Run all diff service tests**: `poetry run pytest tests/test_ssp_docx_diff_service.py -v` → all PASS
- [ ] **A.6.2 Run regression smoke**: `poetry run pytest tests/test_ssp_docx_import_app_service.py tests/test_ssp_write_strategy.py -q` → still all PASS
- [ ] **A.6.3 Commit Phase O-A**：
  ```bash
  git add app/oscal/service/ssp_docx_diff_service.py tests/test_ssp_docx_diff_service.py
  git commit -m "feat(ssp-oscal-alignment): Bug O Phase A — BE diff service 擴展 4 key annotation + summary

  - 加 _annotate_components / _leveraged_authorizations / _inventory_items / _system_characteristic
  - 加 match_components / leveraged / inventory（mirror match_parties dual-index pattern）
  - 加 compute_xxx_diff 4 個 per-row compare
  - build_diff_summary 加 4 key counts；annotate_parse_result 加 4 optional param
  - 4 個 entity → parsed dict helper（attempt props JSONB 攤平 + LA/component ref 反查）

  Tests: tests/test_ssp_docx_diff_service.py 全 PASS
  Backward compat: annotate_parse_result 4 個 v3 current_* param 預設 None，既有 caller 不受影響

  Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
  ```

---

## Phase O-B：BE confirm 接 4 個新 decisions + decision filter

**Files:**
- Modify: `app/oscal/service/ssp_docx_import_app_service.py`（confirm_import + 新 method `_apply_v3_decisions` + `_load_current_v3_lists`）
- Modify: `common/code/grc_error_code.py`（加 4 個 invalid decision error code）
- Test: `tests/test_ssp_docx_import_app_service.py`

### B.0 Pre-flight

- [ ] **B.0.1 Read `confirm_import` line 511~709** 全完整 — 對齊 parties decision flow（line 563~575）
- [ ] **B.0.2 Read `_apply_v2_bundle_overrides` line 880~944** 確認 list overlap 動的 in-place mutation
- [ ] **B.0.3 grep 現有 v3 error code**：`grep "GRC_DOCX_DECISIONS_INVALID" common/code/grc_error_code.py` → 看序號接續從哪開始
- [ ] **B.0.4 Read `_run_v2_bundle_confirm` line 710~816** 看 SC write path 點

### B.1 加 GrcErrorCode

- [ ] **B.1.1 加 4 個 error code 到 `common/code/grc_error_code.py`**：
  ```python
  GRC_DOCX_DECISIONS_INVALID_COMPONENT_UID     = ("Component decision 引用 diff 沒有的 row_uid", "GRC_400xxx")
  GRC_DOCX_DECISIONS_INVALID_LA_UID            = ("Leveraged authorization decision 引用 diff 沒有的 row_uid", "GRC_400xxx")
  GRC_DOCX_DECISIONS_INVALID_INVENTORY_UID     = ("Inventory item decision 引用 diff 沒有的 row_uid", "GRC_400xxx")
  GRC_DOCX_DECISIONS_INVALID_SC_ACTION         = ("System characteristic decision action 不合法", "GRC_400xxx")
  ```
  序號接續既有 GRC_400 系列

### B.2 _load_current_v3_lists helper

- [ ] **B.2.1 TDD `test_load_current_v3_lists_project_ssp`** — mock ssp + repo，call helper → 回 dict `{components, leveraged_authorizations, inventory_items, system_characteristic, la_uid_to_title, component_uid_to_title}`
- [ ] **B.2.2 實作 `_load_current_v3_lists(source_uid, source_type) -> dict`** 在 SspDocxImportAppService — 內部呼叫 `self._la.get_all(LeveragedAuthorizationQueryEntity(ssp_id=...))` / 同 components / inventory / sc，回乾淨 dict
  - 注意：對 source_type=module_frame，可能要 resolve mf → template_ssp_id；若無 template SSP yet → 4 個 list 全 empty + sc=None
  - 注意：對 mf 沒 template SSP 的 case，annotate 仍能跑（current = 全 empty → 全 added）

### B.3 _validate_v3_decisions

- [ ] **B.3.1 TDD `test_validate_v3_decisions_rejects_unknown_uid`** — annotated components 含 row_uid `["abc"]`，decisions 含 `[{"row_uid": "xyz", "action": "use_docx"}]` → raise BadRequestError(GRC_DOCX_DECISIONS_INVALID_COMPONENT_UID)
- [ ] **B.3.2 實作 `_validate_v3_decisions(annotated, components_decisions, la_decisions, inventory_decisions, sc_decision)`** mirror `_validate_decisions` 既有 pattern
- [ ] **B.3.3 SC action 合法值 = `{"use_docx", "keep_current", "skip"}`** 或 None → reject 其它值

### B.4 _apply_v3_decisions（核心 — 含 overwrite caveat）

- [ ] **B.4.1 TDD `test_apply_v3_decisions_keep_current_injects_current_row`** — annotated 1 changed + 1 added，decisions 都 keep_current → final list 含 1 row (current)；added 因為 default skip 不在 final
- [ ] **B.4.2 TDD `test_apply_v3_decisions_use_docx_keeps_parsed_row`** — annotated 1 changed，decision use_docx → final list 含 parsed
- [ ] **B.4.3 TDD `test_apply_v3_decisions_gone_keep_current_prevents_delete`** — annotated 1 gone，decision keep_current → final list 含 current（防被 delete）
- [ ] **B.4.4 TDD `test_apply_v3_decisions_unchanged_always_keeps_current`** — annotated 1 unchanged → final list 含 current（不需 decision）
- [ ] **B.4.5 TDD `test_apply_v3_decisions_sc_use_docx_keeps_parsed`** + `test_apply_v3_decisions_sc_keep_current_clears_parsed`
- [ ] **B.4.6 實作 `_apply_v3_decisions(parsed_result, annotated, components_decisions, la_decisions, inventory_decisions, sc_decision)`**：
  - For each list_key in (components, leveraged_authorizations, inventory_items)：
    - Build `decision_map = {d["row_uid"]: d["action"] for d in decisions}`
    - Walk `annotated[list_key]`, for each entry decide using above table (Caveat 1)，build `final_list`
    - Set `parsed_result[list_key] = final_list`
  - For SC：
    - If sc_decision in {"keep_current", "skip"} OR annotated.sc.diff_status == "unchanged":
      - `parsed_result["system_characteristic"] = None`（會讓 `_dict_to_parsed_system_characteristic` 回 None，sc write skip）
    - Else (use_docx): leave parsed_result["system_characteristic"] as-is
- [ ] **B.4.7 Log warning（per Bug J 教訓不可 silent skip）**：對任何「reflexive merge」（如 decision="keep_current" 但 current_values is None）emit log.warning + treat as skip

### B.5 Wire 進 confirm_import

- [ ] **B.5.1 改 `confirm_import` line 563 附近，read 4 個新 payload key**：
  ```python
  components_decisions = payload.get("components_decisions") or []
  la_decisions = payload.get("leveraged_authorizations_decisions") or []
  inventory_decisions = payload.get("inventory_items_decisions") or []
  sc_decision = payload.get("system_characteristic_decision")  # str | None
  ```
- [ ] **B.5.2 改 `if self._diff_service is not None:` 段**：
  - 既有：load current_parties + annotate 只動 controls/parties
  - 改：同時 `_load_current_v3_lists` + 把 4 個 current_* 餵 annotate_parse_result
  - 改：`_validate_decisions` 之後加 `_validate_v3_decisions`
- [ ] **B.5.3 改 line 677 後**（已 apply v2_bundle_overrides）：加一行 `self._apply_v3_decisions(v2_parsed_result, annotated_parse_result, components_decisions, la_decisions, inventory_decisions, sc_decision)`
- [ ] **B.5.4 確認 backward compat**：既有 caller（FE Phase 4 / Excel import）沒送 4 個新 decision key → all 預設 empty/None → `_apply_v3_decisions` 行為跟「全 use_docx」一致（既有行為）

### B.6 Run all import service tests + commit

- [ ] **B.6.1 Run import service tests**: `poetry run pytest tests/test_ssp_docx_import_app_service.py -v` → PASS
- [ ] **B.6.2 Run regression smoke**: `poetry run pytest tests/test_ssp_docx_diff_service.py tests/test_ssp_write_strategy.py tests/test_component_write_strategy.py tests/test_inventory_item_write_strategy.py -q` → PASS
- [ ] **B.6.3 Commit Phase O-B**：
  ```bash
  git add app/oscal/service/ssp_docx_import_app_service.py tests/test_ssp_docx_import_app_service.py common/code/grc_error_code.py
  git commit -m "feat(ssp-oscal-alignment): Bug O Phase B — BE confirm 接 4 個 v3 decisions filter（含 overwrite caveat）

  - confirm_import 讀 4 個新 payload key (components_decisions / la_decisions / inventory_decisions / system_characteristic_decision)
  - _load_current_v3_lists 撈 current 4 key 餵給 annotate_parse_result
  - _validate_v3_decisions 防 FE 送假 row_uid
  - _apply_v3_decisions 把 decision 套到 parsed_result list（注意：3 strategy overwrite 語意 → keep_current 必須 inject current row 防 delete）
  - GrcErrorCode 加 4 個 invalid decision code

  Tests: 全 PASS
  Backward compat: 既有 FE caller 沒送 4 個新 key → 行為 = 全 use_docx（既有行為）

  ⚠️ 需重啟 BE 讓 fix 生效。

  Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
  ```

---

## Phase O-C：FE store 擴展（decisions schema + 4 sub-map）

**Files:**
- Modify: `~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/stores/sspDocxImportStore.js`（157 → ~280 行）
- Test: `~/Projects/Billows/Audit-Manager/compliance-manager-fe/tests/...` （FE 是否有 store unit test infra？if yes write，if no skip — 待 user 拍板）

### C.0 Pre-flight

- [ ] **C.0.1 Read FE CLAUDE.md** (`~/Projects/Billows/Audit-Manager/compliance-manager-fe/CLAUDE.md`) — 重點看 store / API / TabView quirk / Add 按鈕 style
- [ ] **C.0.2 grep FE store test infra**：`find ~/Projects/Billows/Audit-Manager/compliance-manager-fe -name "*store*.test*"` — 若有寫 test，無則 skip

### C.1 擴展 state

- [ ] **C.1.1 `decisions` 加 4 sub-map**：
  ```js
  decisions: {
    controls: {},
    parties: {},
    components: {},           // {row_uid: {action}}
    leveraged_authorizations: {},
    inventory_items: {},
    system_characteristic: { action: null },  // 單筆
  }
  ```
- [ ] **C.1.2 `filters` 加 4 個對應 key**

### C.2 擴展 getters

- [ ] **C.2.1 加 `componentsList` / `leveragedAuthorizationsList` / `inventoryItemsList` getters** — from `parseResult` 對應 key
- [ ] **C.2.2 加 `systemCharacteristicDiff` getter** — from `parseResult.system_characteristic_diff`
- [ ] **C.2.3 `hasDiff` getter 內部已用 `parseResult.has_diff` BE 端算，FE 不改**

### C.3 initDefaultDecisions

- [ ] **C.3.1 擴展 `initDefaultDecisions`** 對 4 個新 key build default：
  ```js
  for (const c of this.parseResult?.components || []) {
    decisions.components[c.row_uid] = { action: c.default_action ?? (c.diff_status === 'added' ? 'use_docx' : null) }
  }
  // 同 leveraged / inventory
  const scDiff = this.parseResult?.system_characteristic_diff
  if (scDiff) {
    decisions.system_characteristic = { action: scDiff.default_action ?? (scDiff.diff_status === 'added' ? 'use_docx' : null) }
  }
  ```
  added/changed/gone default 同 parties pattern（per `_defaultPartyAction`）

### C.4 setXxxDecision actions

- [ ] **C.4.1 加 4 setter**：`setComponentDecision(rowUid, action)` / `setLaDecision(rowUid, action)` / `setInventoryDecision(rowUid, action)` / `setScDecision(action)`

### C.5 bulkApply 擴展

- [ ] **C.5.1 改 `bulkApply(section, action)`** 支援 'components' / 'leveraged_authorizations' / 'inventory_items'

### C.6 buildConfirmPayload 擴展

- [ ] **C.6.1 改 `buildConfirmPayload`** 加 4 個 payload key：
  ```js
  return {
    decisions,
    parties_decisions,
    components_decisions: Object.entries(this.decisions.components).map(([row_uid, d]) => ({row_uid, action: _safeAction(d.action)})),
    leveraged_authorizations_decisions: ...,
    inventory_items_decisions: ...,
    system_characteristic_decision: _safeAction(this.decisions.system_characteristic.action),
    content_overrides: this.contentOverrides,
  }
  ```

### C.7 reset 擴展

- [ ] **C.7.1 改 `reset`** 對 4 個新 sub-map 都 reset 成 `{}` / `{action: null}`

### C.8 Commit Phase O-C

- [ ] **C.8.1 改 FE working tree status check**: `git -C ~/Projects/Billows/Audit-Manager/compliance-manager-fe status --short` → 只該檔 modified
- [ ] **C.8.2 Commit**：
  ```bash
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
  git add src/stores/sspDocxImportStore.js
  git commit -m "feat(ssp-oscal-alignment): Bug O Phase C — FE store 擴展 4 key decisions schema

  - decisions 加 components / leveraged_authorizations / inventory_items / system_characteristic 4 sub-map
  - initDefaultDecisions 加對應 4 key 預設 action 邏輯
  - setXxxDecision / bulkApply / reset 對 4 key 同步擴展
  - buildConfirmPayload 加 4 個新 payload key 送 BE

  Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
  ```

---

## Phase O-D：FE 4 個新 DiffSection + DiffCard 元件

**Files (新):**
- Create: `~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/components/grc/ssp-docx-import-v2/diff/SystemCharacteristicDiffSection.vue`
- Create: 同上 + `ComponentsDiffSection.vue` / `LeveragedAuthorizationsDiffSection.vue` / `InventoryItemsDiffSection.vue`
- Create: 同上 + `ComponentDiffCard.vue` / `LeveragedAuthorizationDiffCard.vue` / `InventoryItemDiffCard.vue`
- 不需要 `SystemCharacteristicDiffCard` — section 內單筆直接 inline render

### D.0 Pre-flight

- [ ] **D.0.1 Read `PartyDiffCard.vue` line 1~159** 完整 — pattern 抄底
- [ ] **D.0.2 Read `PartiesDiffSection.vue` line 1~107** 完整 — section pattern 抄底

### D.1 ComponentDiffCard.vue（mirror PartyDiffCard）

- [ ] **D.1.1 Create file** — copy PartyDiffCard 結構，改 `fields` array：
  ```js
  const fields = [
    { key: 'title', label: '名稱' },
    { key: 'component_type', label: '類型' },
    { key: 'status', label: '狀態' },
    { key: 'description', label: '描述' },
    { key: 'purpose', label: '目的' },
    { key: 'leveraged_authorization_ref', label: '關聯外部服務' },
    { key: 'protocol', label: '通訊協定' },
    { key: 'port_ranges', label: 'Port' },
    { key: 'security_auth', label: '驗證方式' },
  ]
  ```
- [ ] **D.1.2 displayName** = parsed.title || current.title
- [ ] **D.1.3 actionOptions** mirror PartyDiffCard（added: use_docx/skip; gone: keep_current/use_docx; default: keep_current/use_docx/skip）

### D.2 LeveragedAuthorizationDiffCard.vue

- [ ] **D.2.1 Create file**，fields:
  ```js
  const fields = [
    { key: 'title', label: '服務名稱' },
    { key: 'fedramp_package_id', label: 'FedRAMP Package ID' },
    { key: 'impact_level', label: 'Impact Level' },
    { key: 'data_types', label: 'Data Types' },
    { key: 'nature_of_agreement', label: '協議性質' },
    { key: 'authorized_users', label: '授權使用者' },
    { key: 'date_authorized', label: '授權日期' },
    { key: 'remarks', label: '備註' },
  ]
  ```

### D.3 InventoryItemDiffCard.vue

- [ ] **D.3.1 Create file**，fields:
  ```js
  const fields = [
    { key: 'description', label: '描述' },
    { key: 'asset_id', label: 'Asset ID' },
    { key: 'asset_tag', label: 'Asset Tag' },
    { key: 'ipv4_address', label: 'IPv4' },
    { key: 'mac_address', label: 'MAC' },
    { key: 'fqdn', label: 'FQDN' },
    { key: 'hostname', label: 'Hostname' },
    { key: 'software_name', label: '軟體名稱' },
    { key: 'os_name', label: 'OS' },
    { key: 'implemented_component_refs', label: '關聯元件', isList: true },
  ]
  ```
- [ ] **D.3.2 formatValue 對 implemented_component_refs**：list of title → join `", "` 顯示

### D.4 3 個 DiffSection（mirror PartiesDiffSection）

- [ ] **D.4.1 Create `ComponentsDiffSection.vue`** — mirror PartiesDiffSection，把 `store.partiesList` 換 `store.componentsList`、`PartyDiffCard` 換 `ComponentDiffCard`、`store.filters.parties` 換 `store.filters.components`、`store.diffSummary.parties` 換 `store.diffSummary.components`、`store.bulkApply('parties', ...)` 換 `store.bulkApply('components', ...)`、`store.setPartyDecision` 換 `store.setComponentDecision`
- [ ] **D.4.2 Create `LeveragedAuthorizationsDiffSection.vue`** — 同 pattern
- [ ] **D.4.3 Create `InventoryItemsDiffSection.vue`** — 同 pattern

### D.5 SystemCharacteristicDiffSection.vue（單筆，不複用 DiffSectionShell）

- [ ] **D.5.1 Create file** — 結構：
  ```html
  <template>
    <DiffSectionShell title="受評標的 (System Characteristic)" :badge="badgeText" badge-severity="warning" :collapsed="collapsed" :filter="'all'" @toggle-collapsed="collapsed=!collapsed" @bulk-apply="onBulkApply">
      <div v-if="scDiff && scDiff.diff_status !== 'unchanged'" class="sc-card">
        <!-- 2-col current/parsed 內 inline render 8 fields (system_name / sensitivity / status / target_type / scope_desc / system_identifier / description) -->
        <!-- SelectButton bind store.decisions.system_characteristic.action -->
      </div>
      <div v-else class="empty-state">
        <i class="pi pi-check-circle empty-icon" />
        <p>受評標的無差異</p>
      </div>
    </DiffSectionShell>
  </template>
  ```
- [ ] **D.5.2 onBulkApply** for SC 是 `store.setScDecision(action)` 不是 list

### D.6 Commit Phase O-D

- [ ] **D.6.1 Commit**：
  ```bash
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
  git add src/components/grc/ssp-docx-import-v2/diff/
  git commit -m "feat(ssp-oscal-alignment): Bug O Phase D — FE 4 個新 DiffSection + DiffCard 元件

  - SystemCharacteristicDiffSection (單筆) / ComponentsDiffSection / LeveragedAuthorizationsDiffSection / InventoryItemsDiffSection
  - 3 個 DiffCard mirror PartyDiffCard pattern (2-col current/parsed + SelectButton 三 action)
  - SC section 不用 DiffCard，section 內 inline render 單筆

  Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
  ```

---

## Phase O-E：DiffResolutionStep TabView refactor

**Files:**
- Modify: `~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/components/grc/ssp-docx-import-v2/diff/DiffResolutionStep.vue`

### E.0 Pre-flight

- [ ] **E.0.1 Read FE CLAUDE.md 對 TabView 的 quirk**（per Caveat 5）— `:active-index` not `:active-step`
- [ ] **E.0.2 grep FE 既有 TabView 用法**: `grep -rn "TabView\|TabPanel" ~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/components/grc/ssp/ | head -5` — 抄一份既有 sample

### E.1 改寫 template

- [ ] **E.1.1 把既有 `<ControlDiffSection /> <PartiesDiffSection />` 換成 `<TabView :active-index="..." @update:active-index="...">`** 內含 6 TabPanel（Controls / Parties / SC / Components / LA / Inventory）
- [ ] **E.1.2 每個 TabPanel header 加 badge** — 顯示 `${changed+added+gone} 處衝突` (per `summary.total_with_diff`)；無衝突 tab 隱藏（v-if）或 disable
- [ ] **E.1.3 改既有 summary-pills** 為 7-pill（加 4 個新 key）— 或乾脆移除（tab badge 已表達）

### E.2 active tab logic

- [ ] **E.2.1 預設 active tab = 第一個有 diff 的 tab**（避免 user 進 step 看到「無衝突」tab 困惑）
- [ ] **E.2.2 加 `activeIndex` ref + onMounted 算邏輯**

### E.3 onNext + allSkip check 擴展

- [ ] **E.3.1 `_isAllSkip()` 現在只算 controls + parties，要擴展到 7 key 都算**
- [ ] **E.3.2 SC 單筆獨立判斷** — action === 'skip' 視為 skip
- [ ] **E.3.3 跳過確認的 message 也更新**

### E.4 Visual smoke test（user 手動）

- [ ] **E.4.1 Run dev server**: `cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe && npm run dev`
- [ ] **E.4.2 User 對既有 mf 跑重 import → 點到 Step 2 (DiffResolutionStep)** → 看 6 tab 都 render，無 diff 的 tab 隱藏/disable
- [ ] **E.4.3 點各 tab 切換** → 對應 section render

### E.5 Commit Phase O-E

- [ ] **E.5.1 Commit**：
  ```bash
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
  git add src/components/grc/ssp-docx-import-v2/diff/DiffResolutionStep.vue
  git commit -m "feat(ssp-oscal-alignment): Bug O Phase E — DiffResolutionStep TabView refactor

  - 把既有 2 section inline render 改成 6-tab TabView 容器
  - 每 tab header 顯示對應 changed/added/gone count badge
  - 無衝突 tab 隱藏（避免 user 困惑）
  - active tab default = 第一個有 diff 的 tab
  - allSkip check 擴展到 7 key 都算

  Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
  ```

---

## Phase O-F：i18n + 收尾

### F.1 i18n

- [ ] **F.1.1 grep 既有 ssp_docx_import i18n keys**：`grep -rn "ssp.docx.import\|ssp_docx_import" ~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/locales/ | head -20`
- [ ] **F.1.2 加 4 個新 tab 標題 + 4 個新 section subtitle 到 zh-TW + en**（per FE CLAUDE.md i18n 規範）
- [ ] **F.1.3 Diff card 內 field label 直接寫中文 hardcoded（per 既有 PartyDiffCard pattern）** — 不放 i18n（一致性）

### F.2 design.md §11.34 收尾

- [ ] **F.2.1 改 §11.34 標題拿掉 `⏸ DEFERRED`**
- [ ] **F.2.2 補「Bug O 教訓」段** 寫實際完工後的 1~3 條教訓（candidate：overwrite vs incremental 寫入語意 / TabView vs stack 視覺取捨 / dataclass schema 跟 entity column 對齊的成本）
- [ ] **F.2.3 補 `Commit` 段** 列各 phase 的 commit hash
- [ ] **F.2.4 改 design.md §11 索引 line 940** 把 §11.34 註解從 `⏸ DEFERRED` 改 closed 狀態

### F.3 寫 changelog + FIXED-SUMMARY

- [ ] **F.3.1 寫 `docs/changelog/2026-05-25-feat-bug-o-diff-stepper-4key-tabview.md`** — type=feat，列改動範圍 / 行為差異 / 測試結果
- [ ] **F.3.2 寫 `docs/features/FR-028-2605-ssp-oscal-alignment/handoff/2026-05-25-bug-o-FIXED-SUMMARY.md`** mirror `2026-05-25-bug-h-FIXED-SUMMARY.md` 樣式 — 短收尾報告（commits / 改動範圍 / 行為差異 / 已知 follow-up）

### F.4 跨 repo E2E verify（user 手動）

- [ ] **F.4.1 Restart BE**（per Caveat 6）
- [ ] **F.4.2 User 對既有 mf 跑 docx import → 走完整 wizard**：
  - Step 0 upload / Step 1 預覽 → Step 2 解決差異（看到 6 tab + badge）
  - 對 SC / 1 component / 1 LA / 1 inventory 各設一個 keep_current
  - 對 1 component 設 use_docx
  - 對 1 inventory 設 skip（added）
  - 進 Step 3 確認
- [ ] **F.4.3 confirm 完 verify DB**：
  ```sql
  -- keep_current 的 row 應該 = current value，不被 docx 改
  -- use_docx 的 row 應該 = parsed value
  -- skip(added) 的 row 應該 不存在 DB
  ```
- [ ] **F.4.4 §6.1 重 verify 4 表 org_unit_id all NULL**（H-6 fix 仍持續）

### F.5 commit + 等 user push

- [ ] **F.5.1 commit Phase O-F**：
  ```bash
  git add docs/features/FR-028-2605-ssp-oscal-alignment/design.md docs/changelog/2026-05-25-feat-bug-o-diff-stepper-4key-tabview.md docs/features/FR-028-2605-ssp-oscal-alignment/handoff/2026-05-25-bug-o-FIXED-SUMMARY.md
  # FE i18n 在 FE repo commit
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
  git add src/locales/
  git commit -m "feat(ssp-oscal-alignment): Bug O Phase F — i18n 4 個新 tab 標題"
  ```
- [ ] **F.5.2 整體推 push 由 user 拍板** — BE 4 commits + FE 3 commits

---

## Acceptance criteria

- [ ] BE pytest smoke 全 PASS（含本期新 test ~25 cases）
- [ ] BE diff_summary response 含 7 key counts（不只 3）
- [ ] User 對既有 mf 重 import 進 DiffResolutionStep 看到 6 tab + 對應 badge
- [ ] 每 tab 內 per-row decision UI 可操作（SelectButton 三 action）
- [ ] confirm 後 DB 行為符合 decision（keep_current 保留 / use_docx 寫入 / skip(added) 不寫 / skip(changed) = keep_current）
- [ ] H-6 invariant 仍持續（4 表 org_unit_id all NULL）
- [ ] design.md §11.34 從 `⏸ DEFERRED` 改 closed + 補教訓 + 補 commit

## 不在 scope

- preview UI（SystemCharacteristicSection / LeveragedSection 等）的 inline edit 路徑（已 Bug L-2 + M-B 解，跟 diff stepper 不同 phase）
- Excel import diff path（未來再對齊）
- `template_module_frame_id` entity field wire（Bug K1 follow-up）
- jedi-* 進版 + BE/FE 版號對齊（user 拍板）
- party 的 row_uid stable hash 改進（既有用 sha1(name|email) 已足）

---

## Pre-flight check 開工前一次性跑

- [ ] **PF.1** BE branch + working tree clean: `git -C BE branch --show-current && git -C BE status --short` → `feature/ssp-oscal-alignment` + 只剩 `M pyproject.toml`
- [ ] **PF.2** FE branch + working tree clean
- [ ] **PF.3** BE pytest smoke green: `poetry run pytest tests/test_ssp_docx_import_app_service.py tests/test_ssp_docx_diff_service.py tests/test_ssp_write_strategy.py tests/test_component_write_strategy.py tests/test_inventory_item_write_strategy.py -q` → all PASS
- [ ] **PF.4** BE listener: `lsof -t -i:8000` 有值
- [ ] **PF.5** §6.1 H-6 verify: 4 表 org_unit_id all bad=0
- [ ] **PF.6** Confirm latest parse job (id=162) v3 keys 都有: `psql ... SELECT jsonb_object_keys(parsed_result) ...`

預估時間：**8-12 小時**（含 test + commit + E2E verify）
