# Phase A0.1 Design — SSP system-implementation 結構重整（補丁）

> **Phase**：A0.1（A0 之後補丁，**非新 phase 而是 A0 的結構校正**）
> **級別**：重型（schema rename + 新建表 + 鉤稽路徑改動）
> **狀態**：design draft，等執行 session
> **前置**：A0 已 ship（commits c4804d7 / a1b80cc / e73b1aa / 60a4a2e / 5d8626e）
> **依賴本 phase**：A1, A2, A3, A4, A5（Track A 全部）+ B2, B5（Track B 內容組裝與 OSCAL 匯出）

---

## 1. 為什麼要做 A0.1（補丁的動機）

A0 完成時用「擴充既有表」方案處理 OSCAL system-implementation 鏡像。事後盤點發現幾個結構問題：

1. **既有表名稱誤導**：`system_security_plan_system_implementations` 看似 main 表但實際是 items 集合（multi-row, implementation_type 區分）
2. **缺 OSCAL `system-implementation` block 層**：OSCAL spec 上是 1:1 block per SSP，DB 沒對應 entity
3. **缺 OSCAL `inventory-item.implemented-components[]` 關聯**：M:N 對應沒實作
4. **鉤稽路徑重疊**：A0 加的 `information_system_id` soft FK 跟既有 `system_characteristics` 中介機制重複
5. **table 命名不一致**：oscal schema 內既有 `ssp_*` 前綴慣例（ssp_docx_parse_jobs / ssp_reference_documents），但新加的還是用舊式 `system_security_plan_*` 前綴

A0.1 是這些問題的補丁，**設計校正而非新功能**。

---

## 2. 設計決策摘要（v5 校正）

| # | 決策點 | 結論 | 理由 |
|---|--------|------|------|
| 1 | items 表是不是 main？ | **是 items**（OSCAL spec 上每 row 對應一個 inventory-item / component / leveraged-auth，各自有 UUID）| OSCAL JSON 每個 item 有 UUID，block 本身沒 UUID |
| 2 | OSCAL system-implementation 是 entity 嗎？ | **不是 entity**（無 UUID, logical block）| 但 GuidantAI DB 規範要 id+uid，仍建 main 表，OSCAL 匯出時 mapper 排除 uid |
| 3 | 既有表 repurpose 當 main 還是當 items？ | **保留當 items**（Path A）| 既有 140 筆已是 items 用法，重 purpose 工程量大且破壞 A0 commits |
| 4 | items 命名 | `ssp_system_implementation_items` | 對齊 ssp_* 前綴 + `_items` 後綴明確語意 |
| 5 | main 命名 | `ssp_system_implementations` | OSCAL `system-implementation` 對應，複數對齊 SQL convention |
| 6 | join 表命名 | `ssp_inventory_item_components` | 對應 OSCAL `inventory-item.implemented-components` |
| 7 | `information_system_id` 鉤稽路徑 | 改鉤 SSP-scoped `system_characteristic_id` | 避免 tenant 直連雙路徑，走既有 system_characteristics 中介 |
| 8 | OSCAL `system-implementation.users[]` 表 | **不做**（GuidantAI 客戶不需要這層） | 客戶要列「參與人員」用既有 oscal_parties + responsible_parties polymorphic 已 cover |
| 9 | `system_characteristics` 改 1:1？ | **不動，保留 1:N** | 既有設計支援多 in-scope systems 場景，改 1:1 會破壞 project_system_characteristic_mapping 既有 use case |
| 10 | `hardware` 改 `inventory-item` enum？ | **不改，沿用 hardware** | 既有 140 筆 + caller pattern 已熟，OSCAL 匯出 mapper 翻譯即可 |

---

## 3. Schema 變更詳細規格

### 3.1 既有表 rename: `system_security_plan_system_implementations` → `ssp_system_implementation_items`

**動作**：
- Table RENAME
- Sequence RENAME（PG 不會自動跟）
- PK / UNIQUE / FK constraint RENAME
- 6 個 index RENAME（cosmetic，統一 ix_ssp_si_items_* 前綴）

**既有 column 不動**（含 A0 加的 9 個）：
```
id, uid, system_security_plan_id (nullable),
name, description, implementation_type, responsible_party,
created_at/updated_at/created_user/updated_user,
scope_type (NOT NULL), scope_id (NOT NULL),
device_id, information_system_id (即將 rename),
title, purpose, status, party_uuid, date_authorized
```

**Column rename**：
- `information_system_id` → `system_characteristic_id`
  - soft FK 從 `compliance.information_systems.id` 改鉤 `oscal.system_security_plans_system_characteristics.id`
  - 既有 140 筆此欄為 null，rename 不影響資料
  - 對應 index 同步改名

**Column 新加**：
- `system_implementation_id` INTEGER NOT NULL FK → `ssp_system_implementations.id` ON DELETE CASCADE
  - backfill：依 (scope_type, scope_id) 對應到新 main row

### 3.2 新建 `oscal.ssp_system_implementations`（main, 1:1 per scope）

```
column                    type          nullable  default               purpose
─────────────────────────────────────────────────────────────────────────────────
id                        SERIAL        NOT NULL  -                     PK
uid                       UUID          NOT NULL  gen_random_uuid()     OSCAL UUID (匯出時排除)
scope_type                varchar(20)   NOT NULL  -                     'ssp' or 'module_frame'
scope_id                  integer       NOT NULL  -                     對應 scope 的 id (soft FK)
remarks                   text          nullable  -                     OSCAL block-level remarks
props_jsonb               jsonb         nullable  -                     OSCAL block-level props
created_at                timestamptz   NOT NULL  NOW()                 audit
updated_at                timestamptz   NOT NULL  NOW()                 audit
created_user              varchar(50)   nullable  -                     audit
updated_user              varchar(50)   nullable  -                     audit
─────────────────────────────────────────────────────────────────────────────────
UNIQUE CONSTRAINT (scope_type, scope_id)         ← 強制 1:1 per scope
INDEX (scope_type, scope_id)
```

**為什麼用 scope_type + scope_id 而非直接 ssp_id**：對齊 A0 items 表設計，支援 module_frame scope（範本層也可有 system-implementation 紀錄）。

### 3.3 新建 `oscal.ssp_inventory_item_components`（M:N join）

```
column              type          nullable  purpose
─────────────────────────────────────────────────────────────
id                  SERIAL        NOT NULL  PK
inventory_item_id   integer       NOT NULL  FK → ssp_system_implementation_items.id (type='hardware')
component_id        integer       NOT NULL  FK → ssp_system_implementation_items.id (type IN component family)
description         text          nullable  OSCAL link 補充說明
created_at          timestamptz   NOT NULL  audit
...                                          (省略 audit)
─────────────────────────────────────────────────────────────
UNIQUE CONSTRAINT (inventory_item_id, component_id)
INDEX (inventory_item_id)
INDEX (component_id)
FK both with ON DELETE CASCADE
```

**設計約束**：兩個 FK 都指向 items 表，但 implementation_type 不同。**application 層需驗證**：
- inventory_item_id 指向 row 的 implementation_type = 'hardware'
- component_id 指向 row 的 implementation_type IN ('component', 'system', 'subsystem', 'service', 'software')

DB 層無 CHECK 約束強制此語意（PG 不支援跨 row CHECK），由 SQLAlchemy event listener 或 service 層守。

---

## 4. jedi-oscal 套件變更

### 4.1 既有檔案 rename（語意對齊）

| 既有 | 新 |
|------|-----|
| `infra/model/ssp/ssp_system_implementation.py`（class `OscalSystemSecurityPlanSystemImplementation`）| `infra/model/ssp/ssp_system_implementation_items.py`（class `OscalSspSystemImplementationItem`）|
| `domain/entity/ssp/ssp_system_implementation_entity.py` (class `SystemImplementationEntity`) | `.../ssp_system_implementation_item_entity.py`（class `SspSystemImplementationItemEntity`）|
| `domain/entity/ssp/ssp_system_implementation_query_entity.py` | `.../ssp_system_implementation_item_query_entity.py` |
| `infra/repository/ssp/ssp_system_implementation_repo_impl.py` (class `SystemImplementationRepoImpl`) | `.../ssp_system_implementation_item_repo_impl.py` (class `SspSystemImplementationItemRepoImpl`) |
| `domain/repository/ssp/system_implementation_repo.py` (class `ISystemImplementationRepo`) | `.../system_implementation_item_repo.py` (class `ISspSystemImplementationItemRepo`) |
| `infra/mapper/ssp/system_implementation_mapper.py` | `.../system_implementation_item_mapper.py` |
| `app/dto/ssp/ssp_system_implementation_dto.py` (class `SystemImplementationDTO`) | `.../ssp_system_implementation_item_dto.py` (class `SspSystemImplementationItemDTO`) |
| `domain/services/ssp/system_implementation_domain_service.py` | `.../system_implementation_item_domain_service.py` |

**改 `__tablename__`**：`"system_security_plan_system_implementations"` → `"ssp_system_implementation_items"`

**改 column**：`information_system_id` → `system_characteristic_id`（含 ORM Mapped / Entity attribute / mapper to_model+to_entity / DTO）

**加 column**：`system_implementation_id` Mapped[int] + relationship to main

### 4.2 新建檔案（main 完整 stack）

| 層 | 新檔 |
|----|-----|
| ORM | `jedi_oscal/infra/model/ssp/ssp_system_implementation.py`（class `OscalSspSystemImplementation`，對應 main 表）|
| Entity | `jedi_oscal/domain/entity/ssp/ssp_system_implementation_entity.py`（class `SspSystemImplementationEntity`）|
| Query Entity | `jedi_oscal/domain/entity/ssp/ssp_system_implementation_query_entity.py`（class `SspSystemImplementationQueryEntity`）|
| Repo interface | `jedi_oscal/domain/repository/ssp/system_implementation_repo.py`（class `ISspSystemImplementationRepo`）|
| Repo impl | `jedi_oscal/infra/repository/ssp/ssp_system_implementation_repo_impl.py`（class `SspSystemImplementationRepoImpl`）|
| Mapper | `jedi_oscal/infra/mapper/ssp/system_implementation_mapper.py`（class `SspSystemImplementationMapper`）|
| DTO | `jedi_oscal/app/dto/ssp/ssp_system_implementation_dto.py`（class `SspSystemImplementationDTO`）|
| Domain service | `jedi_oscal/domain/services/ssp/system_implementation_domain_service.py`（class `SspSystemImplementationDomainService`）|

**核心方法**：
- `find_by_scope(scope_type, scope_id) -> Optional[entity]`（拿主檔）
- `upsert_by_scope(scope_type, scope_id, **kwargs) -> entity`（找不到就建，找到就返回 — caller 寫 items 前的 prerequisite）

### 4.3 新建檔案（join 完整 stack）

| 層 | 新檔 |
|----|-----|
| ORM | `jedi_oscal/infra/model/ssp/ssp_inventory_item_component.py`（class `OscalSspInventoryItemComponent`）|
| Entity | `jedi_oscal/domain/entity/ssp/ssp_inventory_item_component_entity.py` |
| Query Entity | `jedi_oscal/domain/entity/ssp/ssp_inventory_item_component_query_entity.py` |
| Repo interface + impl | `jedi_oscal/domain/repository/ssp/inventory_item_component_repo.py` + `infra/repository/ssp/...` |
| Mapper | `jedi_oscal/infra/mapper/ssp/inventory_item_component_mapper.py` |
| DTO | `jedi_oscal/app/dto/ssp/ssp_inventory_item_component_dto.py` |

### 4.4 YAML mapper 更新

`jedi_oscal/infra/mapper/ssp/ssp_yaml_mapper.py`：

- `_system_implementation_to_dict()` 改成從 **main 出發** + 讀 items + 三分支：
  - 從 ssp_system_implementations main 拿 block-level (remarks / props)
  - 從 ssp_system_implementation_items 撈 items
  - 依 implementation_type 三分支序列化（A0 Task 9 已有的邏輯）
  - 加 inventory-item.implemented-components 從 join 表填入

### 4.5 既有 ssp_yaml_mapper / ssp_mapper 父層引用更新

- `infra/model/ssp/ssp.py`：SSP ORM 的 `system_implementations` relationship 改指向 main 表
- `infra/mapper/ssp/system_security_plan_mapper.py`：mapping 父層引用 main
- `domain/entity/ssp/ssp_entity.py`：SSP entity 內 `system_implementation` 屬性指 main entity 而非 items list

---

## 5. 主 BE 變更

### 5.1 既有 caller 改寫

| 檔案 | 改動 |
|------|------|
| `app/oscal/service/ssp_versioning_service.py` | (a) import 改 `OscalSspSystemImplementationItem` (b) clone SSP 版本時 **先 clone main, 再 clone items** (c) items 寫入時填 `system_implementation_id` |
| `app/associations/service/project_device_mapping_service.py` | (a) import 改 `SspSystemImplementationItemEntity` (b) 寫 item 前 **upsert main**（透過 main domain service）拿 system_implementation_id (c) 寫 item 帶 system_implementation_id |
| `di_containers/oscal/oscal_containers.py` | 加 main / join 兩個 service wiring；既有 system_implementation wiring 改用新 class 名 |
| `tests/test_ssp_system_implementation_regression.py` | import 改 + 加 main upsert smoke test |

### 5.2 既有 caller 不動（已 verify）

| 檔案 | 不動原因 |
|------|---------|
| `app/project/service/oscal_project_service.py` | 用 `ProjectInformationSystem`（compliance.project_information_systems），跟 OSCAL items 表的 information_system_id 是不同 column |
| `app/grc/service/project_service.py` | 同上 |

---

## 6. 跨 scope polymorphic 表擴充（零 schema change）

純應用層擴 enum / 字串值，**不需 DDL migration**：

| 表 | 擴 context_type 新值 |
|----|--------------------|
| `oscal.oscal_responsible_parties` | `'system_implementation'`（main 層 responsible-parties） / `'inventory_item'` / `'component'`（per-item responsible-roles）|
| `oscal.ssp_reference_document_mappings` | `'system_implementation'`（block-level link to reference doc）|

新 context_id 都指向 main.id 或 items.id 對應。

---

## 7. 既有資料 Migration

### 7.1 既有 140 筆 hardware items（scope_type='ssp'）

migration 步驟（SQL 內已包）：
1. 不動既有 items 資料
2. 為 distinct `(scope_type, scope_id)` 建 main row（預期約 128-140 個，看 distinct 數）
3. backfill `items.system_implementation_id = main.id`（用 scope match）
4. `information_system_id` rename → `system_characteristic_id`（既有 140 筆此欄全為 null，rename 安全）

### 7.2 既有資料是否需要轉 system_characteristic_id？

既有 140 筆 hardware 的 `information_system_id` 全 null，**不需要資料轉換**。Column rename 只動 metadata（schema info），資料層完全不變。

未來如有 caller 把 information_system 鉤上來的需求，要走新的 system_characteristic 中介路徑（透過 `compliance.project_system_characteristic_mapping`）。

---

## 8. 測試範圍

### 8.1 jedi-oscal 既有測試（需要 fix）

- `tests/test_ssp_system_implementation_extension.py`（A0 32 個測試）
  - import 改新 class 名
  - column rename `information_system_id` → `system_characteristic_id` 對應
  - 加新 main / join 的 CRUD 測試

### 8.2 主 BE 既有測試（需要 fix）

- `tests/test_ssp_system_implementation_regression.py`（A0 regression）
  - import 改新 class 名
  - 加 main upsert smoke test
- `test/test_ssp_versioning_service.py`（既有 SSP versioning test）
  - 確認 clone 流程通新表結構

### 8.3 新增測試

- main 表 CRUD 測試 + UNIQUE constraint 測試
- join 表 CRUD + M:N relationship 測試
- yaml_mapper 三分支序列化 + implemented-components 輸出測試

---

## 9. 不在 A0.1 範圍

| 不做的事 | 何時做 |
|---------|-------|
| `system_characteristics` 改 1:1（對齊 OSCAL spec）| 未來如需要再評估 |
| `system-implementation.users[]` 表實作 | 客戶有真實需求才做 |
| `hardware` enum 改 `inventory-item` | 列「最後統整優化清單」|
| `responsible_party` varchar 欄位資料遷移到 `oscal_responsible_parties` | 列「最後統整優化清單」|
| Phase 2 後續 phase（A1~B6） | 各自 phase 處理 |

---

## 10. Acceptance Criteria

- [x] SQL migration 在 dev DB 跑通，140 筆 items 全部補 system_implementation_id（**shipped**: 129 個 distinct scope → 129 main rows, 140 items 全部補 FK）
- [x] jedi-oscal 既有 ORM rename + 新建 main + join 完整 stack 完成（6 commits 762d8fc / cc65e3b / c0fcae9 / d2178c1 / 8e3e58b / 4666091）
- [x] 既有 32 個 jedi-oscal 測試全綠 + 新加 47 個 unit test
- [x] 主 BE 既有 caller (`project_device_mapping_service` / `ssp_versioning_service`) 改用新 class 後 regression 測試通（33 BE test passed）
- [⚠️] BE 主程式 smoke boot 通過 — **partial**：DI / service / DB sanity 4 項都通，但完整 boot 撞到 jedi-issue / python-gitlab + PyGithub 環境問題（**非 A0.1 引入**，缺 GITLAB_*/GITHUB_TOKEN env vars）。已記為 follow-up。
- [x] 新 main / join 表 CRUD + relationship 測試通（unit test）
- [x] yaml_mapper inventory-item.implemented-components 輸出 OSCAL 結構正確
- [x] A0.1 changelog 完成（`docs/changelog/2026-05-19-tweak-ssp-system-impl-restructure.md`）
- [x] tracker README A0.1 row 標 shipped + commit hashes

---

## 11. Implementation Reality / Reconciliation

實作過程相對於原 spec / plan 的偏離紀錄。**保留作為未來讀者的決策軌跡**（不是補救文件，是脈絡保留）。

### 11.1 `_system_implementation_to_dict` 簽章設計偏離

**Spec / Plan 寫**：`(main_entity, items_list, join_links)` — main_entity 為第一 positional 參數。

**實際實作**：`(items_list, main_entity=None, join_links=None)` — items_list 維持第一 positional，main/join_links 為可選 kwarg。

**為何偏離**：既有 7 個 yaml_mapper 測試（A0 ship 時建的）均以 positional 方式 `_system_implementation_to_dict(sis_list)` 呼叫此 method。若照 spec 把 `main_entity` 放第一位，這 7 個 test 會把 `sis_list` 當 main_entity，全部破。改 backward-compatible 簽章是 minimal-disruption 工程決策，行為層面等價，spec/code reviewer 雙確認 ACCEPTABLE。

### 11.2 `_clone_system_implementations` 用 `old_ssp.system_implementations` 替代 `old_main.items`

**Plan 虛擬碼**：透過 main 的 items relationship `for old_item in old_main.items: ...`。

**實際實作**：用 SSP 既有 relationship `for old_impl in (old_ssp.system_implementations or []):`。

**為何偏離**：SSP ORM 已有指向 items list 的 relationship（A0 既有），複用無需新增 main → items 反向 relationship。Spec reviewer 確認語義等價。

### 11.3 Items 9 個 extension 欄位 clone

A0 既有 clone 只複製 6 個欄位（name / description / implementation_type / responsible_party + scope_type / scope_id）。A0.1 補齊：device_id / system_characteristic_id / title / purpose / status / party_uuid / date_authorized 共 7 個 — 對齊 A0 既有 ORM 但 clone 流程未補的欄位。`scope_type` / `scope_id` 因 caller 直接賦值不走 getattr，commit message 寫「9 個」是 A0 全部 OSCAL extension 欄位的總數，實質 clone 流程補了所有缺漏。

### 11.4 BE 全套 boot smoke 改為 partial

**Plan 寫**：`nohup poetry run python main_app.py` 後驗證 schema mismatch / IntegrityError 無。

**實際執行**：BE 啟動撞到 jedi-issue 套件初始化（python-gitlab `api_version=None` AssertionError + PyGithub `Auth.Token(None)` AssertionError）。**這是環境配置問題（缺 GITLAB_URL / GITLAB_API_VERSION / GITLAB_PRIVATE_TOKEN / GITHUB_TOKEN env vars），與 A0.1 改動無關**。改做 partial smoke：

- DI `OscalContainer` / `AssociationsContainer` 可實例化
- 6 個新 providers 全部 callable（item / main / inventory_item_component 各自的 repo + domain service）
- `SspVersioningService` 可建構
- `ProjectDeviceMappingService` constructor 含兩個新 kwarg
- DB 三表狀態正確：129 main + 140 items（FK 全補）+ 0 join

完整 boot smoke 留給已配置 GitLab / GitHub env 的環境執行（user 自行 or 後續 phase 補）。

### 11.5 Minor follow-ups（不阻 ship，code reviewer 建議）

| # | 項目 | 位置 | 嚴重度 |
|---|------|------|--------|
| 1 | `ProjectDeviceMappingService.add()` 內 `upsert_by_scope(created_user=None)` — main row audit trail 空白 | `app/associations/service/project_device_mapping_service.py` | MINOR (此 caller 無 user context 可取，屬 deliberate) |
| 2 | 刪 item 後不清 main — main 可能變孤兒 | 同上 | MINOR (純元資料，孤兒不破壞功能，屬 deliberate) |
| 3 | `_system_implementation_to_dict` 對 `inv_to_component_uuids[inv_id]` 沒做 sort — 多 component 情境順序不穩定 | `jedi_oscal/infra/mapper/ssp/ssp_yaml_mapper.py` | MINOR (1-component case 不踩到，多 component snapshot test 才會 flaky) |
