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: 完成 ssp-oscal-alignment Phase 1 Foundation — jedi-oscal 套件側加 3 entity (Component / LeveragedAuth / InventoryItem) + 對應 repo/service/ORM model/mapper、主專案 DB 建新表 + RLS + 資料 migrate + DROP 舊表、ParsedExcelEntityBundle dataclass 重設計。
Architecture: 套件側採 3 PR per entity 縱切片,ordering LeveragedAuth → Component → InventoryItem(FK 依賴)+ 最後 1 個 cleanup PR 刪舊 entity。主專案 4 個連續 SQL migration script(建表 → migrate → verification → DROP),dev 階段直接 cutover,不走漸進 deprecation。Phase 1 + Phase 2 合併 deploy unit(avoiding schema_version 中間態),但本 plan 只涵蓋 Phase 1。
Tech Stack: Python 3 / SQLAlchemy (ORM model + mapper) / pytest (套件 + 主專案測試) / PostgreSQL 14+ (DDL + RLS policy) / Poetry (path dependency for dev) / Flask (existing main project)
Spec: docs/features/FR-028-2605-ssp-oscal-alignment/design.md v1.1 Brainstorm 決策紀錄: docs/analysis/2026-05-24-ssp-oscal-alignment-phase1-brainstorm.md
~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/)domain/entity/base/oscal_leveraged_authorization_entity.py — 新domain/entity/base/oscal_leveraged_authorization_query_entity.py — 新domain/repository/base/leveraged_authorization.py — 新 (interface)domain/services/base/leveraged_authorization_domain_service.py — 新infra/model/base/oscal_leveraged_authorization.py — 新 (SQLAlchemy ORM)infra/mapper/base/leveraged_authorization_mapper.py — 新infra/repository/base/leveraged_authorization_repo_impl.py — 新tests/test_oscal_leveraged_authorization_entity.py — 新domain/entity/base/oscal_component_entity.py — 新(leveraged_authorization_uid: Optional[str] 欄位)domain/entity/base/oscal_component_query_entity.py — 新domain/repository/base/component.py — 新domain/services/base/component_domain_service.py — 新infra/model/base/oscal_component.py — 新infra/mapper/base/component_mapper.py — 新infra/repository/base/component_repo_impl.py — 新tests/test_oscal_component_entity.py — 新domain/entity/base/oscal_inventory_item_entity.py — 新(implemented_component_uids: list[str] 欄位)domain/entity/base/oscal_inventory_item_query_entity.py — 新domain/repository/base/inventory_item.py — 新domain/services/base/inventory_item_domain_service.py — 新infra/model/base/oscal_inventory_item.py — 新infra/model/base/oscal_inventory_implemented_component.py — 新 (M2M join model)infra/mapper/base/inventory_item_mapper.py — 新infra/repository/base/inventory_item_repo_impl.py — 新tests/test_oscal_inventory_item_entity.py — 新SspSystemImplementationItemEntity + 對應 repo / service / model / mapper(待 Task 11 grep 確認確切路徑)/Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be/)pyproject.toml — 修(path dependency dev-only,不 commit,Task 1 + Task 14)scripts/sql/ssp_oscal_alignment_create_tables.sql — 新(Task 7:建新表 + RLS policy)scripts/sql/ssp_oscal_alignment_migrate_data.sql — 新(Task 8:Step 1+2 寫資料)scripts/sql/ssp_oscal_alignment_verification.sql — 新(Task 9:Step 3 row count assertion)scripts/sql/ssp_oscal_alignment_drop_old.sql — 新(Task 12:Step 4 DROP CASCADE)domain/oscal/parser/ssp_intermediate.py — 修(Task 5:加 ParsedComponent / ParsedLeveragedAuthorization / ParsedInventoryItem + 重組 Bundle)di_containers/oscal/oscal_containers.py — 修(Task 6:wire 3 個新 domain service)tests/test_ssp_intermediate_v2_bundle.py — 新(Task 5 dataclass test)tests/test_ssp_oscal_migration_e2e.py — 新(Task 10:e2e 驗證 Excel + docx import + GET SSP detail roundtrip)Per CLAUDE.md feedback「實作計畫的 method/欄位假設先 verify 才開工」— plan 寫好到開工經常 days/weeks 期間 method 改名 / entity shape / error code 序號被佔。每個 Task 第一個 Step 統一是 verify 假設(grep / read 確認)。
Files:
pyproject.toml(dev-only,不 commit)Context: Per CLAUDE.md「External jedi-* Packages 異動規範」— dev 階段一律走 poetry path dependency,不每改必 bump。
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
git status -sb
git log --oneline -3Expected: branch feature/ssp-oscal-alignment,working tree 乾淨。如果不在這 branch → 停下問 user(禁切 branch)。
找到 [tool.poetry.dependencies] 內 jedi-oscal = ... 那行:
# 改成
jedi-oscal = { path = "/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/jedi-oscal", develop = true }Per CLAUDE.md「依賴更新一律 poetry update」:
cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
poetry update jedi-oscalExpected: jedi-oscal 切到 path mode,no version pin 衝突。
poetry run python -c "from jedi_oscal.domain.entity.base.oscal_party_entity import PartyEntity; print(PartyEntity.__module__)"Expected: jedi_oscal.domain.entity.base.oscal_party_entity
git status
# Expected: pyproject.toml 顯示 modified,但不 stage / 不 commit
# (Task 14 收尾時改回 pin Nexus 版本才 commit)ab66af0..a748205)⚠️ 接手者注意:實際 pattern 對齊既有 jedi-oscal 既有模組(per §11.1 ORM Mapped[], §11.2 UUID, §11.3 repo interface, §11.4 domain service init param, §11.7 mapper signature)。下面的 code 是 plan-time sketch;實際 ship 的 code 在 commits 內,已 spec compliance + code quality review both Approved。
Working directory: ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/
Files:
jedi_oscal/domain/entity/base/oscal_leveraged_authorization_entity.pyjedi_oscal/domain/entity/base/oscal_leveraged_authorization_query_entity.pyjedi_oscal/domain/repository/base/leveraged_authorization.pyjedi_oscal/domain/services/base/leveraged_authorization_domain_service.pyjedi_oscal/infra/model/base/oscal_leveraged_authorization.pyjedi_oscal/infra/mapper/base/leveraged_authorization_mapper.pyjedi_oscal/infra/repository/base/leveraged_authorization_repo_impl.pyjedi_oscal/tests/test_oscal_leveraged_authorization_entity.pycat ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/domain/entity/base/oscal_party_entity.py
cat ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/domain/entity/base/oscal_party_query_entity.py
cat ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/model/base/oscal_party.py
cat ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/mapper/base/party_mapper.py
cat ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/repository/base/party_repo_impl.py
cat ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/domain/services/base/party_domain_service.py對 Phase 1 全 3 entity 的 pattern 都從 party 為 reference。
jedi_oscal/tests/test_oscal_leveraged_authorization_entity.py:
import uuid
from jedi_oscal.domain.entity.base.oscal_leveraged_authorization_entity import (
LeveragedAuthorizationEntity,
)
def test_leveraged_authorization_entity_defaults():
entity = LeveragedAuthorizationEntity(
uid="uid-1",
ssp_id=1,
title="Crowdstrike",
tenant_id=102,
)
assert entity.uid == "uid-1"
assert entity.ssp_id == 1
assert entity.title == "Crowdstrike"
assert entity.tenant_id == 102
assert entity.party_uuid is None
assert entity.date_authorized is None
assert entity.props is None
assert entity.remarks is None
assert entity.is_active is True
def test_leveraged_authorization_entity_with_full_payload():
entity = LeveragedAuthorizationEntity(
uid="uid-2",
ssp_id=2,
title="AWS GovCloud",
party_uuid="party-uid-3",
date_authorized="2024-03-15",
props={
"fedramp_package_id": "FR18078583629",
"impact_level": "moderate",
"data_types": "FCI, System Logs",
},
remarks="initial signoff",
tenant_id=102,
org_unit_id=99,
)
assert entity.props["fedramp_package_id"] == "FR18078583629"
assert entity.props["impact_level"] == "moderate"cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
poetry run pytest jedi_oscal/tests/test_oscal_leveraged_authorization_entity.py -vExpected: FAIL with ModuleNotFoundError: jedi_oscal.domain.entity.base.oscal_leveraged_authorization_entity
jedi_oscal/domain/entity/base/oscal_leveraged_authorization_entity.py:
from typing import Optional
class LeveragedAuthorizationEntity:
"""OSCAL <leveraged-authorization> domain entity.
Represents a FedRAMP-style ATO (Authorization to Operate) that a downstream
SSP inherits from. Pairs with a `ComponentEntity` via
`Component.leveraged_authorization_uid` to express "this component is the
one that was authorized".
"""
def __init__(
self,
id: Optional[int] = None,
uid: Optional[str] = None,
ssp_id: Optional[int] = None,
title: str = "",
party_uuid: Optional[str] = None,
date_authorized: Optional[str] = None,
props: Optional[dict] = None,
remarks: Optional[str] = None,
tenant_id: Optional[int] = None,
org_unit_id: Optional[int] = None,
is_active: bool = True,
created_at=None,
created_user: Optional[str] = None,
updated_at=None,
updated_user: Optional[str] = None,
):
self.id = id
self.uid = uid
self.ssp_id = ssp_id
self.title = title
self.party_uuid = party_uuid
self.date_authorized = date_authorized
self.props = props
self.remarks = remarks
self.tenant_id = tenant_id
self.org_unit_id = org_unit_id
self.is_active = is_active
self.created_at = created_at
self.created_user = created_user
self.updated_at = updated_at
self.updated_user = updated_userpoetry run pytest jedi_oscal/tests/test_oscal_leveraged_authorization_entity.py -vExpected: 2 tests PASS
jedi_oscal/domain/entity/base/oscal_leveraged_authorization_query_entity.py:
from typing import Optional
class LeveragedAuthorizationQueryEntity:
"""Query DTO for LeveragedAuthorizationRepository — filterable fields."""
def __init__(
self,
id: Optional[int] = None,
uid: Optional[str] = None,
ssp_id: Optional[int] = None,
title: Optional[str] = None,
party_uuid: Optional[str] = None,
is_active: Optional[bool] = None,
tenant_id: Optional[int] = None,
):
self.id = id
self.uid = uid
self.ssp_id = ssp_id
self.title = title
self.party_uuid = party_uuid
self.is_active = is_active
self.tenant_id = tenant_idgit add jedi_oscal/domain/entity/base/oscal_leveraged_authorization_entity.py \
jedi_oscal/domain/entity/base/oscal_leveraged_authorization_query_entity.py \
jedi_oscal/tests/test_oscal_leveraged_authorization_entity.py
git commit -m "feat(oscal): add LeveragedAuthorizationEntity + QueryEntity (PR-1 part 1)"jedi_oscal/infra/model/base/oscal_leveraged_authorization.py:
from sqlalchemy import (
Column, Integer, String, Text, Date, Boolean, DateTime, Index, ForeignKey,
)
from sqlalchemy.dialects.postgresql import JSONB
from jedi_oscal.infra.model.base.base_model import Base # adjust import per package convention
class OscalLeveragedAuthorizationModel(Base):
__tablename__ = "ssp_leveraged_authorizations"
__table_args__ = (
Index("idx_ssp_leveraged_ssp", "ssp_id", "is_active"),
{"schema": "oscal"},
)
id = Column(Integer, primary_key=True, autoincrement=True)
uid = Column(String(36), unique=True, nullable=False)
ssp_id = Column(Integer, ForeignKey("oscal.system_security_plans.id", ondelete="CASCADE"), nullable=False)
title = Column(String(255), nullable=False)
party_uuid = Column(String(36), nullable=True)
date_authorized = Column(Date, nullable=True)
props = Column(JSONB, nullable=True)
remarks = Column(Text, nullable=True)
tenant_id = Column(Integer, nullable=False)
org_unit_id = Column(Integer, nullable=True)
is_active = Column(Boolean, nullable=False, default=True)
created_at = Column(DateTime, nullable=True)
created_user = Column(String(255), nullable=True)
updated_at = Column(DateTime, nullable=True)
updated_user = Column(String(255), nullable=True)Verify before writing: cat ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/model/base/oscal_party.py 確認 Base / schema 命名 / import path 跟既有一致。
git add jedi_oscal/infra/model/base/oscal_leveraged_authorization.py
git commit -m "feat(oscal): add OscalLeveragedAuthorizationModel ORM (PR-1 part 2)"cat ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/mapper/base/party_mapper.pyjedi_oscal/infra/mapper/base/leveraged_authorization_mapper.py:
from jedi_oscal.domain.entity.base.oscal_leveraged_authorization_entity import (
LeveragedAuthorizationEntity,
)
from jedi_oscal.infra.model.base.oscal_leveraged_authorization import (
OscalLeveragedAuthorizationModel,
)
class LeveragedAuthorizationMapper:
@staticmethod
def to_entity(model: OscalLeveragedAuthorizationModel) -> LeveragedAuthorizationEntity:
if model is None:
return None
return LeveragedAuthorizationEntity(
id=model.id,
uid=model.uid,
ssp_id=model.ssp_id,
title=model.title,
party_uuid=model.party_uuid,
date_authorized=model.date_authorized.isoformat() if model.date_authorized else None,
props=model.props,
remarks=model.remarks,
tenant_id=model.tenant_id,
org_unit_id=model.org_unit_id,
is_active=model.is_active,
created_at=model.created_at,
created_user=model.created_user,
updated_at=model.updated_at,
updated_user=model.updated_user,
)
@staticmethod
def to_model(entity: LeveragedAuthorizationEntity) -> OscalLeveragedAuthorizationModel:
return OscalLeveragedAuthorizationModel(
id=entity.id,
uid=entity.uid,
ssp_id=entity.ssp_id,
title=entity.title,
party_uuid=entity.party_uuid,
date_authorized=entity.date_authorized,
props=entity.props,
remarks=entity.remarks,
tenant_id=entity.tenant_id,
org_unit_id=entity.org_unit_id,
is_active=entity.is_active,
)git add jedi_oscal/infra/mapper/base/leveraged_authorization_mapper.py
git commit -m "feat(oscal): add LeveragedAuthorizationMapper (PR-1 part 3)"jedi_oscal/domain/repository/base/leveraged_authorization.py:
from abc import ABC, abstractmethod
from typing import List, Optional
from jedi_oscal.domain.entity.base.oscal_leveraged_authorization_entity import (
LeveragedAuthorizationEntity,
)
from jedi_oscal.domain.entity.base.oscal_leveraged_authorization_query_entity import (
LeveragedAuthorizationQueryEntity,
)
class ILeveragedAuthorizationRepository(ABC):
@abstractmethod
def get_one(self, query: LeveragedAuthorizationQueryEntity) -> Optional[LeveragedAuthorizationEntity]:
...
@abstractmethod
def get_list(self, query: LeveragedAuthorizationQueryEntity) -> List[LeveragedAuthorizationEntity]:
...
@abstractmethod
def add(self, entity: LeveragedAuthorizationEntity) -> LeveragedAuthorizationEntity:
...
@abstractmethod
def update(self, entity: LeveragedAuthorizationEntity, locale: Optional[str] = None) -> LeveragedAuthorizationEntity:
...
@abstractmethod
def deactivate(self, uid: str) -> None:
...jedi_oscal/infra/repository/base/leveraged_authorization_repo_impl.py:
from jedi_common.session.database.repository.base_repository_impl import BaseRepositoryImpl
from jedi_oscal.domain.entity.base.oscal_leveraged_authorization_entity import (
LeveragedAuthorizationEntity,
)
from jedi_oscal.domain.entity.base.oscal_leveraged_authorization_query_entity import (
LeveragedAuthorizationQueryEntity,
)
from jedi_oscal.domain.repository.base.leveraged_authorization import (
ILeveragedAuthorizationRepository,
)
from jedi_oscal.infra.model.base.oscal_leveraged_authorization import (
OscalLeveragedAuthorizationModel,
)
from jedi_oscal.infra.mapper.base.leveraged_authorization_mapper import (
LeveragedAuthorizationMapper,
)
class LeveragedAuthorizationRepoImpl(
ILeveragedAuthorizationRepository,
BaseRepositoryImpl[
LeveragedAuthorizationEntity,
LeveragedAuthorizationQueryEntity,
OscalLeveragedAuthorizationModel,
LeveragedAuthorizationMapper,
],
):
def __init__(self):
super().__init__(
mapper=LeveragedAuthorizationMapper,
model=OscalLeveragedAuthorizationModel,
)Verify before writing: cat ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/repository/base/party_repo_impl.py 確認 BaseRepositoryImpl 簽名跟既有一致。
git add jedi_oscal/domain/repository/base/leveraged_authorization.py \
jedi_oscal/infra/repository/base/leveraged_authorization_repo_impl.py
git commit -m "feat(oscal): add LeveragedAuthorization repo interface + impl (PR-1 part 4)"jedi_oscal/domain/services/base/leveraged_authorization_domain_service.py:
from typing import List, Optional
from jedi_oscal.domain.entity.base.oscal_leveraged_authorization_entity import (
LeveragedAuthorizationEntity,
)
from jedi_oscal.domain.entity.base.oscal_leveraged_authorization_query_entity import (
LeveragedAuthorizationQueryEntity,
)
from jedi_oscal.domain.repository.base.leveraged_authorization import (
ILeveragedAuthorizationRepository,
)
class LeveragedAuthorizationDomainService:
def __init__(self, repository: ILeveragedAuthorizationRepository):
self._repo = repository
def get_one(self, query: LeveragedAuthorizationQueryEntity) -> Optional[LeveragedAuthorizationEntity]:
return self._repo.get_one(query)
def get_list(self, query: LeveragedAuthorizationQueryEntity) -> List[LeveragedAuthorizationEntity]:
return self._repo.get_list(query)
def add(self, entity: LeveragedAuthorizationEntity) -> LeveragedAuthorizationEntity:
return self._repo.add(entity)
def update(self, entity: LeveragedAuthorizationEntity, locale: Optional[str] = None) -> LeveragedAuthorizationEntity:
return self._repo.update(entity, locale=locale)
def deactivate(self, uid: str) -> None:
self._repo.deactivate(uid)git add jedi_oscal/domain/services/base/leveraged_authorization_domain_service.py
git commit -m "feat(oscal): add LeveragedAuthorizationDomainService (PR-1 part 5)"cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
poetry run pytest -vExpected: 既有 tests + 新 leveraged entity tests 全綠
cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
poetry run python -c "
from jedi_oscal.domain.entity.base.oscal_leveraged_authorization_entity import LeveragedAuthorizationEntity
from jedi_oscal.domain.services.base.leveraged_authorization_domain_service import LeveragedAuthorizationDomainService
print('OK')
"Expected: OK
⚠️ 不自 push — 套件 branch push 屬於對外可見動作(per CLAUDE.md「push 永遠要 user 明確指示」),停下問 user。
157d2bc..29b3bf0)⚠️ 接手者注意:同 Task 2 pattern alignment(§11.1-§11.7)。額外:COMPONENT_TYPE_ENUM 15 個 + COMPONENT_STATUS_ENUM 5 個 module-level constants;partial index idx_ssp_components_lev 用 postgresql_where=text(...);leveraged_authorization_uid 是 soft FK(無 ForeignKey constraint)。
Working directory: ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/
Files: mirror Task 2 file structure, 把 leveraged_authorization → component,加 leveraged_authorization_uid 欄位指 PR-1。
jedi_oscal/domain/entity/base/oscal_component_entity.py:
from typing import Optional
COMPONENT_TYPE_ENUM = {
"this-system", "system", "interconnection", "software", "hardware",
"service", "policy", "physical", "process-procedure", "plan",
"guidance", "standard", "validation", "network", "other",
}
COMPONENT_STATUS_ENUM = {
"operational", "under-development", "under-major-modification",
"disposition", "other",
}
class ComponentEntity:
"""OSCAL <component> domain entity.
Component type follows OSCAL spec 14 enum + "other" (allow-other=yes).
For framework-specific values that don't fit the 14 enum (e.g. CMMC 'api' /
'cli'), normalize component_type='service' and store original in
props.cmmc:category (see design.md §2.1).
leveraged_authorization_uid is set when this component IS the resource that
was authorized (FedRAMP-style ATO inheritance).
"""
def __init__(
self,
id: Optional[int] = None,
uid: Optional[str] = None,
ssp_id: Optional[int] = None,
component_type: str = "",
title: str = "",
description: Optional[str] = None,
purpose: Optional[str] = None,
status: Optional[str] = None,
leveraged_authorization_uid: Optional[str] = None,
props: Optional[dict] = None,
tenant_id: Optional[int] = None,
org_unit_id: Optional[int] = None,
is_active: bool = True,
created_at=None,
created_user: Optional[str] = None,
updated_at=None,
updated_user: Optional[str] = None,
):
self.id = id
self.uid = uid
self.ssp_id = ssp_id
self.component_type = component_type
self.title = title
self.description = description
self.purpose = purpose
self.status = status
self.leveraged_authorization_uid = leveraged_authorization_uid
self.props = props
self.tenant_id = tenant_id
self.org_unit_id = org_unit_id
self.is_active = is_active
self.created_at = created_at
self.created_user = created_user
self.updated_at = updated_at
self.updated_user = updated_userjedi_oscal/tests/test_oscal_component_entity.py:
from jedi_oscal.domain.entity.base.oscal_component_entity import (
ComponentEntity, COMPONENT_TYPE_ENUM, COMPONENT_STATUS_ENUM,
)
def test_component_type_enum_includes_oscal_14_plus_other():
expected_oscal_14 = {
"this-system", "system", "interconnection", "software", "hardware",
"service", "policy", "physical", "process-procedure", "plan",
"guidance", "standard", "validation", "network",
}
assert expected_oscal_14.issubset(COMPONENT_TYPE_ENUM)
assert "other" in COMPONENT_TYPE_ENUM
def test_component_status_enum_oscal_5():
expected = {
"operational", "under-development", "under-major-modification",
"disposition", "other",
}
assert COMPONENT_STATUS_ENUM == expected
def test_component_entity_minimal():
entity = ComponentEntity(
uid="comp-uid-1",
ssp_id=1,
component_type="service",
title="AWS S3",
tenant_id=102,
)
assert entity.component_type == "service"
assert entity.leveraged_authorization_uid is None
def test_component_entity_with_leveraged_auth_link():
entity = ComponentEntity(
uid="comp-uid-2",
ssp_id=1,
component_type="service",
title="Crowdstrike",
leveraged_authorization_uid="la-uid-1",
props={"cmmc:category": "api"},
tenant_id=102,
)
assert entity.leveraged_authorization_uid == "la-uid-1"
assert entity.props["cmmc:category"] == "api"poetry run pytest jedi_oscal/tests/test_oscal_component_entity.py -v
# Expected: PASS
git add jedi_oscal/domain/entity/base/oscal_component_entity.py \
jedi_oscal/domain/entity/base/oscal_component_query_entity.py \
jedi_oscal/tests/test_oscal_component_entity.py
git commit -m "feat(oscal): add ComponentEntity + QueryEntity (PR-2 part 1)"簡寫:對 Component 重複 PR-1 全套(model / mapper / repo interface+impl / domain service / 跑測試)。額外注意:
oscal_component.py 加 leveraged_authorization_uid = Column(String(36), nullable=True) 欄位(不設 FK constraint,因 leveraged_auth 跨 schema 寫 string ref;對應 design.md §1.2 idx_ssp_components_lev partial index)__table_args__ Index:
Index("idx_ssp_components_ssp", "ssp_id", "is_active"),
Index("idx_ssp_components_lev", "leveraged_authorization_uid",
postgresql_where=text("leveraged_authorization_uid IS NOT NULL")),Commit 點同 PR-1 5 個(entity → ORM → mapper → repo → service),test 1 個整合 commit。
1281121..2162af2 + 1 follow-up ac1d860)⚠️ 接手者注意:M2M 設計 per §11.8 hardened — _write_join_rows 返 actually-written list,add() 用 resolved list(不是 requested list)重新 attach 給 saved entity;unresolved uid emit logging.warning;input dedup 用 set(uids) 防 composite PK IntegrityError。update() 不 override — mutation 走「deactivate + re-add」(plan §4.4 原 note)。Reads 仍回傳 implemented_component_uids=[](Phase 1 簡化,Phase 2+ caller 自行 fetch M2M list)。
Working directory: ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/
特殊處理: 額外需要 M2M join model oscal_inventory_implemented_component.py,entity 用 implemented_component_uids: list[str] 表達。
jedi_oscal/domain/entity/base/oscal_inventory_item_entity.py:
from typing import Optional, List
class InventoryItemEntity:
"""OSCAL <inventory-item> domain entity.
Represents a specific instance (e.g. server 'web-01.acme.local',
laptop 'asset-tag-12345'). Connects to one or more ComponentEntity via
<implemented-component component-uuid=...> M2M (stored as
implemented_component_uids list).
"""
def __init__(
self,
id: Optional[int] = None,
uid: Optional[str] = None,
ssp_id: Optional[int] = None,
description: str = "",
props: Optional[dict] = None,
implemented_component_uids: Optional[List[str]] = None,
tenant_id: Optional[int] = None,
org_unit_id: Optional[int] = None,
is_active: bool = True,
created_at=None,
created_user: Optional[str] = None,
updated_at=None,
updated_user: Optional[str] = None,
):
self.id = id
self.uid = uid
self.ssp_id = ssp_id
self.description = description
self.props = props
self.implemented_component_uids = implemented_component_uids or []
self.tenant_id = tenant_id
self.org_unit_id = org_unit_id
self.is_active = is_active
self.created_at = created_at
self.created_user = created_user
self.updated_at = updated_at
self.updated_user = updated_userjedi_oscal/infra/model/base/oscal_inventory_implemented_component.py:
from sqlalchemy import Column, Integer, ForeignKey
from jedi_oscal.infra.model.base.base_model import Base
class OscalInventoryImplementedComponentModel(Base):
__tablename__ = "ssp_inventory_implemented_components"
__table_args__ = ({"schema": "oscal"},)
inventory_item_id = Column(
Integer,
ForeignKey("oscal.ssp_inventory_items.id", ondelete="CASCADE"),
primary_key=True,
)
component_id = Column(
Integer,
ForeignKey("oscal.ssp_components.id", ondelete="CASCADE"),
primary_key=True,
)Mapper 需要特殊處理 implemented_component_uids ↔︎ join rows 的轉換。to_entity 時 query 對應的 join rows → component_id list → 再 query components 表拿 uid list;to_model 不在 mapper 內處理 join (由 repo add / update method 在主 entity 寫入後額外寫 join rows)。
class InventoryItemMapper:
@staticmethod
def to_entity(model: OscalInventoryItemModel, implemented_component_uids: List[str] = None) -> InventoryItemEntity:
if model is None:
return None
return InventoryItemEntity(
id=model.id,
uid=model.uid,
ssp_id=model.ssp_id,
description=model.description,
props=model.props,
implemented_component_uids=implemented_component_uids or [],
tenant_id=model.tenant_id,
org_unit_id=model.org_unit_id,
is_active=model.is_active,
# audit fields ...
)
@staticmethod
def to_model(entity: InventoryItemEntity) -> OscalInventoryItemModel:
return OscalInventoryItemModel(
id=entity.id,
uid=entity.uid,
ssp_id=entity.ssp_id,
description=entity.description,
props=entity.props,
tenant_id=entity.tenant_id,
org_unit_id=entity.org_unit_id,
is_active=entity.is_active,
)jedi_oscal/infra/repository/base/inventory_item_repo_impl.py:
from jedi_common.session.database.repository.base_repository_impl import BaseRepositoryImpl
# ... imports
class InventoryItemRepoImpl(IInventoryItemRepository, BaseRepositoryImpl[...]):
def __init__(self):
super().__init__(
mapper=InventoryItemMapper,
model=OscalInventoryItemModel,
)
def add(self, entity: InventoryItemEntity) -> InventoryItemEntity:
# 先寫主表
saved = super().add(entity)
# 寫 M2M join rows
if entity.implemented_component_uids:
self._write_join_rows(saved.id, entity.implemented_component_uids)
return saved
def _write_join_rows(self, inventory_item_id: int, component_uids: List[str]):
# query components.id by uids,寫 join rows
from jedi_oscal.infra.model.base.oscal_component import OscalComponentModel
comp_ids = self.session.query(OscalComponentModel.id).filter(
OscalComponentModel.uid.in_(component_uids)
).all()
for (comp_id,) in comp_ids:
join_row = OscalInventoryImplementedComponentModel(
inventory_item_id=inventory_item_id,
component_id=comp_id,
)
self.session.add(join_row)
self.session.flush()Note: update 對 M2M 處理較複雜(diff old/new uids),本 phase 簡化為「update 只動主表,M2M 修改走 deactivate + 重 add」。若實作後測試發現需要 in-place update,再加 method。
同 PR-1 pattern。預計 5-6 個 commit(entity / M2M ORM / mapper / repo / service / tests)。
95e9efd)⚠️ 接手者注意:
_ref / matched_party_uuid 保持 str 不升 uuid.UUID(per §11.6 — 三段 pipeline 設計)ParsedDevice / ParsedInformationSystem / ParsedLeveraged dataclass 保留(加 deprecation comment)— Task 11 cleanup 才刪,避免 import-chain 連鎖崩TypeError: unexpected keyword argument 'parsed_devices'(這是 plan 故意的 migration trigger,Task 6+ scope 處理):
app/oscal/service/ssp_excel_import_app_service.py 6 處(lines 718-722, 733-735, 778-792, 1549-1566, 1633-1658, 1743-1765, 1918-1935, 1979-2017)tests/test_a4_reconciliation_orchestrator.py + tests/test_ssp_excel_import_system_characteristic_e2e.pyWorking directory: /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be/
Files:
domain/oscal/parser/ssp_intermediate.pytests/test_ssp_intermediate_v2_bundle.pycat /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be/domain/oscal/parser/ssp_intermediate.py | head -200確認既有 ParsedDevice / ParsedInformationSystem / ParsedLeveraged 結構。Plan Step 5.3 改動建立在這個基礎上。
tests/test_ssp_intermediate_v2_bundle.py:
from datetime import date
from domain.oscal.parser.ssp_intermediate import (
ParsedComponent, ParsedLeveragedAuthorization, ParsedInventoryItem,
ParsedExcelEntityBundle, MatchMethod,
)
def test_parsed_component_defaults():
pc = ParsedComponent(title="Crowdstrike", component_type="service")
assert pc.title == "Crowdstrike"
assert pc.component_type == "service"
assert pc.description is None
assert pc.leveraged_authorization_ref is None
assert pc.match_method == MatchMethod.UNMATCHED
def test_parsed_leveraged_authorization_full_payload():
pla = ParsedLeveragedAuthorization(
title="Crowdstrike",
provider="Crowdstrike Inc.",
date_authorized=date(2024, 3, 15),
fedramp_package_id="FR18078583629",
impact_level="moderate",
)
assert pla.fedramp_package_id == "FR18078583629"
assert pla.impact_level == "moderate"
def test_parsed_inventory_item_implemented_refs():
pi = ParsedInventoryItem(
description="web-01.acme.local",
ipv4_address="10.0.1.5",
implemented_component_refs=["nginx", "ubuntu"],
)
assert len(pi.implemented_component_refs) == 2
def test_bundle_has_3_new_lists_and_no_deprecated():
bundle = ParsedExcelEntityBundle()
assert bundle.parsed_components == []
assert bundle.parsed_leveraged_authorizations == []
assert bundle.parsed_inventory_items == []
# 舊三類 dataclass 不在 bundle dataclass attributes
assert not hasattr(bundle, "parsed_devices")
assert not hasattr(bundle, "parsed_info_systems")
assert not hasattr(bundle, "parsed_leveraged")poetry run pytest tests/test_ssp_intermediate_v2_bundle.py -vExpected: FAIL — 新 dataclass 不存在 / Bundle 仍有舊欄位
對應 design.md §1.4 完整 dataclass code。要點:
ParsedComponent / ParsedLeveragedAuthorization / ParsedInventoryItemParsedExcelEntityBundle 加 parsed_components / parsed_leveraged_authorizations / parsed_inventory_items 3 個 list 欄位parsed_devices / parsed_info_systems / parsed_leveraged 3 個欄位ParsedDevice / ParsedInformationSystem / ParsedLeveraged 加 # removed in v3.0 bundle comment 但 class 本身先保留(Task 11 cleanup 才刪,避免 import 失敗連鎖)poetry run pytest tests/test_ssp_intermediate_v2_bundle.py -vExpected: 4 tests PASS
poetry run pytest tests/test_cmmc_ssp_adapter.py tests/test_docx_parser_core_metadata_block.py -vExpected: 既有測試仍綠(舊 path 還在 — adapter 仍輸出舊 shape,Phase 2 才切)
git add domain/oscal/parser/ssp_intermediate.py tests/test_ssp_intermediate_v2_bundle.py
git commit -m "feat(ssp-oscal-alignment): add ParsedComponent/LeveragedAuthorization/InventoryItem dataclass + restructure Bundle"2e6849f)File: di_containers/oscal/oscal_containers.py
⚠️ 接手者注意:本段原寫的 wiring 跟實際對齊既有 jedi-oscal pattern 後有 2 點差異(per §11.4, §11.5):
<entity>_repo=(不是原寫的 repository=)— 對齊套件 __init__(self, leveraged_authorization_repo: ILeveragedAuthorizationRepo) 等簽名 + 既有 PartyDomainService(party_repo=...) 慣例providers.Singleton(不是原警告的 Factory)— BaseRepositoryImpl 用 @property session lazy load,__init__ 不碰 session,Singleton 安全;既有 70+ repo provider 全部用 Singleton 已驗證 production OK實際 wired pattern(已 commit):
leveraged_authorization_repo = providers.Singleton(LeveragedAuthorizationRepoImpl)
component_repo = providers.Singleton(ComponentRepoImpl)
inventory_item_repo = providers.Singleton(InventoryItemRepoImpl)
leveraged_authorization_domain_service = providers.Factory(
LeveragedAuthorizationDomainService,
leveraged_authorization_repo=leveraged_authorization_repo,
)
component_domain_service = providers.Factory(
ComponentDomainService,
component_repo=component_repo,
)
inventory_item_domain_service = providers.Factory(
InventoryItemDomainService,
inventory_item_repo=inventory_item_repo,
)a9712a1)File: scripts/sql/2026-05-24-ssp-oscal-alignment-create-tables.sql
⚠️ 接手者注意:design.md §1.2 原 DDL 是 sketch;實際 SQL 已 ship 在上述檔案,與 §1.2 主要差異:
uid / leveraged_authorization_uid / party_uuid 用 UUID 不是 VARCHAR(36)(per §11.2)EXISTS subquery 連 parent inventory_items 做 RLS(design.md §1.2 註記說「透過 parent 拿 tenant」,實作具體就是 EXISTS policy)TIMESTAMPTZ NOT NULL DEFAULT now() + VARCHAR(50) (created_user / updated_user)Reference: see design.md §11.10 for full SQL reality reconciliation.
567efe7)File: scripts/sql/2026-05-24-ssp-oscal-alignment-migrate-data.sql
⚠️ 接手者注意:design.md §1.3 原 INSERT SELECT SQL 跟實際 DB schema 大幅偏離(8+ columns 不存在、tenant 解析要 4-table chain、discriminator 是 implementation_type 不是 category)。不要從 design.md §1.3 抄 SQL,直接看上面那個 .sql 檔案。完整 reconciliation 在 design.md §11.10。
Migration 實際結果:
WHERE p.tenant_id IS NOT NULL 過濾掉c5b43fb)File: scripts/sql/2026-05-24-ssp-oscal-alignment-verification.sql
3 個 verification gate 全綠:
new_components (341) == old_resolvable (341) ✓new_leveraged_auths (12) == old_resolvable_lev (12) ✓new_comp_lev_linked (12) <= new_leveraged_auths (12) ✓NOTICE output: Migration verified: old_total=394, old_resolvable=341, new_components=341, new_leveraged=12, new_comp_lev_linked=12. Orphans skipped: 53 rows
Files:
tests/test_ssp_oscal_migration_e2e.py目的:DROP 舊表前確認新表能完整支撐 Excel + docx import + GET SSP detail。這是 Phase 1 最重要的 gate。
tests/test_ssp_oscal_migration_e2e.py:
"""E2E test for Phase 1 migration cutover.
Prerequisites:
- Task 7-9 已跑完(新表存在 + 資料 migrated)
- DI 已 wire 3 個新 domain service (Task 6)
Goal: 確認 Excel import → GET SSP detail 能完整經過新表 path 取得資料。
"""
import pytest
from unittest.mock import MagicMock, patch
@pytest.fixture(autouse=True)
def patch_logger():
# Per CLAUDE.md feedback「寫 app service test 必加 logger patch」
with patch("app.oscal.service.ssp_excel_import_app_service.logger", MagicMock()):
yield
def test_excel_import_then_get_ssp_writes_to_new_tables(client, headers):
# 1. Upload Excel template containing leveraged + components + inventory data
# 2. POST /ssp-excel-imports/{parse_uid}/confirm
# 3. GET /ssp/{ssp_uid} — verify response 含 leveraged/components/inventory data
# 4. Direct DB query: confirm rows in new tables (ssp_components / ssp_leveraged / inventory)
pass # Full impl 視 Phase 2 confirm path / GET SSP detail API 進度
def test_docx_import_with_customer_fixture_writes_new_tables(client, headers):
# Use tests/data/oscal/customer_sample_scrubbed.docx
# POST /ssp-docx-imports → confirm
# Direct DB verify new tables populated
pass
def test_migrated_ssp_get_returns_full_data(client, headers):
# 已存在的 SSP(migration 前就有的)— GET 後 leveraged/components 從新表讀
pass⚠️ 完整 impl 視 Phase 2 confirm path 進度。Phase 1 single-task focus 是 新表結構能 cover 所有資料,不必 e2e 100% pass — 但至少 SQL spot check 要全綠。
如果 Phase 2 confirm path 還沒切,手動驗證:
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -c "
SET app.is_super_admin='t';
-- 全表 spot check
SELECT 'leveraged_authorizations' AS tbl, count(*) FROM oscal.ssp_leveraged_authorizations
UNION ALL
SELECT 'components', count(*) FROM oscal.ssp_components
UNION ALL
SELECT 'inventory_items', count(*) FROM oscal.ssp_inventory_items
UNION ALL
SELECT 'inventory_implemented_components', count(*) FROM oscal.ssp_inventory_implemented_components
UNION ALL
SELECT 'old_items_active', count(*) FROM oscal.ssp_system_implementation_items WHERE is_active;
-- Leveraged FK link 驗證
SELECT c.title, c.component_type, la.title AS leveraged_title
FROM oscal.ssp_components c
LEFT JOIN oscal.ssp_leveraged_authorizations la ON la.uid = c.leveraged_authorization_uid
WHERE c.leveraged_authorization_uid IS NOT NULL LIMIT 5;
"Expected:
poetry run pytest tests/test_docx_section_extractors.py \
tests/test_docx_section_extractors_customer.py \
tests/test_cmmc_ssp_adapter.py \
tests/test_ssp_docx_diff_service.py \
tests/test_ssp_docx_import_app_service.py -vExpected: 既有 135+36 tests 全綠(除已知 pre-existing fail 2 個,per handoff §4.2)
⚠️ DROP 前停下問 user:「Task 10 e2e + spot check 都過,要進 Task 11/12 DROP 舊表嗎?」
Working directory: ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
grep -rn "SspSystemImplementationItemEntity\|ssp_system_implementation_item" jedi_oscal/ --include="*.py"確認套件側只有舊 entity / repo / service / model / mapper 自身的引用。
cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
grep -rn "SspSystemImplementationItemEntity\|ssp_system_implementation_item" \
--include="*.py" --include="*.sql" \
--exclude-dir=__pycache__ --exclude-dir=.venv主專案 caller 列表寫到一個臨時 sheet。每個 caller 必須改成新 entity / 新 service 才能進 DROP。
grep -rn "SspSystemImplementationItemEntity\|ssp_system_implementation_item" \
~/Projects/Jedicogy/module/jedi-python-package/ --include="*.py"確認 jedi-compliance / jedi-flow-engine / 其他套件沒人引用。如有 → 停下問 user。
按 Step 11.1.2 列表,每個 file 改 import + method call 用新 service。這 step 是 Phase 1 內最容易出 regression 的地方,每改一個 file 跑對應 test。
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal && poetry run pytest -v
cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be && poetry run pytest tests/ -vExpected: 全綠(除既知 pre-existing fail)
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
git rm jedi_oscal/domain/entity/base/ssp_system_implementation_item_entity.py \
jedi_oscal/domain/entity/base/ssp_system_implementation_item_query_entity.py \
jedi_oscal/domain/repository/base/ssp_system_implementation_item.py \
jedi_oscal/domain/services/base/ssp_system_implementation_item_domain_service.py \
jedi_oscal/infra/model/base/ssp_system_implementation_item.py \
jedi_oscal/infra/mapper/base/ssp_system_implementation_item_mapper.py \
jedi_oscal/infra/repository/base/ssp_system_implementation_item_repo_impl.py
# (確切路徑用 Step 11.1.1 結果調整)cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal && poetry run pytest -v
cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be && poetry run pytest tests/ -vExpected: 全綠。若有 fail → 通常是 Step 11.2 漏掉某個 caller,回去改。
# 套件側
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
git commit -m "feat(oscal): remove SspSystemImplementationItemEntity + repo/service/model/mapper (PR-4 cleanup)"
# 主專案
cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
git add <changed files>
git commit -m "refactor(ssp-oscal-alignment): replace SspSystemImplementationItemEntity callers with new entities"Files:
scripts/sql/ssp_oscal_alignment_drop_old.sqlscripts/sql/ssp_oscal_alignment_drop_old.sql:
-- Date: 2026-05-XX
-- 4. DROP oscal.ssp_system_implementation_items + dependent objects (2026-05-XX)
-- Prerequisite: Task 11 caller cleanup 完成、e2e 驗證通過
-- Run as: cmmgr
DROP TABLE IF EXISTS oscal.ssp_system_implementation_items CASCADE;⚠️ User 明確 confirm 後才跑(per CLAUDE.md「destructive operation 需確認」):
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
-f scripts/sql/ssp_oscal_alignment_drop_old.sqlPGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -c "
SET app.is_super_admin='t';
SELECT tablename FROM pg_tables WHERE schemaname='oscal'
AND tablename = 'ssp_system_implementation_items';"Expected: 0 rows.
git add scripts/sql/ssp_oscal_alignment_drop_old.sql
git commit -m "feat(ssp-oscal-alignment): SQL migration Step 4 — DROP old ssp_system_implementation_items"⚠️ 不自動執行 — Per CLAUDE.md 規範套件發版需 user 明確指示:「絕對禁止:自動執行套件發版 / 自動推 Nexus」。
跟 user 報:「Phase 1 全部完成(Task 1-12),現在要 bump jedi-oscal version + 推 Nexus + 主專案 pin 回 Nexus 版本 — 確認進這 step 嗎?」
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
# 改 pyproject.toml version: 0.0.18 → 0.1.0 (minor bump — 加 3 個 entity 算 functional addition)git add pyproject.toml
git commit -m "chore(jedi-oscal): bump 0.0.18 → 0.1.0 — add Component / LeveragedAuth / InventoryItem"
git push origin feature/ssp-oscal-alignment
# user 自行在 GitLab/GitHub 開 PR 4 個 (PR-1 ~ PR-4) merge 到 main具體指令見套件 README / 既有套件 publish script。
cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
# pyproject.toml jedi-oscal 改回 Nexus pin
# jedi-oscal = "0.1.0"poetry update jedi-oscalExpected: jedi-oscal 切回 Nexus 0.1.0 mode
poetry run pytest tests/ -vgit add pyproject.toml poetry.lock
git commit -m "chore(ssp-oscal-alignment): pin jedi-oscal 0.1.0 from Nexus (cutover from path dependency)"Per CLAUDE.md「Changelog 收尾才寫」— Phase 1 收尾時一次性寫。
docs/changelog/2026-05-XX-feat-ssp-oscal-alignment-phase1.md:
---
type: feat
breaking: true
modules: [oscal, jedi-oscal]
commit: <Task 1-13 主 commit hashes>
---
# SSP OSCAL Alignment — Phase 1 Foundation
[詳細內容: 改動範圍 / API 變更 / migration 路徑 / 測試結果]⚠️ Per CLAUDE.md「禁止憑證入版控」— SQL migration / changelog 內提到 host / port / db 名稱即可,密碼一律改「請查 .env」。
docs/features/FR-028-2605-ssp-oscal-alignment/handoff/2026-05-XX-phase1-SUMMARY.md:
報告 Phase 1 完整 commits / 改動 / 已知 follow-up(Phase 2 confirm path 切換 / Phase 3 Excel template 等)。
poetry run python scripts/extract_claude_sessions.py \
--date 2026-05-XX --topic ssp-oscal-alignment-phase1 --auto存到 docs/conversation-history/2026-05-XX/ssp-oscal-alignment-phase1/。
Phase 1 完成後,Phase 2 (Import Pipeline 對接) 處理:
ParsedSsp 改成 ParsedExcelEntityBundlePhase 2 完成後 Phase 1+2 一起 deploy(per brainstorm Topic 3 決策)。