For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan. Steps use checkbox (
- [ ]) syntax.
Goal:把 A0 ship 的 system_security_plan_system_implementations 表結構正確化 — rename 成 ssp_system_implementation_items、補上 OSCAL system-implementation block 主檔 ssp_system_implementations、補上 inventory-item ↔︎ component M:N join 表、改 column information_system_id 為 system_characteristic_id。
Architecture:DDD 嚴格分層擴充 — 1 個 SQL migration 完成所有 schema change(含 rename / new tables / column rename / backfill);jedi-oscal 套件 ORM rename + 新 main/join 完整 stack;主 BE caller 改寫;既有 A0 32 個測試 fix。dev 期續 poetry path dep,feature 完工才推 Nexus。
Tech Stack:Python 3.11 / SQLAlchemy 2.0 / PostgreSQL / pytest / poetry path dep / jedi-oscal local
Spec:design-A0.1.md 前置:A0 已 ship(commits c4804d7 / a1b80cc / e73b1aa / 60a4a2e / 5d8626e) Branch:feature/ssp-import-export-phase2
git add <file1> <file2>,禁用 -am / -Agit status --short 確認 staged 區只有自己 add 的檔案Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>cmmgr| 動作 | 路徑 |
|---|---|
| 已有(不動) | scripts/sql/2026-05-19-ssp-system-implementation-restructure.sql ★ user 已自行執行 |
| Modify | app/oscal/service/ssp_versioning_service.py(既有 caller,import + clone main 流程) |
| Modify | app/associations/service/project_device_mapping_service.py(既有 caller,upsert main 前置) |
| Modify | di_containers/oscal/oscal_containers.py(wiring main + join service) |
| Modify | tests/test_ssp_system_implementation_regression.py(A0 regression 更名) |
| Create | tests/test_ssp_system_implementation_main_regression.py(main upsert smoke) |
| Create | docs/changelog/2026-05-19-tweak-ssp-system-impl-restructure.md |
Rename 既有檔(class + 檔名改): | 既有 | 改後 | |------|-----| | 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 (class SystemImplementationMapper) | .../system_implementation_item_mapper.py (class SspSystemImplementationItemMapper) | | 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 (class SystemImplementationDomainService) | .../system_implementation_item_domain_service.py (class SspSystemImplementationItemDomainService) |
Modify(父層引用更新):
infra/model/ssp/ssp.py(relationship system_implementations)infra/mapper/ssp/system_security_plan_mapper.pyinfra/mapper/ssp/ssp_yaml_mapper.py(核心改動:從 main 出發 + 三分支 + implemented-components)domain/entity/ssp/ssp_entity.pyapp/dto/ssp/ssp_dto.pyinfra/repository/ssp/__init__.py(export 改 class 名)domain/repository/ssp/__init__.pydomain/services/ssp/__init__.pyCreate 新檔(main 完整 stack — 8 個):
infra/model/ssp/ssp_system_implementation.py(class OscalSspSystemImplementation,main table ORM)domain/entity/ssp/ssp_system_implementation_entity.py(class SspSystemImplementationEntity,main entity)domain/entity/ssp/ssp_system_implementation_query_entity.pydomain/repository/ssp/system_implementation_repo.py(class ISspSystemImplementationRepo)infra/repository/ssp/ssp_system_implementation_repo_impl.py(class SspSystemImplementationRepoImpl)infra/mapper/ssp/system_implementation_mapper.py(class SspSystemImplementationMapper)app/dto/ssp/ssp_system_implementation_dto.py(class SspSystemImplementationDTO)domain/services/ssp/system_implementation_domain_service.py(class SspSystemImplementationDomainService,含 upsert_by_scope)Create 新檔(join 完整 stack — 8 個):
infra/model/ssp/ssp_inventory_item_component.py(class OscalSspInventoryItemComponent)domain/entity/ssp/ssp_inventory_item_component_entity.pydomain/entity/ssp/ssp_inventory_item_component_query_entity.pydomain/repository/ssp/inventory_item_component_repo.pyinfra/repository/ssp/ssp_inventory_item_component_repo_impl.pyinfra/mapper/ssp/inventory_item_component_mapper.pyapp/dto/ssp/ssp_inventory_item_component_dto.pydomain/services/ssp/inventory_item_component_domain_service.pyModify 測試:
tests/test_ssp_system_implementation_extension.py(A0 32 個測試 — import / class 名 / column 名)Create 新測試:
tests/test_ssp_system_implementation_main.py(main 表 CRUD + UNIQUE + upsert_by_scope)tests/test_ssp_inventory_item_component.py(join 表 CRUD + M:N relationship)開工前確認:
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev <<'EOF'
\dt oscal.ssp_system_implementations
\dt oscal.ssp_system_implementation_items
\dt oscal.ssp_inventory_item_components
SELECT COUNT(*) FROM oscal.ssp_system_implementation_items WHERE system_implementation_id IS NULL;
EOFExpected: 3 個表都存在、items.system_implementation_id 全部不 null。
若 SQL 未執行,STOP 並通知 user 先跑 scripts/sql/2026-05-19-ssp-system-implementation-restructure.sql。
grep "jedi-oscal" ~/Projects/Billows/Audit-Manager/compliance-manager-be/pyproject.tomlExpected: 看到 jedi-oscal = { path = "...", develop = true } active。若否,先 uncomment 並 poetry update jedi-oscal。
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be && git branch --show-currentExpected: feature/ssp-import-export-phase2
ls ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/model/ssp/ssp_system_implementation.py
ls ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/domain/entity/ssp/ssp_system_implementation_entity.py
ls ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/mapper/ssp/system_implementation_mapper.py
ls ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/app/dto/ssp/ssp_system_implementation_dto.pyExpected: 全部存在。
目的:把 items 表對應的 8 個 ORM/entity/repo/mapper/dto/domain_service 檔案 + class 名 rename 到 *_item* / Ssp*Item*。
注意:先做檔名 rename + class rename,不改 column — column rename 在 Task 3 做(拆分減少單 commit 範圍)。
mv ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/model/ssp/ssp_system_implementation.py ssp_system_implementation_items.py
編輯內容:
class OscalSystemSecurityPlanSystemImplementation → class OscalSspSystemImplementationItem__tablename__ = "system_security_plan_system_implementations" → __tablename__ = "ssp_system_implementation_items"__table_args__ index 名同步:ix_ssp_sys_impl_* → ix_ssp_si_items_*cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
poetry run python -c "from jedi_oscal.infra.model.ssp.ssp_system_implementation_items import OscalSspSystemImplementationItem; print('OK')"Expected: OK。
對每個檔案重複類似步驟:
mv 改檔名__init__.py export詳細 class rename 對照見 design-A0.1.md §4.1。
更新 4 個父層檔案內的 import / class 名引用:
from jedi_oscal.infra.model.ssp.ssp_system_implementation import OscalSystemSecurityPlanSystemImplementation → ... ssp_system_implementation_items import OscalSspSystemImplementationItemSystemImplementationEntity → SspSystemImplementationItemEntity(依此類推所有 class 名)cd ~/Projects/Jedicogy/module/jedi-python-package
git add jedi-oscal/jedi_oscal/infra/model/ssp/ \
jedi-oscal/jedi_oscal/domain/entity/ssp/ \
jedi-oscal/jedi_oscal/domain/repository/ssp/ \
jedi-oscal/jedi_oscal/infra/repository/ssp/ \
jedi-oscal/jedi_oscal/infra/mapper/ssp/ \
jedi-oscal/jedi_oscal/app/dto/ssp/ \
jedi-oscal/jedi_oscal/domain/services/ssp/ \
jedi-oscal/tests/test_ssp_system_implementation_extension.py
git commit -m "$(cat <<'EOF'
refactor(ssp-system-impl): rename items stack to Ssp* + _items 後綴
Phase 2 A0.1:既有 items 表 ORM + entity + repo + mapper + DTO + domain service
全部 rename 對齊新表名 ssp_system_implementation_items。
功能不變、所有測試綠。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"目的:建立 oscal.ssp_system_implementations main 表對應的 DDD 完整 stack。
import uuid
from typing import Optional
from datetime import datetime
from sqlalchemy import String, Text, UUID, DateTime, Integer, JSON, UniqueConstraint, Index, func
from sqlalchemy.orm import Mapped, mapped_column
from jedi_common.session.database.declarative_base import Base
class OscalSspSystemImplementation(Base):
"""OSCAL system-implementation block 主檔,1:1 per (scope_type, scope_id)。
對應 OSCAL spec system-implementation block(無 UUID,但 DB 規範保留 uid,
OSCAL 匯出時 mapper 排除 uid)。
"""
__tablename__ = "ssp_system_implementations"
__table_args__ = (
UniqueConstraint("scope_type", "scope_id", name="uq_ssp_system_implementations_scope"),
Index("ix_ssp_system_implementations_scope", "scope_type", "scope_id"),
{"schema": "oscal"},
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
uid: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
scope_type: Mapped[str] = mapped_column(String(20), nullable=False,
comment="'ssp' or 'module_frame'")
scope_id: Mapped[int] = mapped_column(Integer, nullable=False,
comment="對應 scope_type 指向的 id (soft FK)")
remarks: Mapped[Optional[str]] = mapped_column(Text)
props_jsonb: Mapped[Optional[dict]] = mapped_column(JSON)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=func.now(), nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=func.now(), onupdate=func.now(), nullable=False)
created_user: Mapped[Optional[str]] = mapped_column(String(50))
updated_user: Mapped[Optional[str]] = mapped_column(String(50))依 pattern 建立。Domain service 需含特殊方法 upsert_by_scope:
def upsert_by_scope(self, scope_type: str, scope_id: int, **kwargs) -> SspSystemImplementationEntity:
"""找 (scope_type, scope_id) 對應 main row;不存在就建。"""
existing = self._repo.find_by_scope(scope_type, scope_id)
if existing:
return existing
new_entity = SspSystemImplementationEntity(scope_type=scope_type, scope_id=scope_id, **kwargs)
return self._repo.add(new_entity)def test_main_table_exists():
from jedi_oscal.infra.model.ssp.ssp_system_implementation import OscalSspSystemImplementation
assert OscalSspSystemImplementation.__tablename__ == "ssp_system_implementations"
def test_main_entity_has_required_attrs():
from jedi_oscal.domain.entity.ssp.ssp_system_implementation_entity import SspSystemImplementationEntity
e = SspSystemImplementationEntity(scope_type="ssp", scope_id=42, remarks="test")
assert e.scope_type == "ssp" and e.scope_id == 42
def test_main_mapper_round_trip():
# to_entity + to_model round-trip
...
def test_main_repo_has_find_by_scope_and_upsert():
from jedi_oscal.infra.repository.ssp.ssp_system_implementation_repo_impl import SspSystemImplementationRepoImpl
assert hasattr(SspSystemImplementationRepoImpl, "find_by_scope")
def test_main_domain_service_upsert_by_scope():
# 確認 upsert 行為(mock repo)
...git add jedi-oscal/jedi_oscal/infra/model/ssp/ssp_system_implementation.py \
jedi-oscal/jedi_oscal/domain/entity/ssp/ssp_system_implementation_entity.py \
jedi-oscal/jedi_oscal/domain/entity/ssp/ssp_system_implementation_query_entity.py \
jedi-oscal/jedi_oscal/domain/repository/ssp/system_implementation_repo.py \
jedi-oscal/jedi_oscal/infra/repository/ssp/ssp_system_implementation_repo_impl.py \
jedi-oscal/jedi_oscal/infra/mapper/ssp/system_implementation_mapper.py \
jedi-oscal/jedi_oscal/app/dto/ssp/ssp_system_implementation_dto.py \
jedi-oscal/jedi_oscal/domain/services/ssp/system_implementation_domain_service.py \
jedi-oscal/tests/test_ssp_system_implementation_main.py
git commit -m "$(cat <<'EOF'
feat(ssp-system-impl): 新增 main 表 OscalSspSystemImplementation 完整 stack
Phase 2 A0.1:OSCAL system-implementation block 鏡像主檔 (1:1 per scope)。
含 ORM + entity + repo + mapper + DTO + domain service(含 upsert_by_scope)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"目的:把 items 表的 information_system_id column 在 ORM / entity / repo / mapper / DTO 全層 rename 為 system_characteristic_id。
注意:DB schema 已在 SQL migration rename,這 task 只動 jedi-oscal 程式碼層。
jedi_oscal/infra/model/ssp/ssp_system_implementation_items.py:
# 將
information_system_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, ...)
# 改成
system_characteristic_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True,
comment="soft FK → oscal.system_security_plans_system_characteristics.id")ssp_system_implementation_item_entity.py:
# Constructor 參數 + self.X 賦值
information_system_id → system_characteristic_idsystem_implementation_item_mapper.py 內 to_entity 跟 to_model 兩個 method 內所有 information_system_id 字串改 system_characteristic_id。
SspSystemImplementationItemRepoImpl + interface:
find_by_information_system_id(information_system_id) → find_by_system_characteristic_id(system_characteristic_id)tests/test_ssp_system_implementation_extension.py:搜尋 information_system_id 全部換 system_characteristic_id。
cd jedi-oscal && poetry run pytest tests/test_ssp_system_implementation_extension.py -vExpected: 32 passed。
git add jedi-oscal/jedi_oscal/infra/model/ssp/ssp_system_implementation_items.py \
jedi-oscal/jedi_oscal/domain/entity/ssp/ssp_system_implementation_item_entity.py \
jedi-oscal/jedi_oscal/domain/entity/ssp/ssp_system_implementation_item_query_entity.py \
jedi-oscal/jedi_oscal/infra/mapper/ssp/system_implementation_item_mapper.py \
jedi-oscal/jedi_oscal/app/dto/ssp/ssp_system_implementation_item_dto.py \
jedi-oscal/jedi_oscal/infra/repository/ssp/ssp_system_implementation_item_repo_impl.py \
jedi-oscal/jedi_oscal/domain/repository/ssp/system_implementation_item_repo.py \
jedi-oscal/tests/test_ssp_system_implementation_extension.py
git commit -m "refactor(ssp-system-impl): items column rename information_system_id → system_characteristic_id ..."system_implementation_id FK + relationship目的:items ORM 加 FK column + relationship 到 main。
# ssp_system_implementation_items.py
system_implementation_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("oscal.ssp_system_implementations.id", ondelete="CASCADE"),
nullable=False,
comment="FK → ssp_system_implementations.id"
)
system_implementation: Mapped["OscalSspSystemImplementation"] = relationship(...)Tests 內既有測試構造 OscalSspSystemImplementationItem(...) 時要補 system_implementation_id(用 fixture 先建 main row)。
目的:建 oscal.ssp_inventory_item_components M:N join 表的 8 層 stack。
[Step 1~9 同 Task 2 pattern,建立 ORM + entity + repo + mapper + DTO + domain service + 測試]
詳細 column 規格見 design-A0.1.md §3.3。
關鍵:domain service 內加 application 層驗證:
def add_link(self, inventory_item_id: int, component_id: int):
inv = self._items_repo.get_by_id(inventory_item_id)
comp = self._items_repo.get_by_id(component_id)
if inv.implementation_type != "hardware":
raise BadRequestError("inventory_item_id 必須指向 type=hardware")
if comp.implementation_type not in ("component", "system", "subsystem", "service", "software"):
raise BadRequestError("component_id 必須指向 type IN component family")
# ... add link目的:ssp_yaml_mapper.py 從 main 出發、items 三分支、加 implemented-components join 輸出。
def test_yaml_mapper_uses_main_block_level_fields():
# main 有 remarks → output 含 "remarks"
...
def test_yaml_mapper_inventory_item_implemented_components():
# join 表有資料 → inventory-item 輸出含 "implemented-components": [...]
...@staticmethod
def _system_implementation_to_dict(main_entity, items_list, join_links) -> Dict:
out = {}
if main_entity:
if main_entity.remarks: out["remarks"] = main_entity.remarks
if main_entity.props_jsonb: out["props"] = main_entity.props_jsonb
# 三分支 items(既有邏輯)
...
# implemented-components 從 join_links 填入
...目的:兩個既有 caller 改用新 class + main upsert pattern。
project_device_mapping_service.pymain = self._main_domain_service.upsert_by_scope(scope_type="ssp", scope_id=int(ssp_id))
item_entity = SspSystemImplementationItemEntity(
system_implementation_id=main.id, # ← 新
system_security_plan_id=int(ssp_id),
scope_type="ssp",
scope_id=int(ssp_id),
name=device_entity.name,
description=device_entity.description,
implementation_type=SystemImplementationType.HARDWARE,
)ssp_versioning_service.pydef _clone_system_implementations(self, session, old_ssp, new_ssp, curr_user):
# 1. 先 clone main (1 row)
old_main = session.query(OscalSspSystemImplementation).filter_by(
scope_type="ssp", scope_id=old_ssp.id
).first()
if old_main:
new_main = OscalSspSystemImplementation(
uid=uuid.uuid4(),
scope_type="ssp",
scope_id=new_ssp.id,
remarks=old_main.remarks,
props_jsonb=old_main.props_jsonb,
created_user=curr_user,
updated_user=curr_user,
)
session.add(new_main)
session.flush()
# 2. clone items with new main_id
for old_item in old_main.items: # via relationship
new_item = OscalSspSystemImplementationItem(
system_implementation_id=new_main.id,
...
)
session.add(new_item)
session.flush()di_containers/oscal/oscal_containers.py:
tests/test_ssp_system_implementation_regression.py:所有 import / class 名 / column 名同步改。
tests/test_ssp_system_implementation_main_regression.py:
def test_caller_upsert_main_idempotent():
# 重複 upsert 同 scope 不會建多筆 main
...poetry run pytest tests/test_ssp_system_implementation_regression.py tests/test_ssp_system_implementation_main_regression.py -vExpected: All pass。
lsof -ti:8000 | xargs kill -9 2>/dev/null
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
set -a; source .env; set +a
nohup poetry run python main_app.py > /tmp/be_boot_a01.log 2>&1 &
sleep 8
tail -50 log/app.logExpected: 無 schema mismatch / IntegrityError。
# manual API call or pytest integration test
# 確認 main + item 雙 row 寫入
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev <<'EOF'
SELECT COUNT(*) FROM oscal.ssp_system_implementations;
SELECT COUNT(*) FROM oscal.ssp_system_implementation_items WHERE system_implementation_id IS NOT NULL;
EOFA0.1 follow-up shipped提供 user 通知文字:
A0.1 完工。改動範圍:
- SQL schema rename + 新建 main / join 表(user 已執行)
- jedi-oscal 全 stack rename + 新 main/join + column rename
- 主 BE 兩個 caller 改寫
- 測試全綠
請 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 rename 破壞既有 import path | Task 1 完成立刻跑 32 個既有測試,破了立即發現 |
| caller upsert main 漏處理 → INSERT items 失敗(NOT NULL system_implementation_id) | Task 8 smoke 階段先測 |
| yaml_mapper 改簽章破壞其他 caller | grep caller 確認;Task 6 開頭先盤點 |
| 既有 32 個測試大量 fix 引入新 bug | TDD 每 task 跑全測試確認 |
| jedi-oscal 父層引用沒改全(ssp.py / mapper 父層) | Task 1 Step 9 顯式列 4 個父層檔案 |
system_characteristics 改 1:1(未來評估)system-implementation.users[] 表(客戶有需求才做)hardware enum 改 inventory-item(最後統整優化清單)docs/conversation-history/2026-05-19/ssp-import-export-phase2-A0.1/SUMMARY.mdgit status --short 確認 staged 區乾淨