# Track B → A → C — SSP Excel round-trip + MF / SSP 結構對齊 — 交接 prompt

> **目的**：讓專案 SSP 支援 Excel 完整匯出匯入 round-trip + MF template-edit 跟 SSP 結構對齊
> **接手者**：新 session 從這份 handoff 直接開工，毋需重看舊對話
> **建立日期**：2026-05-23（前一 session 收尾時寫）
> **branch**：`feature/ssp-import-export-phase3`（沿用，**不要切**）
> **使用者**：raymond

---

## 0. 第一句 prompt 建議（給接手 session 用）

```
接手 docs/features/FR-011.3-2605-ssp-edit-in-project/handoff/2026-05-23-track-bac-excel-roundtrip.md
這份 handoff，照 Track B → A → C 順序開工。Track B 先動。
```

---

## 1. User 真正目標

「**從專案那邊做匯出匯入 SSP**，讓 user 可以用 Excel 編輯 SSP 的部分」。

實現路徑拆 3 track（**B → A → C**）：

| Track | 主題 | 工作量 | 為何這個順序 |
|-------|------|-------|-------------|
| **B** | Excel 鏈條補完（system_characteristic 全新 + leveraged 結構化端到端） | BE 大 | 地基層，沒有 Excel 端到端不通就無法 ship C |
| **A** | MF template-edit 跟 SSP 結構對齊（補 受評標的 + 外部利用服務 兩 tab） | BE 中 + FE 中 | 對齊 + 提供 admin 預設「樣板值」入口 |
| **C** | 專案 SSP tab 加匯出匯入按鈕 | FE 小 | 把 B 的能力 expose 給 end user |

---

## 2. 已拍板的設計決策（**不要再問**）

| 議題 | 決策 |
|------|------|
| system_characteristic 放 MF 的語意 | **「樣板預設值」**（mirror 既有 devices / info_systems 池 pattern）；新建 SSP 啟動時 clone 一份過來，user 進專案後再依實際填寫；MF 那邊存的不是「實際系統描述」，是「建議的預設結構」|
| MF / SSP tab 結構對齊目標 | 兩邊都 7 tab：受評標的 / 責任單位 / 責任人員 / 設備 / 資訊系統 / 外部利用服務 / (MF: 適用控制項 + 程序書管理 / SSP: 不重複出現) |
| Excel 樣板加 `01b_受評標的` 新 sheet | **獨立 sheet**，不塞進 metadata（避免跟 MF 框架 metadata 混淆語意）|
| MF leveraged BE endpoint | mirror SSP `/ssp/<uid>/leveraged` pattern → 新增 `/module-frame/<uid>/leveraged`；內部 thin wrapper 呼 `SspLeveragedContextService`（不重寫 CRUD 邏輯）|
| MF system_characteristic BE endpoint | mirror SSP `/ssp/<uid>/system-characteristic` pattern → 新增 `/module-frame/<uid>/system-characteristic` |
| FE Section component | 對齊 Phase B 4 個 panel pattern — `SspBasicSection` + `SspLeveragedSection` 加 `apiBase: 'module-frame' \| 'ssp'` + `scopeUid` props |
| `_setup_ssp_system_implementation` | 擴充 — 既有只 clone main row + items，要再加 clone system_characteristic |

---

## 3. Track B 詳細 scope

### B1 — Excel sheet 定義擴充

**檔案**：`app/module_frame/excel_template/sheet_definitions.py`

- **新 sheet `SHEET_SYSTEM_CHARACTERISTIC`**（sheet_name="01b_受評標的"）
  - 欄位：`system_name` (required) / `security_sensitivity_level` (enum: low/moderate/high) / `status` (enum: under-development/operational/under-major-modification/disposition/other) / `target_type` / `scope_description` / `authorization_boundary` / `owner_login_name` (lookup_source=USERS — 拿 login_name，BE 端對 matched_user_id)
- **`SHEET_LEVERAGED` 補 column**：加 `status` ColumnDef（enum: operational/under-development/under-major-modification）
- **`ALL_SHEETS`** 在 `SHEET_METADATA` 後插入 `SHEET_SYSTEM_CHARACTERISTIC`
- 樣板版本 v2.1.0 → **v2.2.0**（加 sheet 屬 MINOR bump, 對齊 design.md §14 SemVer）

### B2 — Parser + dataclass

**新 dataclass**：`domain/oscal/parser/ssp_intermediate.py`

```python
@dataclass
class ParsedSystemCharacteristic:
    system_name: str  # required
    security_sensitivity_level: Optional[str] = None  # low/moderate/high
    status: Optional[str] = None
    target_type: Optional[str] = None
    scope_description: Optional[str] = None
    authorization_boundary: Optional[str] = None
    owner_login_name: Optional[str] = None  # parser 拿到的 login_name
    matched_user_id: Optional[int] = None  # reconciler 配對後的 user_id
    match_method: MatchMethod = MatchMethod.UNMATCHED
    match_confidence: float = 0.0
```

**`ParsedLeveraged` 補欄位**：加 `category: Optional[str]` + `status: Optional[str]`

**新增**：`ParsedExcelEntityBundle` 加 `parsed_system_characteristic: Optional[ParsedSystemCharacteristic] = None`（per-SSP 只有一筆，不是 list）

**Parser sheet handler**：`app/oscal/service/excel_parser/sheet_handlers.py`
- 新 `parse_system_characteristic_sheet(ws, errors) -> Optional[dict]`（單筆，不是 list）
- 加進 `SHEET_NAMES` dict

**Parser 主幹**：`app/oscal/service/excel_parser/parser.py:67` 區段加
```python
system_characteristic=sh.parse_system_characteristic_sheet(
    sh.get_ws(wb, sh.SHEET_NAMES["system_characteristic"]), validation_errors,
),
```

`ParsedResult` (types.py) 加 `system_characteristic: Optional[dict] = None`

### B3 — Reconciler

**新檔**：`domain/oscal/service/reconciliation/system_characteristic_reconciler.py`

`SystemCharacteristicReconciler` — 從 `owner_login_name` 配對 `User.login_name` → 填 `matched_user_id`。

Mirror 既有 `leveraged_reconciler.py` 結構（最簡 reconciler，只解一條 user lookup）。

### B4 — Write strategy

**新檔**：`domain/oscal/service/write_strategy/system_characteristic_write_strategy.py`

`SystemCharacteristicWriteStrategy` — 寫 `oscal.system_security_plan_characteristic`（per-SSP 單筆 upsert）。

注意：跟其他 write strategy 不同（其他都是 list item 寫到 `ssp_system_implementation_items`），這個是寫 **per-SSP 單一 row** 到 SC 表。

實作 pattern：先 `get_by_ssp_id(ssp_id)`，存在 → update，不存在 → add。entity 用 `SspSystemCharacteristicEntity` (jedi-oscal)。

**LeveragedWriteStrategy 修正**：`domain/oscal/service/write_strategy/leveraged_write_strategy.py:21`
```python
# 修法（B4.2）
def _parsed_to_item_payload(self, parsed: ParsedLeveraged) -> dict:
    return {
        "name": parsed.service_name,
        "provider": parsed.provider,  # ← 改寫到 provider 欄位（之前錯寫到 title）
        "category": parsed.category,  # ← 新增
        "status": parsed.status,      # ← 新增
        "purpose": parsed.purpose,
        "party_uuid": parsed.matched_party_uuid,
        "date_authorized": parsed.date_authorized,
    }
```

**`_dict_to_parsed_leveraged`**：`app/oscal/service/ssp_excel_import_app_service.py:1935` 接 category + status

### B5 — 樣板下載填值

**檔案**：`app/module_frame/service/ssp_import_template_app_service.py`

- 新 method `_build_system_characteristic(mf) -> Optional[dict]`：從 MF 對應 template SSP 拉 `oscal.system_security_plan_characteristic` 一筆 → 組 dict（含 owner_login_name from `User.get_by_id(owner_user_id).login_name`）
- 修 `_build_leveraged(items)`：加 category + status + provider（**改從 `item.provider` 取，不是 `party.name`** — entity 已有獨立 provider 欄位）
- 主 builder 加 `bundle.system_characteristic = self._build_system_characteristic(mf)`

### B6 — Import write 流程整合

**檔案**：`app/oscal/service/ssp_excel_import_app_service.py`

- DI 加 `system_characteristic_write_strategy`
- `_dict_to_parsed_system_characteristic(d: dict) -> Optional[ParsedSystemCharacteristic]`
- bundle 帶入 `parsed_system_characteristic`
- reconciler 跑 user lookup
- write strategy 寫
- response 加 `system_characteristic_written: bool`

### B7 — Excel preview / FE 預覽頁

**FE 檔案**：`src/views/module_frame/ImportExcelPreviewPage.vue` + 新 `src/components/grc/ssp-excel-import/SheetPreviewSystemCharacteristic.vue`

- 加 system_characteristic preview tab（單筆顯示，跟其他多筆 sheet preview 不同）
- 加 leveraged 預覽 category/status 欄位顯示

### B8 — Test

- `tests/test_system_characteristic_write_strategy.py`
- `tests/test_leveraged_write_strategy_provider_category.py`（補 category + status + provider 對 entity 的映射）
- `tests/test_ssp_excel_import_system_characteristic_e2e.py`（end-to-end with mock）

### B9 — Changelog（Track B ship 後）

`docs/changelog/YYYY-MM-DD-feat-ssp-excel-system-characteristic-and-leveraged-structured.md`

---

## 4. Track A 詳細 scope

### A1 — BE MF system-characteristic 端到端

**檔案**：
- 新 `app/module_frame/service/module_frame_system_characteristic_service.py` — mirror `module_frame_ssp_resources_service.py` pattern（mf_uid → resolve ssp_id → 呼 SspSystemCharacteristicContextService 的方法）
- 新 `api/module_frame/routes/module_frame_system_characteristic_route.py` — GET / PUT endpoints
- `api/module_frame/__init__.py` 加 add_resource `/module-frame/<uid>/system-characteristic`
- `di_containers/module_frame/module_frame_container.py` 加 provider

> **注意**：`SspSystemCharacteristicContextService` 可能尚未抽出 (need to check)。若沒有就先抽（mirror SspResourcesContextService 抽法）。

### A2 — BE MF leveraged 端到端

**檔案**：
- 新 `app/module_frame/service/module_frame_leveraged_service.py` — mirror MF resources pattern
- 新 `api/module_frame/routes/module_frame_leveraged_route.py` — GET / POST / PUT / DELETE
- `api/module_frame/__init__.py` 加 `/module-frame/<uid>/leveraged`
- DI 加 provider
- **走 `ensure_sys_impl_main_id`**（前一輪做的 auto-create, 4595dae）— MF 對應 SSP 不一定有 main row

### A3 — FE SspBasicSection generalize

**檔案**：`src/components/grc/ssp/SspBasicSection.vue`

加 props（mirror Phase B 4 個 panel）：
```js
apiBase: { type: String, default: 'ssp', validator: v => ['module-frame', 'ssp'].includes(v) }
scopeUid: { type: String, required: true }  // 取代既有 sspUid
```

`apiRoot` computed → `${base}/${scopeUid}/system-characteristic`

### A4 — FE SspLeveragedSection generalize

**檔案**：`src/components/grc/ssp/SspLeveragedSection.vue`

同 A3 加 apiBase + scopeUid props，endpoint 走 `apiRoot/leveraged/...`

### A5 — MF template-edit 加 2 個 tab

**檔案**：`src/views/module_frame/ModuleFrameTemplateEditView.vue`

加 2 個 TabPanel：
- `tab_audit_subject`（受評標的）→ mount `<SspBasicSection api-base="module-frame" :scope-uid="moduleFrameUid" />`
- `tab_leveraged`（外部利用服務）→ mount `<SspLeveragedSection api-base="module-frame" :scope-uid="moduleFrameUid" :on-count="(n) => leveragedCount = n" />`

放位置：放在「資訊系統」後、「程序書管理」前（mirror SSP tab 順序）

### A6 — i18n

**檔案**：
- `src/config/locales/i18n/zh-tw/module-frame.json` + en/
- 加 `tab_audit_subject` / `tab_leveraged` (含 count)
- ssp-edit.json 已有的 i18n key 不變（複用）

### A7 — 啟動專案 clone system_characteristic

**檔案**：`app/project/service/oscal_project_service.py:533 _setup_ssp_system_implementation`

擴充加 system_characteristic clone：
```python
# Step 1.5 — clone system_characteristic from template SSP
template_sc = self._sc_domain_service.get_by_ssp_id(template_ssp.id)
if template_sc:
    new_sc = SspSystemCharacteristicEntity(
        ssp_id=new_ssp_id,
        name=template_sc.name,
        # ... 所有欄位 pass-through
    )
    self._sc_domain_service.add(new_sc)
```

DI 補注 `system_characteristic_domain_service`。

### A8 — Changelog（Track A ship 後）

`docs/changelog/YYYY-MM-DD-feat-mf-template-edit-add-audit-subject-and-leveraged-tabs.md`

### A9 — Spec sync

- design-C5.md §10 加段落：「對齊 MF template-edit」
- design-C7.md §10 加段落：「MF endpoint + UI tab」
- README.md tracker 加 row：「Track A: MF / SSP 結構對齊」

---

## 5. Track C 詳細 scope

### C1 — 專案 SSP tab 加匯出匯入按鈕

**檔案**：`src/components/grc/ssp/SspTabPanel.vue`

在 SSP tab 頂部加兩個按鈕（manager only）：
- 「下載 SSP Excel」→ 呼 `GET /ssp/<uid>/excel-template`（既有 C2.2 endpoint, **待確認**）
- 「匯入 SSP Excel」→ open import dialog → 走既有 `SspExcelImportAppService` flow

> **注意**：先確認 `/ssp/<uid>/excel-template` 跟 `/ssp/<uid>/excel-import` 是否已在 C2.2 ship。沒 ship 要先補 BE endpoint。

### C2 — Import dialog 復用

`src/views/module_frame/ImportExcelPreviewPage.vue` 抽 ImportExcelDialog 元件給 SSP tab 用，或新建一個 SspImportExcelDialog wrap 既有預覽元件。

### C3 — Changelog + Spec sync

- changelog: `YYYY-MM-DD-feat-project-ssp-excel-roundtrip-entry.md`
- design-C5.md §10 加 Track C 段落

---

## 6. 既有 task arc 錨點（接手前先了解）

### 既有 commits range（不需動，僅供參考）

| Commit | 主題 |
|--------|------|
| `0960070` | C3.PR2 DROP table |
| `143f354` | C3.PR2 拆 Python stack |
| `4595dae` | auto-create system_implementation main row（**Track A2 會用到 ensure_sys_impl_main_id**） |
| `4fe8595` (FE) | MF parties picker cache 統一 |
| `d9a4128` | C3.PR2 + auto-create changelog + tracker |
| `ebd4f72` | spec sync (C3/C5/C7/plan-C3/api-spec) |
| `8d80972` (FE) | Phase C/D batch changelog |
| `f1c60a0` (FE) | Phase E cleanup |
| `18a1110` | leveraged 結構化欄位 BE 端到端（**provider/category 既有實作**，但 Excel chain 沒接，這次補）|
| `8de7991` (jedi-oscal) | leveraged entity 加 provider + category（**不要再動套件**）|

### 已 sync 的 spec / plan

- `docs/features/FR-011.3-2605-ssp-edit-in-project/design-C3.md` §11 — C3.PR2 已 ship 紀錄
- `docs/features/FR-011.3-2605-ssp-edit-in-project/design-C5.md` §10 — Phase D Reconciliation 全紀錄
- `docs/features/FR-011.3-2605-ssp-edit-in-project/design-C7.md` §10 — leveraged 結構化 Phase C Reconciliation
- `docs/features/FR-011.3-2605-ssp-edit-in-project/implementation-plan-C3.md` T11 — PR2 plan 矯正紀錄
- `docs/api/project/api-spec.md` §2.2 — project-device endpoint 撤除紀錄

### SUMMARY 路徑

`docs/features/FR-011.3-2605-ssp-edit-in-project/handoff/2026-05-23-phase-c-d-SUMMARY.md`

### Tracker

`docs/features/FR-011.3-2605-ssp-edit-in-project/README.md` — 含 Phase C 全 8 子題 + Phase D / B-generalize / Settings / Issue 1 BE / Bug fix / E cleanup / Auto-create / MF picker cache 共 14 row

---

## 7. 規範速查（**接手 session 必看**）

### 操作規範
| 規範 | 說明 |
|------|------|
| **禁止切 branch** | `git checkout/switch` **一律不執行**。current branch = `feature/ssp-import-export-phase3`，發現不對停下問 user，不自己 fix |
| **commit 可自做** | 階段性 commit 直接做不問；**push 永遠等 user 指示** |
| **不可寫憑證進版控** | 密碼 / token / secret 絕對不寫進任何 commit 檔案。連線資訊只寫 host / port / db / 帳號，密碼一律「請查 .env」 |
| **改 jedi-* 套件先走 path dep** | dev 階段把 `pyproject.toml` 的套件改 path 形式；不每改必 bump version；feature 完成 + user 指示才正式發版推 Nexus |
| **`pyproject.toml` 不要 commit dev 改動** | 目前 `pyproject.toml` working tree 有 M（jedi-oscal path dep），等 jedi-oscal 正式發版才一起還原 |

### 開發規範
| 規範 | 說明 |
|------|------|
| **changelog 收尾才 batch 寫** | 不要每個 commit 都停下寫；commit message 寫詳細即可；user 說「收尾」/「總結」/「告一段落」/「summary」/ phase 自然結束才一次性寫 |
| **subagent 平行修改也要 changelog** | 即使透過 subagent 批次修改，每個獨立主題都要有對應 changelog |
| **變更後主動 sync spec** | feature 完工要回頭 update design-*.md 的 Reconciliation 段（mirror design-C5/C7 §10 pattern）|
| **DDD 嚴格分層** | api 不直接 import ORM model 或 get_session；app service `@transaction` 開 scope；repo `session` lazy property |
| **`@transaction` 必加** | 每個 app service public method 都要 `@transaction`；helper（caller 已在 scope）不重複加但 docstring 標示「caller 必須在 @transaction scope 內」 |
| **Error code 用 GrcErrorCode + 標準 exception** | 禁 `raise ValueError`；新 code 命名 `GRC_<HTTP><序號>` |
| **可翻譯欄位 update 必傳 locale** | HTTP 用 `str(get_locale())`；background job 用 `getattr(user_context, "locale", None) or "zh_Hant_TW"` |
| **subagent dispatch prompt 必加 git add 顯式檔名** | 禁用 `git add -am`（會 sweep 不相關修改） |

### 環境參考
| 項目 | 值 |
|------|----|
| Dev DB | `192.168.50.188:25432 / guidant_ai_dev`（host + port + db 名一組記住，**不是 localhost**, 漏 host 或 -p 25432 會 connection refused 像 DB 當機） |
| Migration 跑誰 | **cmmgr 帳號**（密碼 `jedi@123!`）— `cm_app` 受 RLS 擋 |
| BE log 位置 | `log/app.log`（專案根目錄，不要靠 lsof 找 stdout）|
| Dev 登入帳號 | `blsadmin / Billows@123!`（manual）或 `blsit / Billows@123!`（pytest）|
| BE 改 service 後 | 必提醒 user 重啟 BE（沒 hot reload）|

---

## 8. 工作流提案

### Track B 開工順序
1. **B1** Excel sheet 定義（含 SHEET_SYSTEM_CHARACTERISTIC + SHEET_LEVERAGED status column + ALL_SHEETS 加入 + 樣板 v2.2.0 bump）
2. **B2** parser + dataclass（ParsedSystemCharacteristic + ParsedLeveraged.category/status + parser sheet handler）
3. **B3** reconciler（user lookup）
4. **B4** write strategy（new + 修 leveraged）
5. **B5** 樣板下載填值（_build_system_characteristic + 修 _build_leveraged）
6. **B6** import 流程整合（DI + dict mapper + bundle）
7. **B7** FE preview 頁
8. **B8** tests
9. **B9** ship + changelog

### Track A 開工順序
1. **A1** BE MF system-characteristic service + route + DI
2. **A2** BE MF leveraged service + route + DI
3. **A7** `_setup_ssp_system_implementation` 擴充 clone SC（提前做給後續測試用）
4. **A3** + **A4** FE generalize（SspBasicSection + SspLeveragedSection apiBase props）
5. **A5** MF view 加 2 tab + count emit
6. **A6** i18n
7. **A8** ship + changelog
8. **A9** spec sync

### Track C 開工順序
1. **C1** + **C2** FE 入口按鈕 + dialog
2. **C3** changelog + spec sync

---

## 9. 已知潛在地雷（接手前先看）

1. **SspSystemCharacteristicContextService 可能尚未抽出**（C2.1 抽 SspResourcesContextService 時順手抽了 leveraged + resources 兩個，SC 沒抽）— Track A1 第一步 grep 確認，沒抽就先抽 mirror SspResourcesContextService 的 pattern
2. **`_setup_ssp_system_implementation` 擴充 SC clone 後**，要重跑 dev 既有測試 SSP 啟動流程，確認 main row + items + SC 都 clone 過去（dev DB 連 `192.168.50.188:25432 / guidant_ai_dev`）
3. **Track C 需確認 `/ssp/<uid>/excel-template` + `/ssp/<uid>/excel-import` endpoint 是否在 C2.2 已 ship** — 如果沒 ship 要先補 BE endpoint
4. **leveraged_write_strategy 修 provider 映射時**，要同時清掉「title=provider」的舊 hack（既有 SSP UI CRUD `SspLeveragedContextService` 已用獨立 provider 欄位，但 write strategy 還是舊路徑）— 兩邊要對齊
5. **樣板版本 v2.2.0 bump 後**，舊 Excel template (v2.1.0 / v2.0.0) 匯入時 parser 對缺 sheet 要容錯（mirror `SHEET_MISSING` non-blocking 處理 — 看 `parse_leveraged_sheet` 範例）

---

## 10. 收尾流程

**Track B / A / C 各 ship 後**：
1. Commit 用對應 type（feat / fix / tweak）依決策樹判斷
2. 等 user 說「收尾」/「summary」才 batch 寫 changelog
3. spec sync（design-C5/C7 加 Reconciliation 段）
4. tracker README.md 加 row
5. **三 track 全 ship 後**寫 final SUMMARY 到 `docs/features/FR-011.3-2605-ssp-edit-in-project/handoff/YYYY-MM-DD-track-bac-SUMMARY.md`

**push 永遠等 user 指示**，handoff 跑完不自動 push。
