# 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`

---

## 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 確認）。

---

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

- [ ] **Step 1.1: 確認 jedi-oscal 套件目前 branch 狀態**

```bash
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**）。

- [ ] **Step 1.2: 主專案 pyproject.toml 改 jedi-oscal path dependency**

找到 `[tool.poetry.dependencies]` 內 `jedi-oscal = ...` 那行：

```toml
# 改成
jedi-oscal = { path = "/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/jedi-oscal", develop = true }
```

- [ ] **Step 1.3: 跑 poetry install (用 update 不用 lock)**

Per CLAUDE.md「依賴更新一律 poetry update」：

```bash
cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
poetry update jedi-oscal
```

Expected: jedi-oscal 切到 path mode，no version pin 衝突。

- [ ] **Step 1.4: 驗證 import 仍能解析**

```bash
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`

- [ ] **Step 1.5: 不 commit pyproject.toml 改動**

```bash
git status
# Expected: pyproject.toml 顯示 modified，但不 stage / 不 commit
# (Task 14 收尾時改回 pin Nexus 版本才 commit)
```

---

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

- [ ] **Step 2.1.1: 讀 party entity 確認 shape**

```bash
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

- [ ] **Step 2.2.1: 寫 failing entity test**

`jedi_oscal/tests/test_oscal_leveraged_authorization_entity.py`:

```python
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"
```

- [ ] **Step 2.2.2: 跑 test 確認 fail**

```bash
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`

- [ ] **Step 2.2.3: 寫 LeveragedAuthorizationEntity**

`jedi_oscal/domain/entity/base/oscal_leveraged_authorization_entity.py`:

```python
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
```

- [ ] **Step 2.2.4: 跑 test 確認 PASS**

```bash
poetry run pytest jedi_oscal/tests/test_oscal_leveraged_authorization_entity.py -v
```

Expected: 2 tests PASS

- [ ] **Step 2.2.5: 寫 QueryEntity (mirror party pattern)**

`jedi_oscal/domain/entity/base/oscal_leveraged_authorization_query_entity.py`:

```python
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
```

- [ ] **Step 2.2.6: Commit entity + query entity**

```bash
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

- [ ] **Step 2.3.1: 寫 ORM model**

`jedi_oscal/infra/model/base/oscal_leveraged_authorization.py`:

```python
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 跟既有一致。

- [ ] **Step 2.3.2: Commit ORM model**

```bash
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

- [ ] **Step 2.4.1: 讀 party_mapper.py 作 reference**

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

- [ ] **Step 2.4.2: 寫 LeveragedAuthorizationMapper**

`jedi_oscal/infra/mapper/base/leveraged_authorization_mapper.py`:

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

- [ ] **Step 2.4.3: Commit mapper**

```bash
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

- [ ] **Step 2.5.1: 寫 repo interface**

`jedi_oscal/domain/repository/base/leveraged_authorization.py`:

```python
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:
        ...
```

- [ ] **Step 2.5.2: 寫 repo impl**

`jedi_oscal/infra/repository/base/leveraged_authorization_repo_impl.py`:

```python
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 簽名跟既有一致。

- [ ] **Step 2.5.3: Commit repo interface + impl**

```bash
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

- [ ] **Step 2.6.1: 寫 domain service**

`jedi_oscal/domain/services/base/leveraged_authorization_domain_service.py`:

```python
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)
```

- [ ] **Step 2.6.2: Commit domain service**

```bash
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

- [ ] **Step 2.7.1: 跑全套件測試確認沒 regression**

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

Expected: 既有 tests + 新 leveraged entity tests 全綠

- [ ] **Step 2.7.2: 主專案 verify import (因為 path dep)**

```bash
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`

- [ ] **Step 2.7.3: Push 套件 branch (user 明確指示後才 push)**

⚠️ **不自 push** — 套件 branch push 屬於對外可見動作（per CLAUDE.md「push 永遠要 user 明確指示」），停下問 user。

---

## 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_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。

### Task 3.1: Entity + QueryEntity

- [ ] **Step 3.1.1: 寫 ComponentEntity（含 leveraged_authorization_uid 欄位）**

`jedi_oscal/domain/entity/base/oscal_component_entity.py`:

```python
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
```

- [ ] **Step 3.1.2: 寫 entity test**

`jedi_oscal/tests/test_oscal_component_entity.py`:

```python
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"
```

- [ ] **Step 3.1.3: Commit entity + query**

```bash
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.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）
- ORM model 加 `__table_args__` Index:
  ```python
  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。

---

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

- [ ] **Step 4.1.1: 寫 InventoryItemEntity**

`jedi_oscal/domain/entity/base/oscal_inventory_item_entity.py`:

```python
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

- [ ] **Step 4.2.1: 寫 join model**

`jedi_oscal/infra/model/base/oscal_inventory_implemented_component.py`:

```python
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)。

- [ ] **Step 4.3.1: 寫 mapper（不含 M2M）**

```python
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 寫入

- [ ] **Step 4.4.1: 寫 repo impl，override `add` 處理 M2M**

`jedi_oscal/infra/repository/base/inventory_item_repo_impl.py`:

```python
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）。

---

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

- [ ] **Step 5.1.1: 讀現有 ssp_intermediate.py**

```bash
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

- [ ] **Step 5.2.1: 新測試檔**

`tests/test_ssp_intermediate_v2_bundle.py`:

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

- [ ] **Step 5.2.2: 跑 test 確認 fail**

```bash
poetry run pytest tests/test_ssp_intermediate_v2_bundle.py -v
```

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

### Task 5.3: 重設計 dataclass

- [ ] **Step 5.3.1: 改 `domain/oscal/parser/ssp_intermediate.py`**

對應 design.md §1.4 完整 dataclass code。要點：
1. 新增 `ParsedComponent` / `ParsedLeveragedAuthorization` / `ParsedInventoryItem`
2. `ParsedExcelEntityBundle` 加 `parsed_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 失敗連鎖）

- [ ] **Step 5.3.2: 跑 test 確認 PASS**

```bash
poetry run pytest tests/test_ssp_intermediate_v2_bundle.py -v
```

Expected: 4 tests PASS

- [ ] **Step 5.3.3: 跑既有 docx-import-parity tests 確認沒 break 舊 path**

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

Expected: 既有測試仍綠（舊 path 還在 — adapter 仍輸出舊 shape，Phase 2 才切）

- [ ] **Step 5.3.4: Commit**

```bash
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"
```

---

## 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）：
```python
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,
)
```

- [x] **Step 6.1-6.4**: imports + providers + smoke + commit — all 已完成. See commit `2e6849f`.

---

## 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_uuid` 用 `UUID` 不是 `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.

- [x] **Step 7.1: 寫 SQL** — see `scripts/sql/2026-05-24-ssp-oscal-alignment-create-tables.sql`

- [x] **Step 7.2-7.5**: dry-run + apply + verify + commit — all 已完成，4 tables 全建好，rowsecurity=t for all 4. See commit `a9712a1`.

---

## 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` 過濾掉

- [x] **Step 8.1-8.4**: 寫 SQL + apply + spot-check + commit — all 已完成. See commit `567efe7`.

---

## 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`

- [x] **Step 9.1-9.3**: 寫 verification + apply + commit — all 已完成. See commit `c5b43fb`.

---

## 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 測試

- [ ] **Step 10.1.1: 寫 failing e2e test**

`tests/test_ssp_oscal_migration_e2e.py`:

```python
"""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 要全綠。

- [ ] **Step 10.1.2: 跑 manual SQL spot check（替代 e2e）**

如果 Phase 2 confirm path 還沒切，**手動驗證**：

```bash
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 有資料

- [ ] **Step 10.1.3: 跑既有 docx-import-parity test suite 確認沒 regression**

```bash
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）

- [ ] **Step 10.1.4: User sign-off gate（不要自己 ship 過）**

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

---

## Task 11: jedi-oscal PR-4 — Cleanup（刪 SspSystemImplementationItemEntity）

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

### Task 11.1: Grep 確認所有 caller

- [ ] **Step 11.1.1: 套件側 grep**

```bash
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 自身的引用。

- [ ] **Step 11.1.2: 主專案 grep**

```bash
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。

- [ ] **Step 11.1.3: 其他 jedi-* 套件 grep**

```bash
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.2.1: 對每個主專案 caller 改 import**

按 Step 11.1.2 列表，每個 file 改 import + method call 用新 service。**這 step 是 Phase 1 內最容易出 regression 的地方**，每改一個 file 跑對應 test。

- [ ] **Step 11.2.2: 跑全套件 + 主專案 tests**

```bash
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

- [ ] **Step 11.3.1: 刪檔**

```bash
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 結果調整)
```

- [ ] **Step 11.3.2: 跑全套件 + 主專案 tests**

```bash
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，回去改。

- [ ] **Step 11.3.3: 套件 commit + 主專案 commit**

```bash
# 套件側
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"
```

---

## Task 12: 主專案 — DB Migration Step 4（DROP 舊表）

**Files:**
- Create: `scripts/sql/ssp_oscal_alignment_drop_old.sql`

- [ ] **Step 12.1: 寫 DROP SQL**

`scripts/sql/ssp_oscal_alignment_drop_old.sql`:

```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;
```

- [ ] **Step 12.2: User confirm 後跑**

⚠️ User 明確 confirm 後才跑（per CLAUDE.md「destructive operation 需確認」）：

```bash
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
```

- [ ] **Step 12.3: 驗證表已刪**

```bash
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.

- [ ] **Step 12.4: Commit DROP SQL**

```bash
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"
```

---

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

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

### Task 13.1: 等 user 指示

- [ ] **Step 13.1.1: 報告 Task 1-12 完成狀態**

跟 user 報：「Phase 1 全部完成（Task 1-12），現在要 bump jedi-oscal version + 推 Nexus + 主專案 pin 回 Nexus 版本 — 確認進這 step 嗎？」

### Task 13.2: 套件 bump version

- [ ] **Step 13.2.1: 改 jedi-oscal pyproject.toml**

```bash
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
# 改 pyproject.toml version: 0.0.18 → 0.1.0 (minor bump — 加 3 個 entity 算 functional addition)
```

- [ ] **Step 13.2.2: 套件 commit + push branch + merge to main**

```bash
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
```

- [ ] **Step 13.2.3: 推 Nexus（按套件既有 SOP）**

具體指令見套件 README / 既有套件 publish script。

### Task 13.3: 主專案 pin 回 Nexus 版本

- [ ] **Step 13.3.1: 改 pyproject.toml**

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

- [ ] **Step 13.3.2: poetry update**

```bash
poetry update jedi-oscal
```

Expected: jedi-oscal 切回 Nexus 0.1.0 mode

- [ ] **Step 13.3.3: 跑全 test 確認沒 regression**

```bash
poetry run pytest tests/ -v
```

- [ ] **Step 13.3.4: Commit 主專案 pyproject.toml + lock 改動**

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

---

## Task 14: Changelog + handoff

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

### Task 14.1: Changelog

- [ ] **Step 14.1.1: 寫 changelog**

`docs/changelog/2026-05-XX-feat-ssp-oscal-alignment-phase1.md`:

```markdown
---
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 文件

- [ ] **Step 14.2.1: 寫 phase 1 SUMMARY**

`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: 對話歸檔

- [ ] **Step 14.3.1: 跑 conversation history extraction**

```bash
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 完成 Definition of Done

- [ ] 套件側 4 個 PR (LeveragedAuth → Component → InventoryItem → Cleanup) 全部 merge 到 main
- [ ] 套件 v0.1.0 推 Nexus
- [ ] 主專案 pyproject.toml pin Nexus 0.1.0
- [ ] DB 新表（4 個）+ RLS policy 全部 active
- [ ] 舊 `oscal.ssp_system_implementation_items` 表 DROP
- [ ] 主專案所有 caller 改到新 entity
- [ ] ParsedExcelEntityBundle v3.0 dataclass 上線
- [ ] DI container wire 3 個新 domain service
- [ ] 全套件 tests + 主專案 tests + 既有 e2e 全綠
- [ ] Changelog / handoff SUMMARY / 對話紀錄 全部歸檔
- [ ] Phase 1 commits 全 push（user 明確指示後）

---

## 已知 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 決策）。

---

## 反悔條件 / 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
