SSP OSCAL Alignment — Phase 1 Implementation Plan

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


§1

File Structure

jedi-oscal 套件側(路徑 ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/

PR-1 LeveragedAuthorization (Task 2)

  • 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 — 新

PR-2 Component (Task 3) — 含 FK 指 LeveragedAuth

  • 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 — 新

PR-3 InventoryItem (Task 4) — 含 M2M join

  • 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 — 新

PR-4 Cleanup (Task 11)

  • 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)

Pre-flight verification(每個 Task 開工前必查)

Per CLAUDE.md feedback「實作計畫的 method/欄位假設先 verify 才開工」— plan 寫好到開工經常 days/weeks 期間 method 改名 / entity shape / error code 序號被佔。每個 Task 第一個 Step 統一是 verify 假設(grep / read 確認)。


§2

Task 1: 主專案 pyproject.toml 改 path dependency(pre-flight)

Files:

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

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

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

§3

Task 2: jedi-oscal PR-1 — LeveragedAuthorization entity 縱切片 — ✅ SHIPPED 2026-05-24 (5 commits 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:

  • Create: jedi_oscal/domain/entity/base/oscal_leveraged_authorization_entity.py
  • Create: jedi_oscal/domain/entity/base/oscal_leveraged_authorization_query_entity.py
  • Create: jedi_oscal/domain/repository/base/leveraged_authorization.py
  • Create: jedi_oscal/domain/services/base/leveraged_authorization_domain_service.py
  • Create: jedi_oscal/infra/model/base/oscal_leveraged_authorization.py
  • Create: jedi_oscal/infra/mapper/base/leveraged_authorization_mapper.py
  • Create: jedi_oscal/infra/repository/base/leveraged_authorization_repo_impl.py
  • Test: jedi_oscal/tests/test_oscal_leveraged_authorization_entity.py

Task 2.1: 對照既有 party entity pattern

cat ~/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。

Task 2.2: Entity + QueryEntity

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

Expected: 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_user
poetry run pytest jedi_oscal/tests/test_oscal_leveraged_authorization_entity.py -v

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

Task 2.3: ORM model

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

Task 2.4: Mapper

cat ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/mapper/base/party_mapper.py

jedi_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)"

Task 2.5: Repository interface + impl

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

Task 2.6: Domain service

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

Task 2.7: 整合測試 + push branch

cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
poetry run pytest -v

Expected: 既有 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。


§4

Task 3: jedi-oscal PR-2 — Component entity 縱切片(FK to LeveragedAuth)— ✅ SHIPPED 2026-05-24 (5 commits 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_levpostgresql_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_authorizationcomponent,加 leveraged_authorization_uid 欄位指 PR-1。

Task 3.1: Entity + QueryEntity

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_user

jedi_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)"

Task 3.2-3.6: 重複 Task 2.3-2.7 pattern 對 Component

簡寫:對 Component 重複 PR-1 全套(model / mapper / repo interface+impl / domain service / 跑測試)。額外注意:

  • ORM model oscal_component.pyleveraged_authorization_uid = Column(String(36), nullable=True) 欄位(設 FK constraint,因 leveraged_auth 跨 schema 寫 string ref;對應 design.md §1.2 idx_ssp_components_lev partial index)
  • ORM model 加 __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。


§5

Task 4: jedi-oscal PR-3 — InventoryItem entity 縱切片(含 M2M join)— ✅ SHIPPED 2026-05-24 (6 commits 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] 表達。

Task 4.1: InventoryItemEntity (含 M2M list)

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_user

Task 4.2: M2M join ORM model

jedi_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,
    )

Task 4.3: Mapper 處理 M2M

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,
        )

Task 4.4: Repo impl 加 M2M 寫入

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。

Task 4.5-4.6: Domain service + 整合測試 + commit

同 PR-1 pattern。預計 5-6 個 commit(entity / M2M ORM / mapper / repo / service / tests)。


§6

Task 5: 主專案 — ParsedExcelEntityBundle dataclass 重設計 — ✅ SHIPPED 2026-05-24 (95e9efd)

⚠️ 接手者注意

  1. parse-time _ref / matched_party_uuid 保持 str 不升 uuid.UUID(per §11.6 — 三段 pipeline 設計)
  2. ParsedDevice / ParsedInformationSystem / ParsedLeveraged dataclass 保留(加 deprecation comment)— Task 11 cleanup 才刪,避免 import-chain 連鎖崩
  3. 移除 bundle 3 個舊 fields 後,8 個 caller 立即會炸 TypeError: unexpected keyword argument 'parsed_devices'(這是 plan 故意的 migration trigger,Task 6+ scope 處理):
    • Production: 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: tests/test_a4_reconciliation_orchestrator.py + tests/test_ssp_excel_import_system_characteristic_e2e.py

Working directory: /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be/

Files:

  • Modify: domain/oscal/parser/ssp_intermediate.py
  • Test: tests/test_ssp_intermediate_v2_bundle.py

Task 5.1: Verify 現有 dataclass shape

cat /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be/domain/oscal/parser/ssp_intermediate.py | head -200

確認既有 ParsedDevice / ParsedInformationSystem / ParsedLeveraged 結構。Plan Step 5.3 改動建立在這個基礎上。

Task 5.2: 寫 failing test

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

Expected: FAIL — 新 dataclass 不存在 / Bundle 仍有舊欄位

Task 5.3: 重設計 dataclass

對應 design.md §1.4 完整 dataclass code。要點:

  1. 新增 ParsedComponent / ParsedLeveragedAuthorization / ParsedInventoryItem
  2. ParsedExcelEntityBundleparsed_components / parsed_leveraged_authorizations / parsed_inventory_items 3 個 list 欄位
  3. 移除 parsed_devices / parsed_info_systems / parsed_leveraged 3 個欄位
  4. 舊 dataclass ParsedDevice / ParsedInformationSystem / ParsedLeveraged# removed in v3.0 bundle comment 但 class 本身先保留(Task 11 cleanup 才刪,避免 import 失敗連鎖)
poetry run pytest tests/test_ssp_intermediate_v2_bundle.py -v

Expected: 4 tests PASS

poetry run pytest tests/test_cmmc_ssp_adapter.py tests/test_docx_parser_core_metadata_block.py -v

Expected: 既有測試仍綠(舊 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"

§7

Task 6: 主專案 — DI container wire 套件新 services — ✅ SHIPPED 2026-05-24 (2e6849f)

File: di_containers/oscal/oscal_containers.py

⚠️ 接手者注意:本段原寫的 wiring 跟實際對齊既有 jedi-oscal pattern 後有 2 點差異(per §11.4, §11.5):

  1. Init param<entity>_repo=(不是原寫的 repository=)— 對齊套件 __init__(self, leveraged_authorization_repo: ILeveragedAuthorizationRepo) 等簽名 + 既有 PartyDomainService(party_repo=...) 慣例
  2. Repo provider 用 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,
)

§8

Task 7: 主專案 — DB Migration Step 1(建新表 + RLS policy)— ✅ SHIPPED 2026-05-24 (a9712a1)

File: scripts/sql/2026-05-24-ssp-oscal-alignment-create-tables.sql

⚠️ 接手者注意:design.md §1.2 原 DDL 是 sketch;實際 SQL 已 ship 在上述檔案,與 §1.2 主要差異:

  1. uid / leveraged_authorization_uid / party_uuidUUID 不是 VARCHAR(36)(per §11.2)
  2. M2M join table 用 EXISTS subquery 連 parent inventory_items 做 RLS(design.md §1.2 註記說「透過 parent 拿 tenant」,實作具體就是 EXISTS policy)
  3. 全部 audit field 寫成標準 TIMESTAMPTZ NOT NULL DEFAULT now() + VARCHAR(50) (created_user / updated_user)

Reference: see design.md §11.10 for full SQL reality reconciliation.


§9

Task 8: 主專案 — DB Migration Step 2(migrate data)— ✅ SHIPPED 2026-05-24 (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 實際結果

  • 12 leveraged_authorizations 寫入(原 24 中有 12 屬 orphan dev test SSP 被 skip)
  • 341 components 寫入(394 - 53 orphan = 341;hardware/164 + system/165 + service/12 = 341)
  • 12/12 service components 全部成功 link 回 leveraged_authorizations.uid
  • 53 orphan rows / 8 dev test SSPs (blsadmin/blsit fixtures) 用 WHERE p.tenant_id IS NOT NULL 過濾掉

§10

Task 9: 主專案 — DB Migration Step 3(verification)— ✅ SHIPPED 2026-05-24 (c5b43fb)

File: scripts/sql/2026-05-24-ssp-oscal-alignment-verification.sql

3 個 verification gate 全綠:

  1. new_components (341) == old_resolvable (341)
  2. new_leveraged_auths (12) == old_resolvable_lev (12)
  3. 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


§11

Task 10: 主專案 — E2E 驗證(DROP 前必跑)

Files:

  • Create: tests/test_ssp_oscal_migration_e2e.py

目的:DROP 舊表前確認新表能完整支撐 Excel + docx import + GET SSP detail。這是 Phase 1 最重要的 gate

Task 10.1: 撰寫 e2e 測試

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:

  • old_items_active count == components count
  • leveraged-linked components 有資料
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 -v

Expected: 既有 135+36 tests 全綠(除已知 pre-existing fail 2 個,per handoff §4.2)

⚠️ DROP 前停下問 user:「Task 10 e2e + spot check 都過,要進 Task 11/12 DROP 舊表嗎?」


§12

Task 11: jedi-oscal PR-4 — Cleanup(刪 SspSystemImplementationItemEntity)

Working directory: ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/

Task 11.1: Grep 確認所有 caller

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。

Task 11.2: 改 caller 到新 entity

按 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/ -v

Expected: 全綠(除既知 pre-existing fail)

Task 11.3: 刪套件側舊 entity

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

Expected: 全綠。若有 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"

§13

Task 12: 主專案 — DB Migration Step 4(DROP 舊表)

Files:

  • Create: scripts/sql/ssp_oscal_alignment_drop_old.sql

scripts/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.sql
PGPASSWORD='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"

§14

Task 13: 套件版本 bump + Nexus 推送 + 主專案 pin

⚠️ 不自動執行 — Per CLAUDE.md 規範套件發版需 user 明確指示:「絕對禁止:自動執行套件發版 / 自動推 Nexus」。

Task 13.1: 等 user 指示

跟 user 報:「Phase 1 全部完成(Task 1-12),現在要 bump jedi-oscal version + 推 Nexus + 主專案 pin 回 Nexus 版本 — 確認進這 step 嗎?」

Task 13.2: 套件 bump version

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。

Task 13.3: 主專案 pin 回 Nexus 版本

cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
# pyproject.toml jedi-oscal 改回 Nexus pin
# jedi-oscal = "0.1.0"
poetry update jedi-oscal

Expected: jedi-oscal 切回 Nexus 0.1.0 mode

poetry run pytest tests/ -v
git add pyproject.toml poetry.lock
git commit -m "chore(ssp-oscal-alignment): pin jedi-oscal 0.1.0 from Nexus (cutover from path dependency)"

§15

Task 14: Changelog + handoff

Per CLAUDE.md「Changelog 收尾才寫」— Phase 1 收尾時一次性寫。

Task 14.1: Changelog

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」。

Task 14.2: Handoff 文件

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 等)。

Task 14.3: 對話歸檔

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/


§16

Phase 1 完成 Definition of Done


§17

已知 follow-up(Phase 2 接續)

Phase 1 完成後,Phase 2 (Import Pipeline 對接) 處理:

  1. Docx Adapter 重寫 — output 從 ParsedSsp 改成 ParsedExcelEntityBundle
  2. Excel Parser 對接新 sheet — 06a / 06b / 04 sheet
  3. SspEntityReconciliationOrchestrator 補三條 strategy — Component / LeveragedAuth / InventoryItem
  4. Confirm path schema_version 分流 — v1-ssp legacy / v2-bundle new

Phase 2 完成後 Phase 1+2 一起 deploy(per brainstorm Topic 3 決策)。


§18

反悔條件 / Risk register

  • 套件 PR review 過程同質性過高 → 改 squash 1 PR (per brainstorm Topic 1 反悔條件)
  • Task 10 e2e 無法 cover 某個既有 caller path → Phase 1.1 PR-4 cleanup 前回頭補 caller migration
  • Step 3 verification 失敗 (row count mismatch) → 停下 debug Step 2 SQL,不要強跑 DROP
  • Task 11.1.3 發現其他 jedi- 套件有 caller* → 停下問 user
  • dev DB 跑 migration 失敗無法回滾 → 從 DB snapshot restore