FR-038 Wave 1 — jedi-oscal-v2 套件實作 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: 從零建立 jedi-oscal-v2 套件,把 FR-038 契約層定稿的 OSCAL v1.2.2 關聯式 schema 以 DDD 四層(entity / model / mapper / repository / service)落地 —— 本期涵蓋稽核生命週期主流程的 6 個 root(catalog / profile / ssp / ap / ar / poam)+ 共用層 + 全部 delta 表,並提供 snapshot / clone / profile-resolution / AR 全量判定矩陣 / 匯入匯出等 primitive 供主專案使用。

本期不做(follow-up):component-definition(component_definitions + 5 個 cd_* 表)。它是「可重用元件描述」的獨立 OSCAL model,不在 Catalog→Profile→SSP→AP→AR→POA&M 稽核主流程上,CMMC 內部稽核流程用不到(design §1 四邊界與 PM 教材皆未涉及)。等有「元件庫」需求再開。本期落地表數 = base 36(42 − 6 個 cd_*)+ delta 6 = 42 表 + compliance.project_audit_rounds(後者僅建 DB,Wave 2 才建 model)。

Architecture: DDD + Hexagonal,沿用舊 jedi-oscal 的分層慣例(domain/ entity+repo interface+service、infra/ model+mapper+repo impl+adapter、app/ app service+dto、ports/ parser、common/ enum+code)。Repo 繼承 jedi_common.session.database.repository.base_repository_impl.BaseRepositoryImpl;session 由主專案 @transaction 開啟,套件 method 為被呼叫端。import 名 jedi_oscal_v2,dist jedi-oscal-v2。dev 走 poetry path dependency,feature 完成才推 Nexus。

Tech Stack: Python 3.11+ / SQLAlchemy 2.0 (mapped_column) / PostgreSQL (schema oscal + compliance) / Poetry / pytest / jedi-common (BaseRepositoryImpl, BaseModel, exception, transaction)。

Scope: 本計畫只涵蓋 Wave 1(jedi-oscal-v2 套件)。主專案 BE(Wave 2)、FE(Wave 3)另立計畫。契約基準:design.md + oscal-v2-deltas.sql + requirement-analysis.md


§1

0. 前置:pre-flight 驗證(開工前必做,每個 agent 都要)

計畫撰寫到開工可能有時間差;method 改名 / 欄位改 shape / base 套件 signature 變動都可能發生。每個 module agent 開工第一步


§2

1. 平行化地圖(哪些能同時開 agent)

Phase 0  套件骨架 + DB 套用 + 共用層          【序列瓶頸,先做完】
   │
   ├─ A1 catalog 樹 + framework + profile + resolution   ┐
   ├─ A2 SSP + 12 子表 + UI→OSCAL 落點                    │ 契約定稿後
   ├─ A3 AP + reviewed-controls + subjects + tasks       ├ 5 模組可平行
   ├─ A4 AR 全量矩陣 + risk + remediation + milestone + POA&M │ (各自獨立 model)
   └─ A5 parser adapter(CMMC/ISO/NIST)+ 匯入匯出        ┘
   │
A6  OscalSnapshotService(clone/snapshot primitives)   【依賴 A1+A2,故排在其後】
   │
Phase Z  套件層整合 smoke(全 model import + DI wire 驗證)  【序列收尾】

A6 為什麼不在 A1-A5 平行群:clone_resource_library 要同時複製 catalog(A1)+profile(A1)+ssp(A2) 三件組、snapshot_ssp 要深複製 SSP 12 子表(A2),橫跨 A1+A2 的 entity,不屬單一模組。故排在 A1+A2 的 model+mapper 落地後做(可由 A2 的 agent 接手,因它已有 SSP deep-copy 程式)。A5 的 export round-trip 測試 fixture 依賴 A6 產出的 cloned/frozen 物件,A6 完成後再補 A5 export 測試。

A1-A5 為什麼能平行:各模組對應不同 OSCAL model 的表,entity/model/mapper/repo/service 檔案不重疊;跨模組只靠「token / uuid 軟參照」(不建 FK 到別模組的 Python 物件),無共享可變狀態。唯一共用 = Phase 0 的 base class / common enum / DI container 骨架,故 Phase 0 必須先完成。 A4 內部順序:AR finding → risk → remediation → milestone 有 FK 依賴,同一 agent 內依序做;POA&M item 可獨立。 A1 的 catalog 是 A2/A3/A4 的「token 來源」但只是 token 字串參照,不阻塞平行(A2-A5 用測試假資料的 control_id token 即可)。


§3

2. 檔案結構(DDD 佈局,決定分工邊界)

~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2/
├── pyproject.toml                         # dist=jedi-oscal-v2, packages=jedi_oscal_v2
├── jedi_oscal_v2/
│   ├── common/
│   │   ├── enum/oscal_enums.py            # StrEnum: RoundStatus, RoundType, FindingState, RiskStatus, ...
│   │   └── code/oscal_v2_error_code.py    # 套件層 ErrorCode(OSCAL_V2_*)
│   ├── domain/
│   │   ├── entity/<model>/...             # @dataclass entity(純資料,無 ORM)
│   │   ├── repository/<model>/..._repo.py # abstract repo interface
│   │   └── service/<model>/..._service.py # domain service(業務邏輯)
│   ├── infra/
│   │   ├── model/<model>/...              # SQLAlchemy model(mapped_column)
│   │   ├── mapper/<model>/..._mapper.py   # entity ↔ model 雙向
│   │   ├── repository/<model>/..._impl.py # BaseRepositoryImpl 衍生
│   │   └── adapter/{cmmc,iso,nist}/       # parser adapter(A5)
│   ├── app/
│   │   ├── service/<model>/...            # app service(對外,主專案 import 點)
│   │   └── dto/<model>/...                # app DTO
│   └── ports/oscal_parser_factory.py      # parser factory(A5)
└── tests/
    └── <model>/test_*.py

<model>framework / catalog / profile / ssp / ap / ar / poam / common(共用 metadata/roles/parties/resources 歸 common)。


§4

3. 共用慣例(所有 module agent 必守)

3.1 Entity(domain,純 dataclass)

from dataclasses import dataclass, field
@dataclass
class CatalogControlEntity:
    id: int | None = None
    uid: str | None = None            # 若該表有對外 uid(root 有 uuid;子物件用自身 uuid)
    control_id: str | None = None     # OSCAL token
    title: str | None = None
    # ... 對齊 delta/base 欄位;JSONB 欄用 dict | list | None

3.2 Model(infra,SQLAlchemy 2.0)

from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import BigInteger, Text, ForeignKey
from jedi_common.session.database.base_model import BaseModel  # pre-flight 確認實際 base
class CatalogControlModel(BaseModel):
    __tablename__ = "catalog_controls"
    __table_args__ = {"schema": "oscal"}
    id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
    catalog_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("oscal.catalogs.id", ondelete="CASCADE"))
    control_id: Mapped[str] = mapped_column(Text, nullable=False)
    # Python 保留字:class → mapped_column("class", ...) attr class_;metadata → metadata_id + attr metadata_
  • NOT NULL ⟺ delta/base 標 [1];JSONB 用 mapped_column(JSONB)
  • 跨 schema FK(如 compliance.project_audit_roundsoscal.*)字串必帶 schema 前綴(記憶 feedback_cross_schema_fk_must_qualify)。

3.3 Repo(infra)

class CatalogControlRepoImpl(ICatalogControlRepo,
        BaseRepositoryImpl[CatalogControlEntity, CatalogControlQuery, CatalogControlModel, CatalogControlMapper]):
    def __init__(self):
        super().__init__(mapper=CatalogControlMapper, model=CatalogControlModel)
    # self.session 自動有(lazy);禁止 __init__ 寫 self.session = get_session()

3.4 App Service(對外)

class CatalogService:
    def __init__(self, catalog_domain_service, ...):
        self._svc = catalog_domain_service
    # 注意:套件 app service 不自己加 @transaction —— 由主專案 caller 開 scope。
    # 但套件可提供「被 @transaction 包住才安全」的 method,docstring 標明「caller 必須在 @transaction scope 內」。

3.5 測試

  • 每個 model:entity↔︎model mapper round-trip test + repo CRUD test(用 dev DB 或 in-memory 視 base 慣例)。
  • app service test:加 logger patch autouse fixture(記憶 feedback_test_logger_patch_db_handler),patch jedi_oscal_v2.app.service.<m>.<svc>.logger
  • commit 顆粒:一個表的 entity+model+mapper+repo+test 綠 → 一個 commit。

§5

Phase 0 — 套件骨架(序列,~半天;先完成才能平行)

Task 0.1: 建立套件骨架 + pyproject

Files:

  • Create: ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2/pyproject.toml
  • Create: jedi-oscal-v2/jedi_oscal_v2/__init__.py(+ 各層 __init__.py

Task 0.2: DB schema 套用(dev,drop & rebuild)

Task 0.3: common 層(enum + error code)

Files: Create jedi_oscal_v2/common/enum/oscal_enums.pycommon/code/oscal_v2_error_code.py

Task 0.4: BaseEntity / BaseQuery / mapper 基底(若舊套件有共用基底就 port)


§6

4. канonical per-table pattern(A1-A5 每張表都照這個做)

這是「一張表」的標準 5 步 TDD micro-cycle。A1-A5 的每個表 task 都展開成這 5 步(agent 執行時照此 pattern,不需在計畫重抄)。以 framework_versions 為例:

JSONB 欄:entity 用 dict|list|None,mapper 直通,不展開。 自巢狀表(catalog_groups/controls/parts、ap_tasks):parent_id self-FK,repo 提供 get_children(parent_id) + get_tree(root_id)


§7

Phase A1 — catalog 樹 + framework + profile(基礎模組)

負責 agent:A1。Files: */common/**/framework/**/catalog/**/profile/*tests/{common,framework,catalog,profile}/*

⚠️ A1 是所有 root 的共同前置:OSCAL 共用層 oscal.metadata / roles / parties / resources每個 root(catalog/profile/ssp/ap/ar/poam)的 model FK 參照(metadata_id 等)。A1 必須先建這 4 張 common 表的 model,A2-A5 才能建各自 root(依賴 A1 的 metadata model)。這使「A1 先行、A2-A5 再平行」成為硬順序。

A1-0 表清單:OSCAL 共用層(§4 pattern,A1 第一批做)

A1 表清單(各表照 §4 canonical pattern 各一輪 TDD + commit)

A1 特殊邏輯(非 boilerplate,full TDD)

Task A1-R: Profile Resolution(邊界①)

Files: Create domain/service/profile/profile_resolution_service.pytests/profile/test_profile_resolution.py

Task A1-S: AO 查詢

Task A1-App: FrameworkService / CatalogService / ProfileService(app 層)


§8

Phase A2 — SSP + 12 子表 + UI→OSCAL 落點

負責 agent:A2。Files: */ssp/*tests/ssp/*

A2 表清單(§4 pattern)

A2 特殊邏輯

Task A2-SoA: SoA / 實作狀態落點(requirement-analysis §5)

Task A2-App: SspService + 子物件 CRUD app service(含 deep_clone_ssp 的 entity 級複製能力,供 A4/Wave2 用)


§9

Phase A3 — AP + reviewed-controls + subjects + tasks

負責 agent:A3。Files: */ap/*tests/ap/*

A3 表清單(§4 pattern)

A3 特殊邏輯

Task A3-Draft: AP 草稿自動生成(requirement-analysis §4.5)

Task A3-App: AssessmentPlanService(create / generate_draft / set_reviewed_controls / set_assessment_subjects / set_tasks)


§10

Phase A4 — AR 全量矩陣 + risk + remediation + milestone + POA&M

負責 agent:A4(內部依序:finding→risk→remediation→milestone)。Files: */ar/**/poam/*common/(observation/risk/finding 共用)、tests/{ar,poam}/*

A4 表清單(§4 pattern)

A4 特殊邏輯(核心,full TDD)

Task A4-Matrix: AO 全量判定矩陣(Q3)

Task A4-Risk: 風險手動組(Q2)

Task A4-Remed: 整改三層(PM2 / B 方案)

Task A4-Poam: POA&M 從 findings 生成(不複製)

Task A4-App: AssessmentResultService / AssessmentRiskService / PoamService(含 list_remediations / list_milestones)


§11

Phase A5 — parser adapter + 匯入匯出

負責 agent:A5。Files: infra/adapter/{cmmc,iso,nist}/*ports/oscal_parser_factory.pyapp/service/oscal_io_service.pytests/adapter/*


§12

Phase A6 — OscalSnapshotService(clone / snapshot primitives,邊界②③)

負責 agent:A6(建議由 A2 agent 接手,重用 SSP deep-copy)。依賴:A1(catalog/profile entity+mapper)+ A2(SSP 12 子表 entity+mapper + deep_clone_ssp)已落地。Files: Create app/service/snapshot/oscal_snapshot_service.pytests/snapshot/*

這是 design §1 四個「複製並固定」邊界中的②③,是架構核心。clone=可編副本、snapshot=凍結副本,但兩者都實際複製內容(獨立 rows、不存 id join 回母表)。

Task A6-Clone: clone_resource_library(framework_version_uid)(邊界②,clone)

Task A6-Snapshot: snapshot_ssp(ssp_id) → frozen_ssp_id(邊界③,snapshot)

Task A6-App: 對外 OscalSnapshotService(clone_resource_library / snapshot_ssp / deep_clone_ssp 統一出口)+ app service test(logger patch fixture)。

完成後回頭補 A5 的 export round-trip 測試 fixture(用 A6 產出的 cloned/frozen 物件當輸入)。


§13

Phase Z — 套件整合 smoke(序列收尾)


§14

5. 執行注意(全 agent 通用)

  • 顯式 git add 檔名、禁用 -am(記憶 feedback_subagent_explicit_git_add);套件 repo 與主專案分開 commit;push 等 user
  • 不切 branch;pyproject path-dep 改動 dev-only 不 commit。
  • 改 service 後若主專案要驗,提醒 user 重啟 BE(套件源碼改 BE 重啟即生效)。
  • 跨 schema FK 字串必帶 schema 前綴。