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 hardwareinventory-item enum? 不改,沿用 hardware 既有 140 筆 + caller pattern 已熟,OSCAL 匯出 mapper 翻譯即可

3. Schema 變更詳細規格

3.1 既有表 rename: system_security_plan_system_implementationsssp_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_idsystem_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"

改 columninformation_system_idsystem_characteristic_id(含 ORM Mapped / Entity attribute / mapper to_model+to_entity / DTO)

加 columnsystem_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_idsystem_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

  • [⚠️] BE 主程式 smoke boot 通過 — partial:DI / service / DB sanity 4 項都通,但完整 boot 撞到 jedi-issue / python-gitlab + PyGithub 環境問題(非 A0.1 引入,缺 GITLAB_*/GITHUB_TOKEN env vars)。已記為 follow-up。

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_implementationsold_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_dictinv_to_component_uuids[inv_id] 沒做 sort — 多 component 情境順序不穩定 jedi_oscal/infra/mapper/ssp/ssp_yaml_mapper.py MINOR (1-component case 不踩到,多 component snapshot test 才會 flaky)