# C3 — Implementation Plan

> 對應 design：[design-C3.md](design-C3.md)
> 級別：重型（**兩階段執行 PR1 + PR2**）
> 預估工時：PR1 = BE 3 天 + test 1 天；PR2 = BE 1 天

---

## Pre-flight 驗證（PR1 開工前必跑）

```bash
# 1. C2 + C4 已 ship？
git log --oneline | grep -E "C2|C4" | head -5

# 2. 既有 mapping 表 row count（baseline）
psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -c \
  "SELECT 'pdm', COUNT(*) FROM compliance.project_device_mapping
   UNION ALL
   SELECT 'pis', COUNT(*) FROM compliance.project_information_systems;"

# 3. 沒 active AP 的 project 數（這些 backfill 會 skip）
psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -c \
  "SELECT p.id, p.uid, p.name
     FROM compliance.projects p
    WHERE NOT EXISTS (
       SELECT 1 FROM compliance.project_assessment_plan_mapping papm
       JOIN oscal.assessment_plans ap ON ap.id = papm.assessment_plan_id
       WHERE papm.project_id = p.id AND ap.status = 'active'
    );"

# 4. 確認所有 caller 都已盤點（重要！避免 PR2 撞遺漏 caller）
grep -rn "project_device_mapping\|ProjectDeviceMapping\|project_information_systems\|ProjectInformationSystem" \
  --include="*.py" | grep -v __pycache__ | grep -v test | awk -F: '{print $1}' | sort -u

# 5. 額外 grep raw SQL 引用（容易漏）
grep -rn "project_device_mapping\|project_information_systems" \
  --include="*.sql" --include="*.py" scripts/ infra/grc/ | grep -v __pycache__

# 6. RLS / view 依賴
psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -c \
  "SELECT viewname FROM pg_views WHERE definition ILIKE '%project_device_mapping%' OR definition ILIKE '%project_information_systems%';"
```

---

## PR1 — Service derived + 410 endpoints

### T1 — `_resolve_current_ssp_id` helper（BE）

**新檔**：`domain/oscal/service/ssp_current_resolver.py`

**內容**：
- 接 `project_id`，反查 latest active AP 對應 SSP id
- 共用給 `GrcProjectService.get_project` + `ProjectCurrentSspRoute` (C2.T6)
- Helper 而非 service，DI 注入點少

**Unit test**：
- 一般 case：project 有 1 個 active AP → 回對應 SSP
- Edge case：無 active AP → 回 None
- Edge case：多個 active AP → 取最新 (created_at DESC)

**Commit**：`feat(oscal): C3.PR1 add _resolve_current_ssp_id helper`

---

### T2 — `GrcProjectService.get_project()` 改 derived response（BE）

**檔案**：`app/grc/service/project_service.py`

**動作**：
- 對齊 design-C3.md §5.1
- `dto.devices` / `dto.audit_systems` 改從 current SSP items derive
- 沒 current SSP 或無 items → 回 `[]`（C3-Q5 決策）
- `dto.audit_systems` 內每筆需含 `uid` / `name` / `abbreviation`（對齊既有 `AuditSystemSummaryDto` 格式）
- `dto.devices` 內每筆需含 device 資訊（從 `device_id` 反查 `public.devices`）

**注意**：
- 此 PR **不拿掉** `_device_mapping_service` / `_pis_domain_service` 注入（PR2 再拆）
- 但實際呼叫已移除，保留在 `__init__` 是 dead injection

**Unit test**：
- 建專案 → 啟動 AP → backfill SSP items → get_project → 看到 devices/audit_systems
- 沒 SSP → 回 `[]`

**Commit**：`feat(grc-project): C3.PR1 get_project response derived from current SSP`

---

### T3 — `GrcProjectService.update_project()` 拿掉 Section 3 + 5（BE，搭配 C4）

**檔案**：`app/grc/service/project_service.py`

**動作**：對齊 C4 T4
- 拿掉 Section 3（audit_systems 替換）+ Section 5（devices 替換）
- `__init__` 拿掉 device_mapping / pis service 參數（PR1 即拿，避免 C4 撞 DI 註解問題）

**Commit**：合併到 C4 T4 commit 或同 PR

---

### T4 — `oscal_project_service.start_oscal_project` 拿掉 Step C（BE，搭配 C4）

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

**動作**：對齊 C4 T3

**Commit**：合併到 C4 T3 commit

---

### T5 — `/project-device*` 寫操作 endpoint 改 410（BE）— 用 GrcErrorCode

**檔案**：
- `common/code/grc_error_code.py` — 加 2 個 410 error code
- `api/project/routes/project_device_route.py`
- `api/project/routes/project_route.py`（JobExecutionDevice 系列）
- 任何其他 device write endpoint

**新增 error code**（對齊 design-C3.md §5.3）：
```python
GRC_PROJECT_DEVICE_DEPRECATED      = ("專案設備寫操作已下架，請至 SSP 編輯頁維護受評範圍", "GRC_410001")
GRC_PROJECT_INFO_SYSTEM_DEPRECATED = ("專案資訊系統寫操作已下架，請至 SSP 編輯頁維護受評範圍", "GRC_410002")
```

**改動**：
- POST / PUT / DELETE 全改回 410 Gone + `GrcErrorCode` 結構（FE 用 error_code 查 i18n）
- GET 改 derived from SSP（複用 T2 邏輯，或調用 SSP-scoped GET endpoint）
- 設計上不用 `BadRequestError` / `NotFound` 等 jedi exception class（沒有 GoneError），改手動 `return tuple, 410`

**FE 端配套**（C5 / C6 期間）：
- `src/config/locales/i18n/zh-tw/error.json` 加 `GRC_410001` / `GRC_410002` 對應文案
- API client 攔截 410 response 後查 `data.error_code` → i18n 顯示

**注意**：
- Route 仍註冊（PR2 才撤）
- 410 訊息對 FE 友善（指引 SSP-scoped endpoints）

**Commit**：`refactor(project-api): C3.PR1 device write endpoints 改回 410 + GrcErrorCode`

---

### T6 — Backfill SQL（BE — DBA 人工跑）

**新檔**：`scripts/sql/2026-MM-DD-c3-backfill-ssp-items-from-project-mappings.sql`

**內容**：對齊 design-C3.md §4.1

**執行流程**：
1. PR1 ship 後（code deploy 完成）
2. DBA 連線 `cmmgr / guidant_ai_dev / 192.168.50.188:25432`
3. 跑 SQL，看 RAISE NOTICE 確認 backfill row count
4. 抽樣 SQL verify：
   ```sql
   SELECT name, device_id, implementation_type
     FROM oscal.ssp_system_implementation_items
    WHERE updated_user = 'c3-backfill-2026-MM-DD'
    ORDER BY created_at DESC LIMIT 10;
   ```

**Commit**：`chore(sql): C3.PR1 backfill SSP items from project mappings`

---

### T7 — Unit / integration tests (BE)

**新測試**：
- `tests/test_c3_derived_response.py`
  - 建測試 SSP + items → get_project response 內 devices / audit_systems 內容對齊
  - 空 SSP / 無 active AP → 回 `[]`
- `tests/test_c3_410_endpoints.py`
  - POST /project-device → 410
  - PUT /project-device/<id> → 410
  - DELETE /project-device/<id> → 410
- Update 既有 test fixture（拿掉 mapping 表 INSERT 期待）

**Regression**：
- 既有 `tests/test_grc_project_service.py` get_project 部分需 update fixture
- 既有 `tests/test_oscal_project_service.py` start_project 部分需 update（Step C 拿掉後沒 device INSERT 期待）

**Commit**：`test(c3): C3.PR1 derived response + 410 endpoint tests`

---

### T8 — Changelog (PR1)

**檔案**：`docs/changelog/YYYY-MM-DD-refactor-c3-scope-migrate-to-ssp-pr1.md`

```markdown
---
type: tweak
breaking: true
modules: [grc, project, oscal]
issue: docs/features/FR-011.3-2605-ssp-edit-in-project/design-C3.md
---

## 需求說明
C3 PR1 — 受評範圍從 project 層 derive 改為 SSP 層：
- get_project response 內 devices / audit_systems 從 current AP 對應 SSP items derive
- 寫操作 endpoint (POST/PUT/DELETE /project-device 等) 改回 410 Gone
- 同期 backfill SQL 把既有 project_device_mapping / project_information_systems
  資料補進對應 current SSP 的 implementation_items
- mapping 表保留（PR2 才 DROP）

## 變更範圍
### BE
- 新 helper: domain/oscal/service/ssp_current_resolver.py
- GrcProjectService.get_project: dto.devices / dto.audit_systems derived
- 寫操作 route 改 410
- DI: 拿掉 device_mapping / pis service 注入（搭配 C4）
- Backfill SQL: scripts/sql/2026-MM-DD-c3-backfill-ssp-items-from-project-mappings.sql

## 行為差異
| 情境 | 改前 | 改後 |
|------|------|------|
| GET /grc/project/<uid> | devices/audit_systems from mapping 表 | derived from current SSP items |
| POST /project-device | 寫入 mapping 表 | 410 Gone |
| start_oscal_project devices payload | 寫入 mapping 表 | unknown=EXCLUDE 忽略（搭配 C4）|

## Breaking change 警示
- 寫操作 endpoint 已下架（410），FE 必須改用 /ssp/<uid>/ssp-resources/items (C2)

## 後續行動
- 1-2 週觀察期，監控 mapping 表無新 INSERT
- 之後 ship C3 PR2 (DROP TABLE + 拆 stack)
```

---

### T9 — 監控 Query 設定（PR1 後跑）

**目的**：確認沒有遺漏 caller 還在寫 mapping 表

**SQL**：
```sql
-- 觀察期每天跑一次
SELECT 'pdm', COUNT(*) FROM compliance.project_device_mapping
WHERE created_at > '<PR1-ship-date>'
UNION ALL
SELECT 'pis', COUNT(*) FROM compliance.project_information_systems
WHERE created_at > '<PR1-ship-date>';

-- 預期：0 / 0
```

**異常處理**：若觀察到非 0：
- 撈出 row 內容
- grep `created_user` 找 caller 路徑
- 補修後再進 PR2

---

## PR2 — DROP TABLE + 拆 stack

> **前提**：PR1 ship 1-2 週後，T9 監控 query 持續 0，無 regression report

### T10 — DROP TABLE SQL（BE — DBA 跑）

**新檔**：`scripts/sql/2026-MM-DD-c3-drop-project-mapping-tables.sql`

**內容**：對齊 design-C3.md §7.1

**前置驗證 SQL**（跑 DROP 前手動跑一次）：
```sql
-- 觀察期內 0 新 INSERT?
SELECT created_at, created_user
  FROM compliance.project_device_mapping
 WHERE created_at > '<PR1-ship-date>'
 LIMIT 5;
-- 預期：0 row

-- 沒有 view / 其他物件依賴?
SELECT viewname FROM pg_views
 WHERE definition ILIKE '%project_device_mapping%'
    OR definition ILIKE '%project_information_systems%';
-- 預期：0 row
```

**Commit**：`chore(sql): C3.PR2 DROP project_device_mapping + project_information_systems`

---

### T11 — 拆 Python stack（BE）

> ⚠️ **實作矯正紀錄（2026-05-23 PR2 ship 時補）**：原 plan 寫 `git rm -r` 三個
> associations 資料夾 + `整檔刪 associations_containers.py` 過度激進。**實際不能整 rm** —
> `app/associations/` 內 8 個 mapping 服務只有 2 個是 device 相關 (`project_device_mapping` +
> `job_execution_device_mapping`)，而 `job_execution_device_mapping` 還屬「任務指派裝置」
> 不是 C3 scope（AI dashboard registry + 4 個 API 還在用）。實際只逐檔拆 device 系列
> 10 檔 + jedi_information_system 內 project_* 系列 7 檔。詳見 changelog
> `2026-05-23-tweak-c3-pr2-drop-project-mapping-tables.md` §「Plan 矯正」。

**動作（原 plan 寫法，僅供參考）**：
```bash
git rm -r domain/associations/
git rm -r infra/associations/
git rm -r app/associations/

# jedi_information_system 內 project_* 系列
git rm jedi_information_system/domain/entity/project_information_system_entity.py
git rm jedi_information_system/domain/entity/project_information_system_query_entity.py
git rm jedi_information_system/domain/repository/project_information_system.py
git rm jedi_information_system/domain/service/project_information_system_domain_service.py
git rm jedi_information_system/infra/mapper/project_information_system_mapper.py
git rm jedi_information_system/infra/models/project_information_system.py
git rm jedi_information_system/infra/repository/project_information_system_repo_impl.py
```

**實際動作（精準逐檔，PR2 ship 時調整）**：device 10 檔 + pis 7 檔 + 8 個 `__init__.py`
拿掉對應 export + DI cleanup 3 處（associations_containers / information_system_container
拿掉 project_*_provider 但不整檔刪、oscal_containers 拿掉 SspWriteStrategy.project_info_system_repo
wiring）+ ssp_write_strategy `__init__` 拿掉 dead injection param。

**DI container 清理**：
- ~~`di_containers/associations/associations_containers.py` — 整檔刪~~ → **不整檔刪**，僅拿掉
  `project_device_mapping_repo` / `project_device_mapping_domain_service` /
  `project_device_mapping_service` 三個 provider + import
- `di_containers/information_system/information_system_container.py` — 拿掉
  `project_information_system_repo` + `project_information_system_domain_service` provider + import
- ~~`di_containers/grc/grc_containers.py` — 確認 PR1 已拿掉~~ → PR1 已拿掉，本 PR 無需動
- ~~`di_containers/project/project_containers.py` — 確認 PR1 已拿掉~~ → 同上
- `di_containers/oscal/oscal_containers.py:657` — 拿掉 SspWriteStrategy 的
  `project_info_system_repo=information_system_container.project_information_system_repo` wiring

**其他清理**：
- `domain/oscal/strategy/ssp_write_strategy.py` — `__init__` 拿掉 `project_info_system_repo`
  param + `self._project_info_sys_repo` 賦值（dead injection 從未被使用）；line 275 原「TODO
  Phase E.2」段換成 PR2 落地註記
- ~~`app/__init__.py` / `domain/__init__.py` — 移除對應 import~~ → 實際 `__init__.py` 8 個分別
  拿掉 device 系列 export
- ~~`config/app_modules.py` — 確認沒有 associations module 註冊~~ → 確認過沒有

**Commit**：`refactor(c3): C3.PR2 拆 device + pis Python stack + 撤 410 endpoint`（`143f354`）

---

### T12 — 撤 410 endpoint（BE）

**檔案**：
- 刪 `api/project/routes/project_device_route.py`
- 刪相關 JobExecutionDevice 系列 route 檔案
- `api/project/__init__.py` 拿掉對應 `add_resource` 註冊

**結果**：URL 變 404 而非 410

**Commit**：`refactor(project-api): C3.PR2 撤除已 deprecated 的 device write endpoints`

---

### T13 — Tests cleanup（BE）

**動作**：
- 跑 `pytest` 確認沒有 test 還在引用已刪檔
- 刪 `tests/test_project_device_mapping_*.py` 等
- 既有 grc / project test 若引用 associations 模組 → 同步刪 import

**Commit**：`test(c3): C3.PR2 拆 stack 後 tests 清理`

---

### T14 — Changelog (PR2)

**檔案**：`docs/changelog/YYYY-MM-DD-refactor-c3-scope-migrate-to-ssp-pr2.md`

```markdown
---
type: tweak
breaking: false
modules: [grc, project, associations]
issue: docs/features/FR-011.3-2605-ssp-edit-in-project/design-C3.md
---

## 需求說明
C3 PR2 — 完成 project_device_mapping / project_information_systems 廢除：
- DROP 兩張表
- 拆 domain/associations/ + infra/associations/ + app/associations/ 整套 stack
- 拆 jedi_information_system project_information_system 系列
- 撤 410 endpoint (改 404)
- 拿掉 ssp_write_strategy.py 內 Phase E.2 TODO 段

## 變更範圍
- BE: 大量 git rm + DI cleanup
- DB: DROP TABLE
- API: 404 取代 410

## 後續清理 (可選)
- OscalRole ORM model（C 階段尾聲評估）
```

---

## 測試規格

| 階段 | 測試 | 內容 |
|------|------|------|
| PR1 | Unit | derived response 對齊 SSP items / 空回 [] / 寫操作 410 |
| PR1 | Integration | 建專案 → AP → SSP items → get_project response 正確 |
| PR1 | Regression | 既有 grc / project test 全綠 |
| PR1 | Migration | backfill SQL 跑前後 row count 對等性驗證 |
| PR2 | Unit | 撤 stack 後 import 全找不到 |
| PR2 | Integration | 跑既有 e2e 不撞 missing module |
| PR2 | DB | psql `\dt` 確認 mapping 表已不存在 |

---

## 完成標準（DoD）

### PR1
- [ ] _resolve_current_ssp_id helper + unit test
- [ ] GrcProjectService.get_project derived
- [ ] 寫操作 route 改 410
- [ ] backfill SQL 跑完 + verify
- [ ] BE pytest 全綠
- [ ] Changelog 完成 (breaking: true)
- [ ] README tracker (C3 PR1) 狀態更新

### PR2
- [ ] DROP TABLE SQL 跑完
- [ ] stack `git rm` 完成
- [ ] DI cleanup
- [ ] 410 endpoint 撤除
- [ ] Tests 全綠
- [ ] Changelog 完成
- [ ] README tracker (C3 PR2) 狀態 → shipped

---

## 風險 / Rollback

### PR1 Rollback
- Code revert（PR commit revert）
- Data：`DELETE FROM oscal.ssp_system_implementation_items WHERE updated_user='c3-backfill-2026-MM-DD'`
- mapping 表保留中，rollback 完整無資料遺失

### PR2 Rollback
- **無法簡單 revert** — mapping 表已 DROP
- 需要：恢復 mapping 表 → 從 SSP items 反向 backfill → revert code
- **強烈建議 PR2 前確認 PR1 觀察期穩定**，PR2 是 point of no return

### 監控 metric
- PR1 期間每天跑 T9 SQL
- 任何 PR1 ~ PR2 期間 mapping 表 INSERT 都要追源頭
- 連續 7 天 0 INSERT 才考慮 PR2

---

## 跨 PR 整合 checkpoint

| Checkpoint | PR1 | PR2 |
|-----------|-----|-----|
| Migration SQL 跑過 | ✅ backfill | ✅ DROP |
| Tests 全綠 | ✅ | ✅ |
| Changelog | ✅ breaking | ✅ |
| Tracker 更新 | ✅ (in progress) | ✅ (shipped) |
| Smoke test | ✅ | ✅ |
| 觀察期 | 1-2 週 | — |
