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 匯出)
A0 完成時用「擴充既有表」方案處理 OSCAL system-implementation 鏡像。事後盤點發現幾個結構問題:
system_security_plan_system_implementations 看似 main 表但實際是 items 集合(multi-row, implementation_type 區分)system-implementation block 層:OSCAL spec 上是 1:1 block per SSP,DB 沒對應 entityinventory-item.implemented-components[] 關聯:M:N 對應沒實作information_system_id soft FK 跟既有 system_characteristics 中介機制重複ssp_* 前綴慣例(ssp_docx_parse_jobs / ssp_reference_documents),但新加的還是用舊式 system_security_plan_* 前綴A0.1 是這些問題的補丁,設計校正而非新功能。
| # | 決策點 | 結論 | 理由 |
|---|---|---|---|
| 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 翻譯即可 |
system_security_plan_system_implementations → ssp_system_implementation_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
compliance.information_systems.id 改鉤 oscal.system_security_plans_system_characteristics.idColumn 新加:
system_implementation_id INTEGER NOT NULL FK → ssp_system_implementations.id ON DELETE CASCADE
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 紀錄)。
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 層需驗證:
DB 層無 CHECK 約束強制此語意(PG 不支援跨 row CHECK),由 SQLAlchemy event listener 或 service 層守。
| 既有 | 新 |
|---|---|
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
| 層 | 新檔 |
|---|---|
| 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)| 層 | 新檔 |
|---|---|
| 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 |
jedi_oscal/infra/mapper/ssp/ssp_yaml_mapper.py:
_system_implementation_to_dict() 改成從 main 出發 + 讀 items + 三分支:
infra/model/ssp/ssp.py:SSP ORM 的 system_implementations relationship 改指向 main 表infra/mapper/ssp/system_security_plan_mapper.py:mapping 父層引用 maindomain/entity/ssp/ssp_entity.py:SSP entity 內 system_implementation 屬性指 main entity 而非 items list| 檔案 | 改動 |
|---|---|
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 |
| 檔案 | 不動原因 |
|---|---|
app/project/service/oscal_project_service.py |
用 ProjectInformationSystem(compliance.project_information_systems),跟 OSCAL items 表的 information_system_id 是不同 column |
app/grc/service/project_service.py |
同上 |
純應用層擴 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 對應。
migration 步驟(SQL 內已包):
(scope_type, scope_id) 建 main row(預期約 128-140 個,看 distinct 數)items.system_implementation_id = main.id(用 scope match)information_system_id rename → system_characteristic_id(既有 140 筆此欄全為 null,rename 安全)既有 140 筆 hardware 的 information_system_id 全 null,不需要資料轉換。Column rename 只動 metadata(schema info),資料層完全不變。
未來如有 caller 把 information_system 鉤上來的需求,要走新的 system_characteristic 中介路徑(透過 compliance.project_system_characteristic_mapping)。
tests/test_ssp_system_implementation_extension.py(A0 32 個測試)
information_system_id → system_characteristic_id 對應tests/test_ssp_system_implementation_regression.py(A0 regression)
test/test_ssp_versioning_service.py(既有 SSP versioning test)
| 不做的事 | 何時做 |
|---|---|
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 處理 |
實作過程相對於原 spec / plan 的偏離紀錄。保留作為未來讀者的決策軌跡(不是補救文件,是脈絡保留)。
_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。
_clone_system_implementations 用 old_ssp.system_implementations 替代 old_main.itemsPlan 虛擬碼:透過 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 確認語義等價。
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 流程補了所有缺漏。
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:
OscalContainer / AssociationsContainer 可實例化SspVersioningService 可建構ProjectDeviceMappingService constructor 含兩個新 kwarg完整 boot smoke 留給已配置 GitLab / GitHub env 的環境執行(user 自行 or 後續 phase 補)。
| # | 項目 | 位置 | 嚴重度 |
|---|---|---|---|
| 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) |