system_security_plan_system_implementations 表擴充For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal:在既有 oscal.system_security_plan_system_implementations 表 + jedi-oscal 對應 stack 上加 9 個新欄位、改 1 欄 nullable、enum +1 值、補 index,使其能服 Phase 2 後續所有 phase 的匯入 / 匯出需求。
Architecture:DDD 嚴格分層擴充 — DB schema migration 在主專案 scripts/sql/ 內以 cmmgr 帳號執行(避 RLS);jedi-oscal 套件 8 個層(ORM / entity / query_entity / repo interface + impl / mapper / yaml_mapper / DTO / enum)平行擴充。dev 期走 poetry path dependency 不立即發版,feature 整體完成才一次性 bump 版本推 Nexus。
Tech Stack:Python 3.11 / SQLAlchemy 2.0 / PostgreSQL / pytest / poetry(path dep)/ jedi-oscal local source
Spec:design.md(A0 SDD 收斂 10 個 brainstorm 問題)
Branch:feature/ssp-import-export-phase2(已 checkout)
跨 repo 範圍:
git add <file1> <file2>,禁用 -am / -A(避免 sweep 不相關修改)Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>poetry lock:用 poetry update jedi-oscal(CLAUDE.md「依賴更新一律 poetry update」)cmmgr(不用 cm_app,避 RLS)| 動作 | 路徑 | 說明 |
|---|---|---|
| Modify | pyproject.toml |
取消 jedi-oscal path dep 註解,pin 0.0.16 改 comment |
| Create | scripts/sql/2026-05-18-extend-ssp-system-implementations.sql |
DB schema + 140 筆資料 migration |
| Create | tests/test_ssp_system_implementation_regression.py |
既有 caller smoke regression test |
| Create | docs/changelog/2026-05-18-tweak-ssp-system-impl-extension.md |
A0 變更紀錄 |
| Modify | docs/features/FR-011.2-2605-ssp-import-export-phase2/README.md |
A0 row 狀態 → shipped + commit hash |
| Modify | docs/features/FR-011.2-2605-ssp-import-export-phase2/requirement-understanding.md |
A0 段加 ✅ shipped 標記 |
~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/)| 動作 | 路徑 | 說明 |
|---|---|---|
| Modify | jedi_oscal/common/enum/code_enum.py |
SystemImplementationType 加 LEVERAGED_AUTHORIZATION |
| Modify | jedi_oscal/infra/model/ssp/ssp_system_implementation.py |
加 9 個 Mapped[] column + 改 system_security_plan_id nullable + 加 3 個 index |
| Modify | jedi_oscal/domain/entity/ssp/ssp_system_implementation_entity.py |
加 9 個 attribute |
| Modify | jedi_oscal/domain/entity/ssp/ssp_system_implementation_query_entity.py |
加 query 篩選欄位 |
| Modify | jedi_oscal/infra/repository/ssp/ssp_system_implementation_repo_impl.py |
加 list_by_scope / find_by_device_id / find_by_information_system_id |
| Modify | jedi_oscal/domain/repository/ssp/system_implementation_repo.py |
interface 加對應 method |
| Modify | jedi_oscal/infra/mapper/ssp/system_implementation_mapper.py |
新欄位 entity ↔︎ model 雙向 mapping |
| Modify | jedi_oscal/app/dto/ssp/ssp_system_implementation_dto.py |
加 9 個欄位 |
| Modify | jedi_oscal/infra/mapper/ssp/ssp_yaml_mapper.py |
依 implementation_type 分支序列化 |
| Create | tests/test_ssp_system_implementation_extension.py |
9 欄位 + 3 query method + enum + mapper round-trip 整合測試 |
開工前驗證所有 design.md §9 假設仍成立 — 從 brainstorm 到實作中間環境可能已變。
Files: (無,只是 verify 動作)
head -90 ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/model/ssp/ssp_system_implementation.pyExpected: class OscalSystemSecurityPlanSystemImplementation、__tablename__ = "system_security_plan_system_implementations"、system_security_plan_id 是 NOT NULL ForeignKey、有 name / description / implementation_type / responsible_party 6 欄位。
grep -A 8 "class SystemImplementationType" ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/common/enum/code_enum.pyExpected: SYSTEM / SUBSYSTEM / SERVICE / COMPONENT / HARDWARE / SOFTWARE 6 個值。
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -c "SELECT implementation_type, COUNT(*) FROM oscal.system_security_plan_system_implementations GROUP BY implementation_type;"Expected: hardware | 140。若數字不同,停下確認原因再開工。
ls ~/Projects/Billows/Audit-Manager/compliance-manager-be/app/associations/service/project_device_mapping_service.py
ls ~/Projects/Billows/Audit-Manager/compliance-manager-be/app/oscal/service/ssp_versioning_service.pyExpected: 兩個檔都存在。
grep "jedi-oscal" ~/Projects/Billows/Audit-Manager/compliance-manager-be/pyproject.tomlExpected: "jedi-oscal==0.0.16"(active 行)+ commented out path line。若版本已不是 0.0.16,記錄當前版本後繼續。
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be && git branch --show-currentExpected: feature/ssp-import-export-phase2。
目的:dev 期改套件源碼直接生效,避免每改 bump 版本(CLAUDE.md「改套件先走 poetry path dependency」)。
Files:
compliance-manager-be/pyproject.toml註解掉 active 的 pin 行,取消註解 path 行:
# jedi-oscal 改 path dep(dev only,feature 完成還原)
# "jedi-oscal==0.0.16",
jedi-oscal = { path = "/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/jedi-oscal", develop = true }cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
poetry update jedi-oscalExpected: 安裝完成,無錯誤。
poetry run python -c "from jedi_oscal.common.enum.code_enum import SystemImplementationType; print(list(SystemImplementationType))"Expected: 印出 6 個 enum 值。
⚠️
pyproject.toml的 path dep 改動屬於 dev-only,不入 commit(CLAUDE.md memory)。整個 A0 feature 完工 + smoke test 通過後,最後一個 task 才還原 pin Nexus 版本一起 commit。
目的:加 LEVERAGED_AUTHORIZATION = "leveraged-authorization" 一個值。
Files:
jedi-oscal/jedi_oscal/common/enum/code_enum.pyjedi-oscal/tests/test_ssp_system_implementation_extension.py(首次建立此測試檔)mkdir -p ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/tests建立 ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/tests/test_ssp_system_implementation_extension.py:
"""Phase 2 A0: 既有 system_security_plan_system_implementations 表擴充測試"""
from jedi_oscal.common.enum.code_enum import SystemImplementationType
class TestEnumExtension:
def test_leveraged_authorization_value_exists(self):
assert SystemImplementationType.LEVERAGED_AUTHORIZATION.value == "leveraged-authorization"
def test_existing_hardware_value_preserved(self):
assert SystemImplementationType.HARDWARE.value == "hardware"cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
poetry run pytest tests/test_ssp_system_implementation_extension.py::TestEnumExtension::test_leveraged_authorization_value_exists -vExpected: FAIL with AttributeError: LEVERAGED_AUTHORIZATION。
編輯 jedi_oscal/common/enum/code_enum.py,找到 class SystemImplementationType,加最後一行:
LEVERAGED_AUTHORIZATION = "leveraged-authorization"poetry run pytest tests/test_ssp_system_implementation_extension.py::TestEnumExtension -vExpected: 2 passed。
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
git add jedi_oscal/common/enum/code_enum.py tests/test_ssp_system_implementation_extension.py
git commit -m "$(cat <<'EOF'
feat(ssp-system-impl): SystemImplementationType 加 LEVERAGED_AUTHORIZATION
Phase 2 A0:支援 OSCAL leveraged-authorization 多型。既有 enum 6 值保留。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"目的:DB schema 先擴充才能讓後續 ORM / mapper 整合測試在實際 DB 上跑通。
Files:
compliance-manager-be/scripts/sql/2026-05-18-extend-ssp-system-implementations.sqlCreate scripts/sql/2026-05-18-extend-ssp-system-implementations.sql:
-- Date: 2026-05-18
-- Purpose: Phase 2 A0 — 擴充 oscal.system_security_plan_system_implementations
-- 1. 加 9 個新 column (scope_type / scope_id / device_id / information_system_id /
-- title / purpose / status / party_uuid / date_authorized)
-- 2. system_security_plan_id 改 nullable(讓 scope_type='module_frame' 可為 null)
-- 3. 既有 140 筆 hardware 資料補 scope_type='ssp' / scope_id
-- 4. 加 3 個 index
-- 5. Table COMMENT 標示多型用途
BEGIN;
-- 1. ADD COLUMN — 9 個新欄位 (2026-05-18)
ALTER TABLE oscal.system_security_plan_system_implementations
ADD COLUMN scope_type varchar(20),
ADD COLUMN scope_id integer,
ADD COLUMN device_id integer,
ADD COLUMN information_system_id integer,
ADD COLUMN title varchar(255),
ADD COLUMN purpose text,
ADD COLUMN status varchar(50),
ADD COLUMN party_uuid varchar(36),
ADD COLUMN date_authorized date;
-- 2. 既有 140 筆 hardware 資料補 scope_type / scope_id (2026-05-18)
UPDATE oscal.system_security_plan_system_implementations
SET scope_type = 'ssp',
scope_id = system_security_plan_id
WHERE scope_type IS NULL;
-- 3. scope_type / scope_id 補資料後改 NOT NULL (2026-05-18)
ALTER TABLE oscal.system_security_plan_system_implementations
ALTER COLUMN scope_type SET NOT NULL,
ALTER COLUMN scope_id SET NOT NULL;
-- 4. system_security_plan_id 改 nullable (2026-05-18)
-- 既有 CASCADE FK 保留;module_frame scope 時此欄為 null
ALTER TABLE oscal.system_security_plan_system_implementations
ALTER COLUMN system_security_plan_id DROP NOT NULL;
-- 5. 加 3 個 index 加速 scope / soft FK 查詢 (2026-05-18)
CREATE INDEX IF NOT EXISTS ix_ssp_sys_impl_scope
ON oscal.system_security_plan_system_implementations (scope_type, scope_id);
CREATE INDEX IF NOT EXISTS ix_ssp_sys_impl_device_id
ON oscal.system_security_plan_system_implementations (device_id);
CREATE INDEX IF NOT EXISTS ix_ssp_sys_impl_info_system_id
ON oscal.system_security_plan_system_implementations (information_system_id);
-- 6. Table COMMENT 標示多型 + scope 雙層用途 (2026-05-18)
COMMENT ON TABLE oscal.system_security_plan_system_implementations IS
'OSCAL system-implementation 多型鏡像表(Phase 2 A0 擴充:scope_type 區分 ssp / module_frame)。
implementation_type 區分 hardware (device 鏡像) / component (information_system 鏡像) /
leveraged-authorization / system / subsystem / service / software。';
COMMIT;PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
-c "BEGIN; \i ~/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/sql/2026-05-18-extend-ssp-system-implementations.sql ROLLBACK;" 2>&1 | head -20不對,這指令會直接執行整支 SQL(裡頭已有 COMMIT)。實際 dry-run 用:
# 退而求其次:先檢視 SQL 語法
cat ~/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/sql/2026-05-18-extend-ssp-system-implementations.sql確認 SQL 內容無誤。
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
-f ~/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/sql/2026-05-18-extend-ssp-system-implementations.sqlExpected: BEGIN / ALTER TABLE x3 / UPDATE 140 / CREATE INDEX x3 / COMMENT / COMMIT。
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev <<'EOF'
\d oscal.system_security_plan_system_implementations
SELECT scope_type, COUNT(*) FROM oscal.system_security_plan_system_implementations GROUP BY scope_type;
EOFExpected:
\d 輸出含 scope_type / scope_id / device_id / information_system_id / title / purpose / status / party_uuid / date_authorized 9 個新欄位system_security_plan_id 行顯示「可空」/「nullable」scope_type='ssp', count=140cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
git add scripts/sql/2026-05-18-extend-ssp-system-implementations.sql
git commit -m "$(cat <<'EOF'
tweak(ssp-system-impl): 擴充 system_security_plan_system_implementations schema
Phase 2 A0:
- 加 9 個 column (scope_type / scope_id / device_id / info_system_id / title /
purpose / status / party_uuid / date_authorized)
- system_security_plan_id 改 nullable(讓 module_frame scope 可為 null)
- 既有 140 筆 hardware 補 scope_type='ssp' / scope_id
- 加 3 個 index + Table COMMENT
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"目的:把 §3 SQL 加的 9 欄位反映到 SQLAlchemy ORM;改 system_security_plan_id nullable;補 index 宣告。
Files:
jedi-oscal/jedi_oscal/infra/model/ssp/ssp_system_implementation.pyjedi-oscal/tests/test_ssp_system_implementation_extension.py在 tests/test_ssp_system_implementation_extension.py 加 class:
from jedi_oscal.infra.model.ssp.ssp_system_implementation import (
OscalSystemSecurityPlanSystemImplementation,
)
class TestOrmExtension:
def test_new_columns_declared(self):
cols = OscalSystemSecurityPlanSystemImplementation.__table__.columns
for name in (
"scope_type", "scope_id", "device_id", "information_system_id",
"title", "purpose", "status", "party_uuid", "date_authorized",
):
assert name in cols, f"missing column: {name}"
def test_system_security_plan_id_is_nullable(self):
col = OscalSystemSecurityPlanSystemImplementation.__table__.columns["system_security_plan_id"]
assert col.nullable is True
def test_scope_columns_not_nullable(self):
cols = OscalSystemSecurityPlanSystemImplementation.__table__.columns
assert cols["scope_type"].nullable is False
assert cols["scope_id"].nullable is Falsecd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
poetry run pytest tests/test_ssp_system_implementation_extension.py::TestOrmExtension -vExpected: FAIL — 9 個欄位都不在 model 上 + nullable 都還是 False。
編輯 jedi_oscal/infra/model/ssp/ssp_system_implementation.py。
(a) imports 段加:
from datetime import date
from sqlalchemy import Integer, Date(既有 import 已有 String、Text、UUID、ForeignKey、DateTime、func、Index,按需要保留)
(b) __table_args__ 補 3 個 index(在既有 Index() list 內):
__table_args__ = (
Index("ix_ssp_sys_impl_uid", "uid"),
Index("ix_ssp_sys_impl_ssp_id", "system_security_plan_id"),
Index("ix_ssp_sys_impl_ssp_type", "system_security_plan_id", "implementation_type"),
Index("ix_ssp_sys_impl_scope", "scope_type", "scope_id"),
Index("ix_ssp_sys_impl_device_id", "device_id"),
Index("ix_ssp_sys_impl_info_system_id", "information_system_id"),
{"schema": "oscal", "comment": "..."},
)(c) 改 system_security_plan_id nullable:
system_security_plan_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("oscal.system_security_plans.id", ondelete="CASCADE"),
nullable=True, # ← 從 False 改成 True
comment="所屬 SSP ID(scope_type='module_frame' 時為 null)",
)(d) 在 responsible_party 後加 9 個新欄位(含 deprecated 註記給 responsible_party):
# ========= Scope (Phase 2 A0) =========
scope_type: Mapped[str] = mapped_column(
String(20),
nullable=False,
comment="'ssp' or 'module_frame' — 此 row 隸屬範圍",
)
scope_id: Mapped[int] = mapped_column(
Integer,
nullable=False,
comment="對應 scope_type 指向的 id(soft FK,無 DB constraint)",
)
# ========= Soft FK to tenant 層 (Phase 2 A0) =========
device_id: Mapped[Optional[int]] = mapped_column(
Integer,
nullable=True,
comment="soft FK → public.devices.id;hardware 鉤稽既有 device 時填",
)
information_system_id: Mapped[Optional[int]] = mapped_column(
Integer,
nullable=True,
comment="soft FK → compliance.information_systems.id;component 鉤稽既有 system 時填",
)
# ========= OSCAL Common Optional Fields (Phase 2 A0) =========
title: Mapped[Optional[str]] = mapped_column(
String(255),
nullable=True,
comment="OSCAL component.title 人類可讀標題(與 name 區分)",
)
purpose: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
comment="OSCAL component.purpose 用途說明",
)
status: Mapped[Optional[str]] = mapped_column(
String(50),
nullable=True,
comment="OSCAL component.status (under-development | operational | disposition | other)",
)
# ========= OSCAL Leveraged-Authorization Specific (Phase 2 A0) =========
party_uuid: Mapped[Optional[str]] = mapped_column(
String(36),
nullable=True,
comment="OSCAL leveraged-authorization.party-uuid(授權方;對齊 oscal_responsible_parties.party_uuid 型別)",
)
date_authorized: Mapped[Optional[date]] = mapped_column(
Date,
nullable=True,
comment="OSCAL leveraged-authorization.date-authorized",
)(e) 在 responsible_party 欄位的 docstring 加 deprecated 註記:
responsible_party: Mapped[Optional[str]] = mapped_column(
String(100),
comment="DEPRECATED (Phase 2 A0): 新功能改用 oscal_responsible_parties; 既有資料保留",
)poetry run pytest tests/test_ssp_system_implementation_extension.py::TestOrmExtension -vExpected: 3 passed。
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
git add jedi_oscal/infra/model/ssp/ssp_system_implementation.py \
tests/test_ssp_system_implementation_extension.py
git commit -m "$(cat <<'EOF'
feat(ssp-system-impl): ORM 擴充 — 加 9 欄位 + 改 ssp_id nullable
Phase 2 A0:ORM 反映 SQL schema 擴充。responsible_party 標 deprecated。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"目的:DDD domain entity 反映 9 個新欄位;Query entity 支援新篩選欄位。
Files:
jedi-oscal/jedi_oscal/domain/entity/ssp/ssp_system_implementation_entity.pyjedi-oscal/jedi_oscal/domain/entity/ssp/ssp_system_implementation_query_entity.pyjedi-oscal/tests/test_ssp_system_implementation_extension.pyhead -60 ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/domain/entity/ssp/ssp_system_implementation_entity.py
head -50 ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/domain/entity/ssp/ssp_system_implementation_query_entity.py加 class 到 tests/test_ssp_system_implementation_extension.py:
from datetime import date
from jedi_oscal.domain.entity.ssp.ssp_system_implementation_entity import (
SspSystemImplementationEntity,
)
from jedi_oscal.domain.entity.ssp.ssp_system_implementation_query_entity import (
SspSystemImplementationQueryEntity,
)
class TestEntityExtension:
def test_entity_accepts_new_attributes(self):
entity = SspSystemImplementationEntity(
scope_type="module_frame",
scope_id=1,
device_id=42,
information_system_id=None,
title="Daisy's MacBook",
purpose="Engineering workstation",
status="operational",
party_uuid=None,
date_authorized=None,
)
assert entity.scope_type == "module_frame"
assert entity.scope_id == 1
assert entity.device_id == 42
assert entity.title == "Daisy's MacBook"
def test_entity_leveraged_authorization_fields(self):
entity = SspSystemImplementationEntity(
scope_type="ssp",
scope_id=10,
party_uuid="abc-123-def",
date_authorized=date(2026, 5, 18),
)
assert entity.party_uuid == "abc-123-def"
assert entity.date_authorized == date(2026, 5, 18)
class TestQueryEntityExtension:
def test_query_entity_accepts_scope_filter(self):
q = SspSystemImplementationQueryEntity(scope_type="module_frame", scope_id=1)
assert q.scope_type == "module_frame"
assert q.scope_id == 1
def test_query_entity_accepts_soft_fk_filter(self):
q = SspSystemImplementationQueryEntity(device_id=42, information_system_id=None)
assert q.device_id == 42poetry run pytest tests/test_ssp_system_implementation_extension.py::TestEntityExtension tests/test_ssp_system_implementation_extension.py::TestQueryEntityExtension -vExpected: FAIL with TypeError: __init__() got an unexpected keyword argument.
編輯 ssp_system_implementation_entity.py,在 __init__ 加 9 個參數(mirror Task 4 ORM 欄位):
def __init__(
self,
# ...既有參數...
scope_type: Optional[str] = None,
scope_id: Optional[int] = None,
device_id: Optional[int] = None,
information_system_id: Optional[int] = None,
title: Optional[str] = None,
purpose: Optional[str] = None,
status: Optional[str] = None,
party_uuid: Optional[str] = None,
date_authorized: Optional[date] = None,
):
# ...既有 self.x = x...
self.scope_type = scope_type
self.scope_id = scope_id
self.device_id = device_id
self.information_system_id = information_system_id
self.title = title
self.purpose = purpose
self.status = status
self.party_uuid = party_uuid
self.date_authorized = date_authorizedssp_system_implementation_query_entity.py 加同樣 9 個參數(全部 default None)。
poetry run pytest tests/test_ssp_system_implementation_extension.py::TestEntityExtension tests/test_ssp_system_implementation_extension.py::TestQueryEntityExtension -vExpected: 4 passed。
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
git add jedi_oscal/domain/entity/ssp/ssp_system_implementation_entity.py \
jedi_oscal/domain/entity/ssp/ssp_system_implementation_query_entity.py \
tests/test_ssp_system_implementation_extension.py
git commit -m "$(cat <<'EOF'
feat(ssp-system-impl): Entity + QueryEntity 擴充 — 加 9 欄位
Phase 2 A0:Domain layer 反映 ORM 擴充。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"目的:entity ↔︎ ORM model 雙向 mapping 包含 9 個新欄位。
Files:
jedi-oscal/jedi_oscal/infra/mapper/ssp/system_implementation_mapper.pyjedi-oscal/tests/test_ssp_system_implementation_extension.pycat ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/mapper/ssp/system_implementation_mapper.py加 class 到 test 檔:
from datetime import date
from jedi_oscal.infra.mapper.ssp.system_implementation_mapper import (
SystemImplementationMapper,
)
class TestMapperRoundTrip:
def test_entity_to_model_includes_new_fields(self):
entity = SspSystemImplementationEntity(
name="test",
implementation_type="hardware",
scope_type="module_frame",
scope_id=1,
device_id=42,
title="Daisy's MacBook",
purpose="Engineering workstation",
status="operational",
)
model = SystemImplementationMapper.to_model(entity)
assert model.scope_type == "module_frame"
assert model.scope_id == 1
assert model.device_id == 42
assert model.title == "Daisy's MacBook"
assert model.purpose == "Engineering workstation"
assert model.status == "operational"
def test_model_to_entity_includes_new_fields(self):
model = OscalSystemSecurityPlanSystemImplementation(
name="test",
implementation_type="leveraged-authorization",
scope_type="ssp",
scope_id=10,
title="AWS GovCloud",
party_uuid="aws-party-uuid",
date_authorized=date(2026, 5, 18),
)
entity = SystemImplementationMapper.to_entity(model)
assert entity.scope_type == "ssp"
assert entity.party_uuid == "aws-party-uuid"
assert entity.date_authorized == date(2026, 5, 18)poetry run pytest tests/test_ssp_system_implementation_extension.py::TestMapperRoundTrip -vExpected: FAIL with AttributeError(mapper 沒 map 新欄位)。
在 to_model 與 to_entity 兩個方法內加 9 個新欄位 mapping(pattern 對齊既有欄位):
@staticmethod
def to_model(entity: SspSystemImplementationEntity) -> OscalSystemSecurityPlanSystemImplementation:
return OscalSystemSecurityPlanSystemImplementation(
# ...既有...
scope_type=entity.scope_type,
scope_id=entity.scope_id,
device_id=entity.device_id,
information_system_id=entity.information_system_id,
title=entity.title,
purpose=entity.purpose,
status=entity.status,
party_uuid=entity.party_uuid,
date_authorized=entity.date_authorized,
)
@staticmethod
def to_entity(model: OscalSystemSecurityPlanSystemImplementation) -> SspSystemImplementationEntity:
return SspSystemImplementationEntity(
# ...既有...
scope_type=model.scope_type,
scope_id=model.scope_id,
device_id=model.device_id,
information_system_id=model.information_system_id,
title=model.title,
purpose=model.purpose,
status=model.status,
party_uuid=model.party_uuid,
date_authorized=model.date_authorized,
)poetry run pytest tests/test_ssp_system_implementation_extension.py::TestMapperRoundTrip -vExpected: 2 passed。
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
git add jedi_oscal/infra/mapper/ssp/system_implementation_mapper.py \
tests/test_ssp_system_implementation_extension.py
git commit -m "$(cat <<'EOF'
feat(ssp-system-impl): Mapper 擴充 — 9 欄位雙向 mapping
Phase 2 A0:entity ↔ ORM round-trip。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"目的:domain repo interface + infra impl 加 3 個 query method(list_by_scope / find_by_device_id / find_by_information_system_id)。
Files:
jedi-oscal/jedi_oscal/domain/repository/ssp/system_implementation_repo.pyjedi-oscal/jedi_oscal/infra/repository/ssp/ssp_system_implementation_repo_impl.pyjedi-oscal/tests/test_ssp_system_implementation_extension.pycat ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/domain/repository/ssp/system_implementation_repo.py
cat ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/repository/ssp/ssp_system_implementation_repo_impl.py看既有
tests/test_ssp_template_module_frame_id.py用什麼 DB fixture 對齊。
替代方案:若 jedi-oscal 沒 DB fixture,這個 task 的 query method 在主專案的 integration test (Task 11) 統一驗證,jedi-oscal 端只測 method 簽章存在:
class TestRepoExtension:
def test_repo_has_list_by_scope_method(self):
from jedi_oscal.infra.repository.ssp.ssp_system_implementation_repo_impl import (
SspSystemImplementationRepoImpl,
)
assert hasattr(SspSystemImplementationRepoImpl, "list_by_scope")
def test_repo_has_find_by_device_id_method(self):
from jedi_oscal.infra.repository.ssp.ssp_system_implementation_repo_impl import (
SspSystemImplementationRepoImpl,
)
assert hasattr(SspSystemImplementationRepoImpl, "find_by_device_id")
def test_repo_has_find_by_information_system_id_method(self):
from jedi_oscal.infra.repository.ssp.ssp_system_implementation_repo_impl import (
SspSystemImplementationRepoImpl,
)
assert hasattr(SspSystemImplementationRepoImpl, "find_by_information_system_id")poetry run pytest tests/test_ssp_system_implementation_extension.py::TestRepoExtension -vExpected: FAIL(method 不存在)。
domain/repository/ssp/system_implementation_repo.py:
from abc import abstractmethod
from typing import List, Optional
class SystemImplementationRepo:
# ...既有 abstract method...
@abstractmethod
def list_by_scope(
self, scope_type: str, scope_id: int
) -> List[SspSystemImplementationEntity]:
"""依 scope_type + scope_id 查全部 row。"""
@abstractmethod
def find_by_device_id(
self, device_id: int
) -> List[SspSystemImplementationEntity]:
"""依 device soft FK 反查。"""
@abstractmethod
def find_by_information_system_id(
self, information_system_id: int
) -> List[SspSystemImplementationEntity]:
"""依 information_system soft FK 反查。"""infra/repository/ssp/ssp_system_implementation_repo_impl.py:
def list_by_scope(self, scope_type: str, scope_id: int):
rows = (
self.session.query(OscalSystemSecurityPlanSystemImplementation)
.filter_by(scope_type=scope_type, scope_id=scope_id)
.all()
)
return [SystemImplementationMapper.to_entity(r) for r in rows]
def find_by_device_id(self, device_id: int):
rows = (
self.session.query(OscalSystemSecurityPlanSystemImplementation)
.filter_by(device_id=device_id)
.all()
)
return [SystemImplementationMapper.to_entity(r) for r in rows]
def find_by_information_system_id(self, information_system_id: int):
rows = (
self.session.query(OscalSystemSecurityPlanSystemImplementation)
.filter_by(information_system_id=information_system_id)
.all()
)
return [SystemImplementationMapper.to_entity(r) for r in rows]注意 session 用
self.sessionlazy property(對齊 CLAUDE.md「Repo 層 session 必須 lazy property」)。
poetry run pytest tests/test_ssp_system_implementation_extension.py::TestRepoExtension -vExpected: 3 passed。
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
git add jedi_oscal/domain/repository/ssp/system_implementation_repo.py \
jedi_oscal/infra/repository/ssp/ssp_system_implementation_repo_impl.py \
tests/test_ssp_system_implementation_extension.py
git commit -m "$(cat <<'EOF'
feat(ssp-system-impl): Repository 加 3 個 query method
Phase 2 A0:list_by_scope / find_by_device_id / find_by_information_system_id
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"目的:app layer DTO 反映 9 個新欄位(給 BE app service 用)。
Files:
jedi-oscal/jedi_oscal/app/dto/ssp/ssp_system_implementation_dto.pyjedi-oscal/tests/test_ssp_system_implementation_extension.pycat ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/app/dto/ssp/ssp_system_implementation_dto.pyclass TestDtoExtension:
def test_dto_accepts_new_fields(self):
from jedi_oscal.app.dto.ssp.ssp_system_implementation_dto import (
SspSystemImplementationDto,
)
dto = SspSystemImplementationDto(
name="test",
implementation_type="hardware",
scope_type="ssp",
scope_id=1,
device_id=42,
title="t",
purpose="p",
status="operational",
)
assert dto.scope_type == "ssp"
assert dto.device_id == 42把 9 個欄位加到 DTO(pattern 對齊既有欄位,注意是 dataclass / pydantic / 自訂 class,視既有 DTO 風格而定)。
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
git add jedi_oscal/app/dto/ssp/ssp_system_implementation_dto.py \
tests/test_ssp_system_implementation_extension.py
git commit -m "$(cat <<'EOF'
feat(ssp-system-impl): DTO 擴充 — 9 欄位
Phase 2 A0:app layer DTO 反映 ORM 擴充。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"目的:依 implementation_type 把 row 翻譯成正確的 OSCAL JSON 區段:
hardware → system-implementation.inventory-items[*]component / software / service / system / subsystem → system-implementation.components[*]leveraged-authorization → system-implementation.leveraged-authorizations[*]Files:
jedi-oscal/jedi_oscal/infra/mapper/ssp/ssp_yaml_mapper.pyjedi-oscal/tests/test_ssp_system_implementation_extension.pycat ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/mapper/ssp/ssp_yaml_mapper.py預期看到 yaml mapper 已組 SSP dict 結構,含 system-implementation 段。
class TestYamlMapperSerialize:
def test_hardware_row_becomes_inventory_item(self):
entity = SspSystemImplementationEntity(
uid=uuid.UUID("aaaa-1111-..."),
name="Daisy's MacBook",
description="Engineering workstation",
implementation_type="hardware",
scope_type="ssp",
scope_id=10,
device_id=42,
)
oscal_dict = SspYamlMapper.serialize_system_implementation_item(entity)
assert "uuid" in oscal_dict
assert oscal_dict["description"] == "Engineering workstation"
# hardware 對 OSCAL inventory-item structure
def test_component_row_becomes_component(self):
entity = SspSystemImplementationEntity(
name="HR System",
implementation_type="component",
scope_type="ssp",
scope_id=10,
information_system_id=7,
title="HR System",
purpose="Personnel management",
status="operational",
)
oscal_dict = SspYamlMapper.serialize_system_implementation_item(entity)
assert oscal_dict["title"] == "HR System"
assert oscal_dict["status"] == "operational"
def test_leveraged_authorization_row_becomes_leveraged_auth(self):
entity = SspSystemImplementationEntity(
name="AWS GovCloud",
implementation_type="leveraged-authorization",
scope_type="ssp",
scope_id=10,
title="AWS GovCloud (FedRAMP High)",
party_uuid="aws-party-uuid",
date_authorized=date(2026, 1, 1),
)
oscal_dict = SspYamlMapper.serialize_system_implementation_item(entity)
assert oscal_dict["title"] == "AWS GovCloud (FedRAMP High)"
assert oscal_dict["party-uuid"] == "aws-party-uuid"
assert oscal_dict["date-authorized"] == "2026-01-01"注意:實際 method 名 / 簽章 view 既有 mapper 後對齊。若既有 mapper 是聚合層級(一次序列化整個 SSP),則 test 改成驗證
serialize_ssp輸出的 dict 結構含三段。
實作骨架:
def serialize_system_implementation_item(entity):
impl_type = entity.implementation_type
common = {
"uuid": str(entity.uid),
"description": entity.description,
}
if impl_type == "hardware":
# OSCAL inventory-item
return {**common, "props": _build_props_for_inventory_item(entity)}
elif impl_type == "leveraged-authorization":
# OSCAL leveraged-authorization
return {
**common,
"title": entity.title,
"party-uuid": entity.party_uuid,
"date-authorized": entity.date_authorized.isoformat() if entity.date_authorized else None,
}
else:
# default: OSCAL component (covers system/subsystem/service/component/software)
return {
**common,
"type": impl_type,
"title": entity.title or entity.name,
"purpose": entity.purpose,
"status": {"state": entity.status} if entity.status else None,
}細節參考 OSCAL SSP schema (https://pages.nist.gov/OSCAL/reference/latest/system-security-plan/json-outline/)。
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
git add jedi_oscal/infra/mapper/ssp/ssp_yaml_mapper.py \
tests/test_ssp_system_implementation_extension.py
git commit -m "$(cat <<'EOF'
feat(ssp-system-impl): YAML mapper 三分支序列化
Phase 2 A0:依 implementation_type 翻譯成 OSCAL inventory-items / components /
leveraged-authorizations 三種結構。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"目的:確認 schema + ORM 擴充後,既有 caller (project_device_mapping_service + ssp_versioning_service) 不破。
Files:
compliance-manager-be/tests/test_ssp_system_implementation_regression.pygrep -rn "system_security_plan_id\|SystemImplementation\|ssp_system_impl" \
~/Projects/Billows/Audit-Manager/compliance-manager-be/tests/ 2>/dev/null | head -20cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
poetry run pytest tests/ -k "ssp_versioning or system_implementation or device_mapping" -vExpected: 既有測試全綠(schema 擴充 + nullable 改動不影響既有寫入流程)。
Create tests/test_ssp_system_implementation_regression.py:
"""A0 regression:既有 caller 在 schema 擴充後仍能正常寫入。"""
import pytest
class TestSchemaExtensionRegression:
"""新欄位 nullable + 既有欄位 nullable 化後,既有寫入流程不破。"""
def test_legacy_hardware_write_still_works(self, client, headers, app):
"""模擬既有 project_device_mapping_service 寫入 hardware row。
若 ORM 對舊 caller 仍可用,這個測試應該綠。"""
from jedi_oscal.domain.entity.ssp.ssp_system_implementation_entity import (
SspSystemImplementationEntity,
)
from jedi_oscal.infra.mapper.ssp.system_implementation_mapper import (
SystemImplementationMapper,
)
# 用既有 caller 寫法(不填新欄位)
entity = SspSystemImplementationEntity(
name="legacy device",
description="legacy",
implementation_type="hardware",
scope_type="ssp",
scope_id=1,
# 不填 device_id / title / purpose 等新欄位 → 全部 default None
)
model = SystemImplementationMapper.to_model(entity)
assert model.scope_type == "ssp"
assert model.device_id is None
assert model.title is None這是 smoke test — 重點不是新欄位 CRUD(jedi-oscal 端已測),是「沒填新欄位也能 mapping」。
poetry run pytest tests/test_ssp_system_implementation_regression.py -vExpected: PASS。
啟動 BE,確認 schema 擴充後啟動不破:
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
# 若已有 BE 跑著,先 kill -9(memory: "Backend restart 必 kill -9")
lsof -ti:8000 | xargs kill -9 2>/dev/null
set -a; source .env; set +a
poetry run python main_app.py &
BE_PID=$!
sleep 5
# 看 log
tail -30 log/app.log
# 健康檢查
curl -s http://localhost:8000/healthz | head -5
# 關掉
kill -9 $BE_PID 2>/dev/nullExpected: BE 順利啟動、log/app.log 無 schema mismatch / migration 相關錯誤。
若啟動失敗:root cause 必查(不 ignore),通常是 ORM 欄位與 DB 不對齊。
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
git add tests/test_ssp_system_implementation_regression.py
git commit -m "$(cat <<'EOF'
tweak(ssp-system-impl): A0 regression smoke test
確認既有 caller 在 schema 擴充後仍能正常寫入(未填新欄位也 OK)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"目的:把 A0 ship 紀錄完整歸檔。
Files:
compliance-manager-be/docs/changelog/2026-05-18-tweak-ssp-system-impl-extension.mdcompliance-manager-be/docs/features/FR-011.2-2605-ssp-import-export-phase2/README.mdcompliance-manager-be/docs/features/FR-011.2-2605-ssp-import-export-phase2/requirement-understanding.mdCreate docs/changelog/2026-05-18-tweak-ssp-system-impl-extension.md:
---
type: tweak
breaking: false
modules: [oscal, jedi-oscal, system-implementation]
issue: —
commit: <BE commit hash>
---
# Phase 2 A0:擴充 system_security_plan_system_implementations 表
## 變更說明(為什麼)
Phase 2 SSP 匯入匯出需要把「合規資源庫」與「專案 SSP 版本」雙向匯入 / 匯出含 devices / information_systems / leveraged services 等資料。既有 `oscal.system_security_plan_system_implementations` 已是 OSCAL system-implementation 多型鏡像表(`implementation_type` enum + 140 筆 hardware 資料),擴充比新建務實。
## 變更範圍
### DB schema(compliance-manager-be)
- 加 9 個欄位:scope_type / scope_id / device_id / information_system_id / title / purpose / status / party_uuid / date_authorized
- system_security_plan_id 改 nullable(讓 scope_type='module_frame' 可為 null)
- 加 3 個 index(scope / device_id / information_system_id)
- 既有 140 筆 hardware 補 scope_type='ssp' / scope_id
### jedi-oscal 套件
- ORM model + entity + query entity + repo + mapper + DTO + yaml mapper 擴充
- enum 加 `LEVERAGED_AUTHORIZATION = "leveraged-authorization"`
- 新 repo method:list_by_scope / find_by_device_id / find_by_information_system_id
## API 變更
無(A0 是基礎建設,未動 endpoint)。
## 測試結果
- jedi-oscal `tests/test_ssp_system_implementation_extension.py` 全綠
- 主專案 `tests/test_ssp_system_implementation_regression.py` 全綠
- 既有 `project_device_mapping_service` / `ssp_versioning_service` regression 通
## 參考資訊
- design: docs/features/FR-011.2-2605-ssp-import-export-phase2/design.md
- plan: docs/features/FR-011.2-2605-ssp-import-export-phase2/implementation-plan-A0.md
- tracker: docs/features/FR-011.2-2605-ssp-import-export-phase2/README.md把 A0 row 狀態改為 shipped,填 design / plan / ship commit / changelog 連結。
在 §6 Track A 階段表的 A0 row 加 ✅ shipped (commit hash, 2026-05-18) 標記。
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
git add docs/changelog/2026-05-18-tweak-ssp-system-impl-extension.md \
docs/features/FR-011.2-2605-ssp-import-export-phase2/README.md \
docs/features/FR-011.2-2605-ssp-import-export-phase2/requirement-understanding.md
git commit -m "$(cat <<'EOF'
docs(ssp-import-export-phase2): A0 shipped — 更新 changelog + tracker
Phase 2 A0 既有表擴充完成。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"剛 commit 完拿到 hash,回頭把 2026-05-18-tweak-ssp-system-impl-extension.md 內 commit: 改成實際 hash,再 amend 上一個 commit(或新建 chore commit)。
對齊既有慣例:看 git log 內有
chore(changelog): 補 BE commit hash xxxx 到 ...的 follow-up pattern。
目的:A0 完工通知,提醒重啟 BE(CLAUDE.md memory:「改 BE service 後必提醒 user 重啟」)。
動作(無 step checkbox,是給人類執行的通知):
通知 raymond:
A0 完工。改動範圍:
- DB schema migration 已執行(dev DB)
- jedi-oscal 8 個層擴充完成(local commits 在 jedi-oscal repo)
- 主專案 regression test 通過、BE smoke boot 通過
請 restart BE 讓改動生效:
lsof -ti:8000 | xargs kill -9 cd ~/Projects/Billows/Audit-Manager/compliance-manager-be set -a; source .env; set +a nohup poetry run python main_app.py > /dev/null 2>&1 &jedi-oscal 套件目前在 path dependency mode(dev only),等 Phase 2 整體完工後再 bump 版本推 Nexus。
接下來可開工 A1(Excel 樣板)/ B1(Docx 樣板)— 兩條可平行。
| 風險 | 緩解 |
|---|---|
| 既有 caller regression | Task 10 跑既有測試 + smoke boot |
| 140 筆資料 migration 出錯 | Migration SQL 包 transaction;Task 3 Step 4 SELECT count 對帳 |
| ORM 欄位與 DB 不對齊 → BE boot 失敗 | Task 3 完成才開 Task 4(schema 先擴;ORM 後跟) |
| jedi-oscal 改後既有 method 簽章衝突 | TDD 每 task 先跑既有測試,破了立刻發現 |
responsible_party varchar 資料遷移 → 最後統整優化清單