Phase A0 Implementation Plan — 既有 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

Specdesign.md(A0 SDD 收斂 10 個 brainstorm 問題)

Branchfeature/ssp-import-export-phase2(已 checkout)

跨 repo 範圍

  • compliance-manager-be(主):SQL migration + 觸 jedi-oscal path dep + 整合測試
  • jedi-oscal 套件:ORM / entity / repo / mapper / DTO / enum / yaml_mapper

§1

全域實作規範(每個 task 都要遵守)

  1. TDD:每個有可測 unit 的 task — 先寫 failing test → 確認 fail → 實作最小可過 → 確認 pass → commit
  2. 顯式 git add:每 commit 用 git add <file1> <file2>禁用 -am / -A(避免 sweep 不相關修改)
  3. CLAUDE.md commit 規範:commit message 含 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  4. 不自動 publish jedi-oscal:所有套件 commit 留在 local,feature 完工再正式 bump 版本(CLAUDE.md「jedi-* 進版流程」)
  5. 不下 poetry lock:用 poetry update jedi-oscal(CLAUDE.md「依賴更新一律 poetry update」)
  6. SQL migration 用 cmmgr(不用 cm_app,避 RLS)
  7. 改 BE service 後提醒 user 重啟(BE 沒 hot reload)

§2

File Structure

compliance-manager-be(主專案)

動作 路徑 說明
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 標記

jedi-oscal 套件(~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/

動作 路徑 說明
Modify jedi_oscal/common/enum/code_enum.py SystemImplementationTypeLEVERAGED_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 整合測試

§3

Task 0: Pre-Flight Verification

開工前驗證所有 design.md §9 假設仍成立 — 從 brainstorm 到實作中間環境可能已變。

Files: (無,只是 verify 動作)

head -90 ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/model/ssp/ssp_system_implementation.py

Expected: 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.py

Expected: 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.py

Expected: 兩個檔都存在。

grep "jedi-oscal" ~/Projects/Billows/Audit-Manager/compliance-manager-be/pyproject.toml

Expected: "jedi-oscal==0.0.16"(active 行)+ commented out path line。若版本已不是 0.0.16,記錄當前版本後繼續。

cd ~/Projects/Billows/Audit-Manager/compliance-manager-be && git branch --show-current

Expected: feature/ssp-import-export-phase2


§4

Task 1: 切換 jedi-oscal 為 Poetry Path Dependency

目的:dev 期改套件源碼直接生效,避免每改 bump 版本(CLAUDE.md「改套件先走 poetry path dependency」)。

Files:

  • Modify: 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-oscal

Expected: 安裝完成,無錯誤。

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。


§5

Task 2: jedi-oscal Enum 擴充

目的:加 LEVERAGED_AUTHORIZATION = "leveraged-authorization" 一個值。

Files:

  • Modify: jedi-oscal/jedi_oscal/common/enum/code_enum.py
  • Test: jedi-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 -v

Expected: 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 -v

Expected: 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
)"

§6

Task 3: SQL Migration(DB Schema + 140 筆資料)

目的:DB schema 先擴充才能讓後續 ORM / mapper 整合測試在實際 DB 上跑通。

Files:

  • Create: compliance-manager-be/scripts/sql/2026-05-18-extend-ssp-system-implementations.sql

Create 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.sql

Expected: 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;
EOF

Expected:

  • \d 輸出含 scope_type / scope_id / device_id / information_system_id / title / purpose / status / party_uuid / date_authorized 9 個新欄位
  • system_security_plan_id 行顯示「可空」/「nullable」
  • 3 個新 index 列在 indexes 段
  • scope_type='ssp', count=140
cd ~/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
)"

§7

Task 4: jedi-oscal ORM Model 擴充

目的:把 §3 SQL 加的 9 欄位反映到 SQLAlchemy ORM;改 system_security_plan_id nullable;補 index 宣告。

Files:

  • Modify: jedi-oscal/jedi_oscal/infra/model/ssp/ssp_system_implementation.py
  • Test: jedi-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 False
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
poetry run pytest tests/test_ssp_system_implementation_extension.py::TestOrmExtension -v

Expected: 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 已有 StringTextUUIDForeignKeyDateTimefuncIndex,按需要保留)

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

Expected: 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
)"

§8

Task 5: jedi-oscal Entity + Query Entity 擴充

目的:DDD domain entity 反映 9 個新欄位;Query entity 支援新篩選欄位。

Files:

  • Modify: jedi-oscal/jedi_oscal/domain/entity/ssp/ssp_system_implementation_entity.py
  • Modify: jedi-oscal/jedi_oscal/domain/entity/ssp/ssp_system_implementation_query_entity.py
  • Test: jedi-oscal/tests/test_ssp_system_implementation_extension.py
head -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 == 42
poetry run pytest tests/test_ssp_system_implementation_extension.py::TestEntityExtension tests/test_ssp_system_implementation_extension.py::TestQueryEntityExtension -v

Expected: 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_authorized

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

Expected: 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
)"

§9

Task 6: jedi-oscal Mapper 擴充(Entity ↔︎ Model Round-Trip)

目的:entity ↔︎ ORM model 雙向 mapping 包含 9 個新欄位。

Files:

  • Modify: jedi-oscal/jedi_oscal/infra/mapper/ssp/system_implementation_mapper.py
  • Test: jedi-oscal/tests/test_ssp_system_implementation_extension.py
cat ~/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 -v

Expected: FAIL with AttributeError(mapper 沒 map 新欄位)。

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

Expected: 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
)"

§10

Task 7: jedi-oscal Repository 擴充(3 個新 Query Method)

目的:domain repo interface + infra impl 加 3 個 query method(list_by_scope / find_by_device_id / find_by_information_system_id)。

Files:

  • Modify: jedi-oscal/jedi_oscal/domain/repository/ssp/system_implementation_repo.py
  • Modify: jedi-oscal/jedi_oscal/infra/repository/ssp/ssp_system_implementation_repo_impl.py
  • Test: jedi-oscal/tests/test_ssp_system_implementation_extension.py
cat ~/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 -v

Expected: 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.session lazy property(對齊 CLAUDE.md「Repo 層 session 必須 lazy property」)。

poetry run pytest tests/test_ssp_system_implementation_extension.py::TestRepoExtension -v

Expected: 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
)"

§11

Task 8: jedi-oscal DTO 擴充

目的:app layer DTO 反映 9 個新欄位(給 BE app service 用)。

Files:

  • Modify: jedi-oscal/jedi_oscal/app/dto/ssp/ssp_system_implementation_dto.py
  • Test: jedi-oscal/tests/test_ssp_system_implementation_extension.py
cat ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/app/dto/ssp/ssp_system_implementation_dto.py
class 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
)"

§12

Task 9: jedi-oscal YAML Mapper(OSCAL 序列化)三分支擴充

目的:依 implementation_type 把 row 翻譯成正確的 OSCAL JSON 區段:

  • hardwaresystem-implementation.inventory-items[*]
  • component / software / service / system / subsystemsystem-implementation.components[*]
  • leveraged-authorizationsystem-implementation.leveraged-authorizations[*]

Files:

  • Modify: jedi-oscal/jedi_oscal/infra/mapper/ssp/ssp_yaml_mapper.py
  • Test: jedi-oscal/tests/test_ssp_system_implementation_extension.py
cat ~/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
)"

§13

Task 10: 既有 Caller Regression 驗證

目的:確認 schema + ORM 擴充後,既有 caller (project_device_mapping_service + ssp_versioning_service) 不破。

Files:

  • Create: compliance-manager-be/tests/test_ssp_system_implementation_regression.py
grep -rn "system_security_plan_id\|SystemImplementation\|ssp_system_impl" \
    ~/Projects/Billows/Audit-Manager/compliance-manager-be/tests/ 2>/dev/null | head -20
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
poetry run pytest tests/ -k "ssp_versioning or system_implementation or device_mapping" -v

Expected: 既有測試全綠(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 -v

Expected: 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/null

Expected: 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
)"

§14

Task 11: Changelog + Tracker 更新

目的:把 A0 ship 紀錄完整歸檔。

Files:

  • Create: compliance-manager-be/docs/changelog/2026-05-18-tweak-ssp-system-impl-extension.md
  • Modify: compliance-manager-be/docs/features/FR-011.2-2605-ssp-import-export-phase2/README.md
  • Modify: compliance-manager-be/docs/features/FR-011.2-2605-ssp-import-export-phase2/requirement-understanding.md

Create 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.mdcommit: 改成實際 hash,再 amend 上一個 commit(或新建 chore commit)。

對齊既有慣例:看 git log 內有 chore(changelog): 補 BE commit hash xxxx 到 ... 的 follow-up pattern。


§15

Task 12: 提醒 raymond 重啟 BE + 收口

目的: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 樣板)— 兩條可平行。


§16

Acceptance Criteria(A0 整體驗收,對齊 design.md §10)


§17

風險與緩解(從 design.md §8 帶入)

風險 緩解
既有 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 先跑既有測試,破了立刻發現

§18

不在 A0 範圍(提醒實作者,避免 scope creep)

  • ❌ Excel parser / 匯入流程 → A2
  • ❌ 鉤稽演算法 → A3 / A4
  • ❌ Confirm 寫入 service(含 inline 新建 device)→ A5
  • ❌ docx generator → B1 / B2
  • ❌ OSCAL JSON / XML serialize → B5
  • responsible_party varchar 資料遷移 → 最後統整優化清單
  • ❌ Frontend 改動 → A5 / B6