C3 — Implementation Plan

對應 design:design-C3.md 級別:重型(兩階段執行 PR1 + PR2) 預估工時:PR1 = BE 3 天 + test 1 天;PR2 = BE 1 天


§1

Pre-flight 驗證(PR1 開工前必跑)

# 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%';"

§2

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)

Commitfeat(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 → 回 []

Commitfeat(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):

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.jsonGRC_410001 / GRC_410002 對應文案
  • API client 攔截 410 response 後查 data.error_code → i18n 顯示

注意

  • Route 仍註冊(PR2 才撤)
  • 410 訊息對 FE 友善(指引 SSP-scoped endpoints)

Commitrefactor(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:
    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;

Commitchore(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/ → 410
    • DELETE /project-device/ → 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 期待)

Committest(c3): C3.PR1 derived response + 410 endpoint tests


T8 — Changelog (PR1)

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

---
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

-- 觀察期每天跑一次
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

§3

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 前手動跑一次):

-- 觀察期內 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

Commitchore(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 過度激進。實際不能整 rmapp/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 寫法,僅供參考)

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 註冊 → 確認過沒有

Commitrefactor(c3): C3.PR2 拆 device + pis Python stack + 撤 410 endpoint143f354


T12 — 撤 410 endpoint(BE)

檔案

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

結果:URL 變 404 而非 410

Commitrefactor(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

Committest(c3): C3.PR2 拆 stack 後 tests 清理


T14 — Changelog (PR2)

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

---
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 階段尾聲評估)

§4

測試規格

階段 測試 內容
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 表已不存在

§5

完成標準(DoD)

PR1

PR2


§6

風險 / 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

§7

跨 PR 整合 checkpoint

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