↑ 需求首頁 ⌂ 需求中心

Module Frame Template Defaults Phase 2 — 補齊 4 張 mf-default 表 + endpoint + FE

項目 內容
緣由 Bug T 真因分析發現 mf template-edit 上 4 個 tab(系統元件 / 外部利用服務 / 受評標的 / 資產)的 default 資料沒對應的 mf-scoped 表。BE 過去 hack 寫到「該 mf 第一個 SSP 的子表」,造成:① user 寫的東西看不到自己 ② docx import 累積廢物 SSP shell ③ OSCAL project 啟動沒帶這 4 tab default 過去
Phase 1 已 ship(control / AO / 程序書 3 張 default 表)— 見 docs/features/FR-018-2604-module-frame-template-defaults/本期 Phase 2 補剩下 4 張 + 跨 repo FE 改動
Branch 接手 session 必須 user 自己切 feature/mf-template-defaults-phase2 branch 後才開工(不自己 checkout)
估時 3 天(BE 2 天 + FE 1 天 + E2E verify)
接手 session 用 讀本文件即可開工,§Tasks 是執行順序
影響 repo compliance-manager-be + compliance-manager-fe(jedi-oscal 不動)
執行 LLM Sonnet — spec 已寫死所有實際路徑 + 完整 code template,盡量無歧義

§1

§0 接手讀序

按本順序 read,再開工:

  1. 本文件 — root cause + 完整實作 plan
  2. 既有 Phase 1 specdocs/features/FR-018-2604-module-frame-template-defaults/design.md + implementation-plan.md — 既有 3 張 default 表怎麼建(mirror 同樣 pattern)
  3. Bug T 根因docs/features/FR-028-2605-ssp-oscal-alignment/design.md §11.39 — 完整 root cause
  4. 既有 mf control_default 完整套件(必 read,作為 4 套新 entity 的模板):
    • infra/module_frame/models/module_frame_control_default.py (66 行)
    • infra/module_frame/mapper/module_frame_control_default_mapper.py (68 行)
    • infra/module_frame/repository/module_frame_control_default_repo_impl.py (151 行)
    • domain/module_frame/repository/module_frame_control_default_repo.py (53 行)
    • domain/module_frame/entity/module_frame_control_default_entity.py (27 行)
    • domain/module_frame/entity/module_frame_control_default_query_entity.py (19 行)
    • domain/module_frame/service/module_frame_control_default_domain_service.py (60 行)
  5. 既有 ModuleFrameTemplateCopyService.copy()app/module_frame/service/module_frame_template_copy_service.py (300+ 行,看 Step 1-3 結構)
  6. 既有 mf-side leveraged service(架構債示範)app/module_frame/service/module_frame_leveraged_service.py (113 行,註解 L6-8 標 Task 11B-3 follow-up — 本期 Phase 2 一併解決)
  7. 既有 docx import 對 mf source pathapp/oscal/service/ssp_docx_import_app_service.py:_run_v2_bundle_confirm() (~106 行內)
  8. FE 入口compliance-manager-fe/src/views/module_frame/ModuleFrameTemplateEditView.vue L27 import SspComponentsLeveragedInventoryTab,L2030 使用該元件傳 sspUid
  9. FE 改寫對象compliance-manager-fe/src/components/grc/ssp/SspComponentsLeveragedInventoryTab.vue (1004 行,3 tab 合一元件,9 個 ssp API call 寫死) — 本期 Phase 5 拆 mf-only 版本

讀完跑 §Pre-flight + §Verify Hypotheses → §Tasks Phase 1 開工。


§2

§1 問題定義(白話)

mf(合規資源庫)= 一份「合規模板書」。OSCAL project 啟動時 snapshot 複製成該 project 的 SSP(系統安全計畫)。

1.1 mf 模板內容存哪?schema 對稱性破口

mf template-edit tab 存哪 OSCAL project 啟動時複製 mf-side endpoint
控制項 compliance.module_frame_control_defaults ✓ 有 ✓ 有
評估目標 (AO) compliance.module_frame_control_objective_defaults ✓ 有 ✓ 有
程序書 compliance.module_frame_reference_documents + _mappings ✓ 有 ✓ 有
系統元件 ❌ 沒對應的 mf-default 表 ❌ 沒複製邏輯 ❌ 沒 (FE reuse ssp-side endpoint)
外部利用服務 ❌ 沒對應的 mf-default 表 ❌ 沒複製邏輯 ✓ 有 (但 service 內部 hack 走 SSP)
受評標的 ❌ 沒對應的 mf-default 表 ❌ 沒複製邏輯 ✓ 有 (但 service 內部 hack 走 SSP)
資產 ❌ 沒對應的 mf-default 表 ❌ 沒複製邏輯 ❌ 沒 (FE reuse ssp-side endpoint)

1.2 過去的 BE hack

mf template-edit 的後 4 個 tab,BE 直接走「找該 mf 對應的某筆 SSP,把資料寫進 SSP 的子表」:

mf_uid
  → mf.oscal_profile_uid
    → profile.id
      → ssp.get_one(profile_id=profile.id)  # 拿第一個 hit (id 排序最舊)
        → 把資料寫到 ssp_components / ssp_leveraged_authorizations / etc.

1.3 三個 bug 來源

  1. 讀寫不對齊:GET endpoint 用 resolve_existing_shell 拿最舊 id 的 SSP;POST docx import 用 ensure_shell resolve 失敗 fallback create 新 SSP → user 寫的看不到自己
  2. 廢物 SSP 累積:每次 docx import resolve fail → fallback create → mf 343 累積 5 個 orphan shell (id 280-284)
  3. OSCAL project 啟動沒帶這 4 tab default:因為 mf 沒這些 default 表,啟動 project 也沒複製 → 新 project 永遠是空

§3

§2 目標

補齊 schema 對稱性:

                       【補完後 Phase 2】
  compliance.module_frame_*_defaults  (mf 範本層)
        │   control_defaults                                  ← Phase 1 已有
        │   control_objective_defaults                        ← Phase 1 已有
        │   reference_documents + reference_document_mappings ← Phase 1 已有
        │   component_defaults                                ← Phase 2 新增
        │   leveraged_authorization_defaults                  ← Phase 2 新增
        │   inventory_item_defaults                           ← Phase 2 新增
        │   system_characteristic_defaults                    ← Phase 2 新增
        ▼
  oscal.system_security_plans + oscal.ssp_*  (project 成品層)
        ssp + 7 個子表(control_impl / AO / refdoc / components / leveraged / inventory / system_characteristic)

完工後:

  • ✓ mf template-edit 4 個 tab 寫到對應的 mf-default 表
  • ✓ OSCAL project 啟動 snapshot 複製 7 張 default 全部
  • ✓ docx import 對 mf source 寫 mf-default 表,不再用 SspShellService 建 SSP shell
  • ✓ 廢物 SSP shell 不再累積
  • ✓ 讀寫對齊(template-edit 讀 mf-default,docx import 寫 mf-default)
  • ✓ FE 不再 reuse SspComponentsLeveragedInventoryTab.vue 傳 sspUid(改新建 mf-only 元件)

§4

§3 新表 schema 設計

3.1 compliance.module_frame_component_defaults

mirror oscal.ssp_components

CREATE TABLE compliance.module_frame_component_defaults (
    id                            SERIAL PRIMARY KEY,
    uid                           UUID NOT NULL DEFAULT gen_random_uuid(),
    module_frame_id               INTEGER NOT NULL,
    component_type                VARCHAR NOT NULL,
    title                         VARCHAR NOT NULL,
    description                   TEXT,
    purpose                       TEXT,
    status                        VARCHAR,
    leveraged_authorization_uid   UUID,  -- soft reference to module_frame_leveraged_authorization_defaults.uid
    props                         JSONB,
    created_at                    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
    updated_at                    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
    created_user                  VARCHAR,
    updated_user                  VARCHAR,
    UNIQUE (uid),
    UNIQUE (module_frame_id, title, component_type)
);

CREATE INDEX idx_module_frame_component_defaults_mf_id
    ON compliance.module_frame_component_defaults (module_frame_id);

GRANT SELECT, INSERT, UPDATE, DELETE ON compliance.module_frame_component_defaults TO cm_app;
GRANT USAGE, SELECT ON SEQUENCE compliance.module_frame_component_defaults_id_seq TO cm_app;

3.2 compliance.module_frame_leveraged_authorization_defaults

mirror oscal.ssp_leveraged_authorizations

CREATE TABLE compliance.module_frame_leveraged_authorization_defaults (
    id                  SERIAL PRIMARY KEY,
    uid                 UUID NOT NULL DEFAULT gen_random_uuid(),
    module_frame_id     INTEGER NOT NULL,
    title               VARCHAR NOT NULL,
    party_uuid          UUID,        -- soft reference to oscal_parties.uid (跨 schema)
    date_authorized     DATE,
    props               JSONB,
    remarks             TEXT,
    created_at          TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
    updated_at          TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
    created_user        VARCHAR,
    updated_user        VARCHAR,
    UNIQUE (uid),
    UNIQUE (module_frame_id, title)
);

CREATE INDEX idx_module_frame_leveraged_authorization_defaults_mf_id
    ON compliance.module_frame_leveraged_authorization_defaults (module_frame_id);

GRANT SELECT, INSERT, UPDATE, DELETE ON compliance.module_frame_leveraged_authorization_defaults TO cm_app;
GRANT USAGE, SELECT ON SEQUENCE compliance.module_frame_leveraged_authorization_defaults_id_seq TO cm_app;

3.3 compliance.module_frame_inventory_item_defaults

mirror oscal.ssp_inventory_items

CREATE TABLE compliance.module_frame_inventory_item_defaults (
    id                  SERIAL PRIMARY KEY,
    uid                 UUID NOT NULL DEFAULT gen_random_uuid(),
    module_frame_id     INTEGER NOT NULL,
    description         TEXT NOT NULL,
    props               JSONB,
    created_at          TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
    updated_at          TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
    created_user        VARCHAR,
    updated_user        VARCHAR,
    UNIQUE (uid)
);

CREATE INDEX idx_module_frame_inventory_item_defaults_mf_id
    ON compliance.module_frame_inventory_item_defaults (module_frame_id);

GRANT SELECT, INSERT, UPDATE, DELETE ON compliance.module_frame_inventory_item_defaults TO cm_app;
GRANT USAGE, SELECT ON SEQUENCE compliance.module_frame_inventory_item_defaults_id_seq TO cm_app;

3.4 compliance.module_frame_system_characteristic_defaults

mirror oscal.system_security_plans_system_characteristics(每個 mf 唯一):

CREATE TABLE compliance.module_frame_system_characteristic_defaults (
    id                            SERIAL PRIMARY KEY,
    uid                           UUID NOT NULL DEFAULT gen_random_uuid(),
    module_frame_id               INTEGER NOT NULL,
    name                          VARCHAR NOT NULL,
    description                   TEXT,
    system_identifier             VARCHAR,
    security_sensitivity_level    VARCHAR,
    target_type                   VARCHAR,
    scope_description             TEXT,
    status                        VARCHAR,
    owner_uid                     VARCHAR,
    created_at                    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
    updated_at                    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
    created_user                  VARCHAR,
    updated_user                  VARCHAR,
    UNIQUE (uid),
    UNIQUE (module_frame_id)
);

CREATE INDEX idx_module_frame_system_characteristic_defaults_mf_id
    ON compliance.module_frame_system_characteristic_defaults (module_frame_id);

GRANT SELECT, INSERT, UPDATE, DELETE ON compliance.module_frame_system_characteristic_defaults TO cm_app;
GRANT USAGE, SELECT ON SEQUENCE compliance.module_frame_system_characteristic_defaults_id_seq TO cm_app;

3.5 Schema 設計注意事項

  • 不加 FK to public.module_frames:跨 schema FK 是 soft reference(per CLAUDE.md「跨 schema FK 用 Integer soft reference」)
  • leveraged_authorization_uid 跨表 soft ref:component → leveraged 的 link 用 uid 不用 id
  • created_user / updated_user audit 欄位:app service 寫入必填(user.login_name)
  • tenant_id / org_unit_id:mf 是 system-level 範本,所有 tenant 共用同個 mf

§5

§Pre-flight Command(開工前必跑)

# 1. branch + working tree
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
git branch --show-current
# 預期:feature/mf-template-defaults-phase2 (由 user 先 checkout)

git status --short
# 預期:working tree 乾淨(或只有 dev-only 改動如 pyproject.toml path dep)

# 2. BE listener
lsof -t -i:8000 && echo "BE up" || echo "BE down"
# 後面 Phase 1 跑完要重啟,現在不一定要 up

# 3. 既有 pytest 通過
poetry run pytest tests/ -q
# 預期:234+ tests pass

# 4. DB 連線(cmmgr)
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -c "SELECT 1"
# 預期:?column? = 1

# 5. FE repo branch
cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
git branch --show-current
# 預期:feature/mf-template-defaults-phase2 (由 user 先 checkout)
git status --short
# 預期:working tree 乾淨

§6

§Verify Hypotheses(開工前必跑)

# A. 確認 4 張新表確實不存在
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -A -c "
SET app.is_super_admin='t';
SELECT table_name FROM information_schema.tables
 WHERE table_schema='compliance' AND table_name LIKE 'module_frame_%default%' ORDER BY table_name;
"
# 預期:只有 control_defaults, control_objective_defaults, reference_documents, reference_document_mappings (4 row)

# B. 確認既有 ModuleFrameTemplateCopyService.copy 只 cover control / AO / refdoc
grep -nE 'def copy|def _copy_|component|leveraged|inventory|system_characteristic' \
  ~/Projects/Billows/Audit-Manager/compliance-manager-be/app/module_frame/service/module_frame_template_copy_service.py
# 預期:完全沒看到 component/leveraged/inventory/system_characteristic 相關 method

# C. 確認既有 mf-side leveraged endpoint 是 hack(走 profile_id → ssp.get_one)
grep -nA 3 'def _resolve_ssp_id' \
  ~/Projects/Billows/Audit-Manager/compliance-manager-be/app/module_frame/service/module_frame_leveraged_service.py
# 預期:看到 profile_id → ssp.get_one(profile_id=...)

# D. 確認 FE SspComponentsLeveragedInventoryTab.vue 只接 sspUid props
grep -nE 'defineProps|sspService\.|apiBase' \
  ~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/components/grc/ssp/SspComponentsLeveragedInventoryTab.vue | head -15
# 預期:props 只有 sspUid,9 個 sspService.* API call 寫死

# E. 確認 既有 mf control_default 套件 7 個檔案存在
ls ~/Projects/Billows/Audit-Manager/compliance-manager-be/{infra,domain,app,api}/module_frame/**/module_frame_control_default*.py 2>/dev/null | wc -l
# 預期:11 files (含 dto / route 等)

任一條 verify 不符 → 停下回報 spec 作者(前 session),不開工。


§Tasks — Phase 1: DB Migration

§7

Task 1.1: 寫 SQL migration script

File (新建): scripts/sql/YYYY-MM-DD-mf-template-defaults-phase2.sql

寫法照 §3.1~3.4 4 個 CREATE TABLE block + INDEX + GRANT。檔案頭加 -- Date: YYYY-MM-DD,每個 CREATE 語句後加日期註解(per CLAUDE.md SQL Migration 規範)。

Step 1.1.1 — Write SQL file

整份 SQL:

-- Date: YYYY-MM-DD
-- Module Frame Template Defaults Phase 2 — 補 4 張 mf-scoped default 表
-- (component / leveraged_authorization / inventory_item / system_characteristic)
--
-- 參考 docs/features/FR-018.2-2605-module-frame-template-defaults-phase2/design.md §3

BEGIN;

-- 1. module_frame_component_defaults (YYYY-MM-DD)
CREATE TABLE compliance.module_frame_component_defaults (
    id                            SERIAL PRIMARY KEY,
    uid                           UUID NOT NULL DEFAULT gen_random_uuid(),
    module_frame_id               INTEGER NOT NULL,
    component_type                VARCHAR NOT NULL,
    title                         VARCHAR NOT NULL,
    description                   TEXT,
    purpose                       TEXT,
    status                        VARCHAR,
    leveraged_authorization_uid   UUID,
    props                         JSONB,
    created_at                    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
    updated_at                    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
    created_user                  VARCHAR,
    updated_user                  VARCHAR,
    UNIQUE (uid),
    UNIQUE (module_frame_id, title, component_type)
);
CREATE INDEX idx_module_frame_component_defaults_mf_id
    ON compliance.module_frame_component_defaults (module_frame_id);
GRANT SELECT, INSERT, UPDATE, DELETE ON compliance.module_frame_component_defaults TO cm_app;
GRANT USAGE, SELECT ON SEQUENCE compliance.module_frame_component_defaults_id_seq TO cm_app;

-- 2. module_frame_leveraged_authorization_defaults (YYYY-MM-DD)
CREATE TABLE compliance.module_frame_leveraged_authorization_defaults (
    id                  SERIAL PRIMARY KEY,
    uid                 UUID NOT NULL DEFAULT gen_random_uuid(),
    module_frame_id     INTEGER NOT NULL,
    title               VARCHAR NOT NULL,
    party_uuid          UUID,
    date_authorized     DATE,
    props               JSONB,
    remarks             TEXT,
    created_at          TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
    updated_at          TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
    created_user        VARCHAR,
    updated_user        VARCHAR,
    UNIQUE (uid),
    UNIQUE (module_frame_id, title)
);
CREATE INDEX idx_module_frame_leveraged_authorization_defaults_mf_id
    ON compliance.module_frame_leveraged_authorization_defaults (module_frame_id);
GRANT SELECT, INSERT, UPDATE, DELETE ON compliance.module_frame_leveraged_authorization_defaults TO cm_app;
GRANT USAGE, SELECT ON SEQUENCE compliance.module_frame_leveraged_authorization_defaults_id_seq TO cm_app;

-- 3. module_frame_inventory_item_defaults (YYYY-MM-DD)
CREATE TABLE compliance.module_frame_inventory_item_defaults (
    id                  SERIAL PRIMARY KEY,
    uid                 UUID NOT NULL DEFAULT gen_random_uuid(),
    module_frame_id     INTEGER NOT NULL,
    description         TEXT NOT NULL,
    props               JSONB,
    created_at          TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
    updated_at          TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
    created_user        VARCHAR,
    updated_user        VARCHAR,
    UNIQUE (uid)
);
CREATE INDEX idx_module_frame_inventory_item_defaults_mf_id
    ON compliance.module_frame_inventory_item_defaults (module_frame_id);
GRANT SELECT, INSERT, UPDATE, DELETE ON compliance.module_frame_inventory_item_defaults TO cm_app;
GRANT USAGE, SELECT ON SEQUENCE compliance.module_frame_inventory_item_defaults_id_seq TO cm_app;

-- 4. module_frame_system_characteristic_defaults (YYYY-MM-DD)
CREATE TABLE compliance.module_frame_system_characteristic_defaults (
    id                            SERIAL PRIMARY KEY,
    uid                           UUID NOT NULL DEFAULT gen_random_uuid(),
    module_frame_id               INTEGER NOT NULL,
    name                          VARCHAR NOT NULL,
    description                   TEXT,
    system_identifier             VARCHAR,
    security_sensitivity_level    VARCHAR,
    target_type                   VARCHAR,
    scope_description             TEXT,
    status                        VARCHAR,
    owner_uid                     VARCHAR,
    created_at                    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
    updated_at                    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
    created_user                  VARCHAR,
    updated_user                  VARCHAR,
    UNIQUE (uid),
    UNIQUE (module_frame_id)
);
CREATE INDEX idx_module_frame_system_characteristic_defaults_mf_id
    ON compliance.module_frame_system_characteristic_defaults (module_frame_id);
GRANT SELECT, INSERT, UPDATE, DELETE ON compliance.module_frame_system_characteristic_defaults TO cm_app;
GRANT USAGE, SELECT ON SEQUENCE compliance.module_frame_system_characteristic_defaults_id_seq TO cm_app;

COMMIT;

Step 1.1.2 — Run migration

cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
  -f scripts/sql/YYYY-MM-DD-mf-template-defaults-phase2.sql

預期 output:

BEGIN
CREATE TABLE
CREATE INDEX
GRANT
GRANT
... (重複 4 次)
COMMIT

Step 1.1.3 — Verify

PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -A -c "
SET app.is_super_admin='t';
SELECT table_name FROM information_schema.tables
 WHERE table_schema='compliance' AND table_name LIKE 'module_frame_%default%' ORDER BY table_name;
"

預期 8 row:

  • module_frame_component_defaults
  • module_frame_control_defaults
  • module_frame_control_objective_defaults
  • module_frame_inventory_item_defaults
  • module_frame_leveraged_authorization_defaults
  • module_frame_reference_document_mappings
  • module_frame_reference_documents
  • module_frame_system_characteristic_defaults

Step 1.1.4 — Commit

git add scripts/sql/YYYY-MM-DD-mf-template-defaults-phase2.sql
git commit -m "$(cat <<'EOF'
feat(mf-template-defaults-phase2): SQL migration — 4 張 mf-scoped default 表

新增 4 張 compliance.module_frame_*_defaults 表,補齊 schema 對稱性:
- module_frame_component_defaults
- module_frame_leveraged_authorization_defaults
- module_frame_inventory_item_defaults
- module_frame_system_characteristic_defaults

詳見 docs/features/FR-018.2-2605-module-frame-template-defaults-phase2/design.md §3。

下一步:Phase 2 加 BE entity/repo/service 4 套。

Co-Authored-By: Claude Sonnet (1M context) <noreply@anthropic.com>
EOF
)"

§Tasks — Phase 2: BE Entity / Repo / Domain Service (4 套 mirror pattern)

§8

2.0 共通模板(基於既有 mf_control_default 改寫)

每張新表 7 個檔案。完整範例見 §2.A component_default 完整模板(下面)。其他 3 套 mirror 同 pattern。

2.0.1 既有 mf_control_default 7 個檔案路徑

infra/module_frame/models/module_frame_control_default.py                     ← SQLAlchemy ORM model
infra/module_frame/mapper/module_frame_control_default_mapper.py              ← model ↔ entity 雙向 mapper
infra/module_frame/repository/module_frame_control_default_repo_impl.py       ← repo 實作
domain/module_frame/repository/module_frame_control_default_repo.py           ← repo abstract interface
domain/module_frame/entity/module_frame_control_default_entity.py             ← domain entity
domain/module_frame/entity/module_frame_control_default_query_entity.py       ← query entity (for filter)
domain/module_frame/service/module_frame_control_default_domain_service.py    ← domain service

2.0.2 DI wire 位置

di_containers/module_frame/module_frame_containers.py — 註冊 4 套新 domain service


§9

§2.A module_frame_component_default 完整模板(第一套 — sonnet copy-modify 用)

2.A.1 — infra/module_frame/models/module_frame_component_default.py

"""SQLAlchemy ORM model for compliance.module_frame_component_defaults."""
import uuid
from datetime import datetime
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint
from sqlalchemy.dialects.postgresql import JSONB, UUID

from jedi_common.session.database.base_model import Base


class ModuleFrameComponentDefault(Base):
    __tablename__ = "module_frame_component_defaults"
    __table_args__ = (
        UniqueConstraint("uid", name="module_frame_component_defaults_uid_key"),
        UniqueConstraint(
            "module_frame_id", "title", "component_type",
            name="module_frame_component_defaults_mf_title_type_key",
        ),
        {"schema": "compliance"},
    )

    id = Column(Integer, primary_key=True)
    uid = Column(UUID(as_uuid=True), nullable=False, default=uuid.uuid4)
    module_frame_id = Column(Integer, nullable=False)
    component_type = Column(String, nullable=False)
    title = Column(String, nullable=False)
    description = Column(Text)
    purpose = Column(Text)
    status = Column(String)
    leveraged_authorization_uid = Column(UUID(as_uuid=True))
    props = Column(JSONB)
    created_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow)
    updated_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
    created_user = Column(String)
    updated_user = Column(String)

2.A.2 — domain/module_frame/entity/module_frame_component_default_entity.py

"""Domain entity for ModuleFrame component default."""
import uuid
from datetime import datetime
from typing import Optional


class ModuleFrameComponentDefaultEntity:
    def __init__(
        self,
        id: Optional[int] = None,
        uid: Optional[uuid.UUID] = None,
        module_frame_id: Optional[int] = None,
        component_type: Optional[str] = None,
        title: Optional[str] = None,
        description: Optional[str] = None,
        purpose: Optional[str] = None,
        status: Optional[str] = None,
        leveraged_authorization_uid: Optional[uuid.UUID] = None,
        props: Optional[dict] = None,
        created_at: Optional[datetime] = None,
        updated_at: Optional[datetime] = None,
        created_user: Optional[str] = None,
        updated_user: Optional[str] = None,
    ):
        self.id = id
        self.uid = uid
        self.module_frame_id = module_frame_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.created_at = created_at
        self.updated_at = updated_at
        self.created_user = created_user
        self.updated_user = updated_user

2.A.3 — domain/module_frame/entity/module_frame_component_default_query_entity.py

"""Query entity for ModuleFrame component default filtering."""
import uuid
from typing import Optional


class ModuleFrameComponentDefaultQueryEntity:
    def __init__(
        self,
        id: Optional[int] = None,
        uid: Optional[uuid.UUID] = None,
        module_frame_id: Optional[int] = None,
        component_type: Optional[str] = None,
        title: Optional[str] = None,
    ):
        self.id = id
        self.uid = uid
        self.module_frame_id = module_frame_id
        self.component_type = component_type
        self.title = title

2.A.4 — infra/module_frame/mapper/module_frame_component_default_mapper.py

"""model ↔ entity mapper for ModuleFrameComponentDefault."""
from typing import List, Optional

from domain.module_frame.entity.module_frame_component_default_entity import (
    ModuleFrameComponentDefaultEntity,
)
from infra.module_frame.models.module_frame_component_default import (
    ModuleFrameComponentDefault,
)


class ModuleFrameComponentDefaultMapper:
    @staticmethod
    def to_entity(model: ModuleFrameComponentDefault) -> ModuleFrameComponentDefaultEntity:
        return ModuleFrameComponentDefaultEntity(
            id=model.id,
            uid=model.uid,
            module_frame_id=model.module_frame_id,
            component_type=model.component_type,
            title=model.title,
            description=model.description,
            purpose=model.purpose,
            status=model.status,
            leveraged_authorization_uid=model.leveraged_authorization_uid,
            props=model.props,
            created_at=model.created_at,
            updated_at=model.updated_at,
            created_user=model.created_user,
            updated_user=model.updated_user,
        )

    @staticmethod
    def to_list_entity(models: List[ModuleFrameComponentDefault]) -> List[ModuleFrameComponentDefaultEntity]:
        return [ModuleFrameComponentDefaultMapper.to_entity(m) for m in models]

    @staticmethod
    def to_model(
        entity: ModuleFrameComponentDefaultEntity,
        model: Optional[ModuleFrameComponentDefault] = None,
    ) -> ModuleFrameComponentDefault:
        m = model or ModuleFrameComponentDefault()
        if entity.uid is not None:
            m.uid = entity.uid
        m.module_frame_id = entity.module_frame_id
        m.component_type = entity.component_type
        m.title = entity.title
        m.description = entity.description
        m.purpose = entity.purpose
        m.status = entity.status
        m.leveraged_authorization_uid = entity.leveraged_authorization_uid
        m.props = entity.props
        if entity.created_user is not None:
            m.created_user = entity.created_user
        if entity.updated_user is not None:
            m.updated_user = entity.updated_user
        return m

2.A.5 — domain/module_frame/repository/module_frame_component_default_repo.py

"""Abstract repo interface for ModuleFrameComponentDefault."""
from abc import abstractmethod
from typing import List, Optional

from jedi_common.session.database.repository.base_repository import IBaseRepository

from domain.module_frame.entity.module_frame_component_default_entity import (
    ModuleFrameComponentDefaultEntity,
)
from domain.module_frame.entity.module_frame_component_default_query_entity import (
    ModuleFrameComponentDefaultQueryEntity,
)


class IModuleFrameComponentDefaultRepo(IBaseRepository[
    ModuleFrameComponentDefaultEntity,
    ModuleFrameComponentDefaultQueryEntity,
]):
    @abstractmethod
    def get_by_uid(self, uid: str) -> Optional[ModuleFrameComponentDefaultEntity]:
        ...

    @abstractmethod
    def get_all_by_mf_id(self, module_frame_id: int) -> List[ModuleFrameComponentDefaultEntity]:
        ...

    @abstractmethod
    def delete_by_uid(self, uid: str) -> bool:
        ...

2.A.6 — infra/module_frame/repository/module_frame_component_default_repo_impl.py

mirror module_frame_control_default_repo_impl.py (既有 151 行) — copy 改名 + entity / mapper / model import 即可。Sonnet 直接抄那檔改 4 個 import name。

2.A.7 — domain/module_frame/service/module_frame_component_default_domain_service.py

mirror module_frame_control_default_domain_service.py (既有 60 行) — copy 改名 + repo / entity import 即可。

2.A.8 — DI wire 進 container

File: di_containers/module_frame/module_frame_containers.py

加新 provider(既有 control_default 已有 wire,看那邊 pattern):

# Add to ModuleFrameContainer providers (位置在既有 module_frame_control_default_domain_service 後面)
module_frame_component_default_repo = providers.Singleton(
    ModuleFrameComponentDefaultRepoImpl,
)
module_frame_component_default_domain_service = providers.Factory(
    ModuleFrameComponentDefaultDomainService,
    repo=module_frame_component_default_repo,
)

完整位置由 sonnet 看既有 container 找對應 pattern 插入。


§10

§2.B ~ §2.D 其他 3 套(leveraged / inventory / system_characteristic)

按 §2.A 同樣 pattern 寫,每套 7 個檔案 + DI wire。Field 列表照 §3.2 ~ §3.4 SQL schema。

每套寫完跑 unit test(§2.E)→ commit 一次(4 commits)。


§11

§2.E Tests

2.E.1 — 加單元測試

對每套 domain service 寫 tests/test_module_frame_<entity>_default_domain_service.py

"""Unit tests for ModuleFrameComponentDefaultDomainService."""
from unittest.mock import MagicMock
from types import SimpleNamespace
import pytest

from domain.module_frame.entity.module_frame_component_default_entity import (
    ModuleFrameComponentDefaultEntity,
)
from domain.module_frame.entity.module_frame_component_default_query_entity import (
    ModuleFrameComponentDefaultQueryEntity,
)
from domain.module_frame.service.module_frame_component_default_domain_service import (
    ModuleFrameComponentDefaultDomainService,
)


@pytest.fixture
def mock_repo():
    return MagicMock()


@pytest.fixture
def service(mock_repo):
    return ModuleFrameComponentDefaultDomainService(repo=mock_repo)


def test_add(service, mock_repo):
    entity = ModuleFrameComponentDefaultEntity(
        module_frame_id=343, component_type="service",
        title="Test Component", description="Test desc",
        created_user="alice",
    )
    mock_repo.add.return_value = SimpleNamespace(id=1, uid="mock-uid", **entity.__dict__)
    result = service.add(entity)
    mock_repo.add.assert_called_once_with(entity)
    assert result.id == 1


def test_get_all_by_mf_id(service, mock_repo):
    mock_repo.get_all_by_mf_id.return_value = [
        SimpleNamespace(id=1, title="Comp A"),
        SimpleNamespace(id=2, title="Comp B"),
    ]
    result = service.get_all_by_mf_id(343)
    assert len(result) == 2


def test_delete_by_uid(service, mock_repo):
    mock_repo.delete_by_uid.return_value = True
    assert service.delete_by_uid("test-uid") is True

2.E.2 — Run tests

poetry run pytest tests/test_module_frame_component_default_domain_service.py \
                  tests/test_module_frame_leveraged_authorization_default_domain_service.py \
                  tests/test_module_frame_inventory_item_default_domain_service.py \
                  tests/test_module_frame_system_characteristic_default_domain_service.py \
                  -q
# 預期:全 pass

2.E.3 — Commit Phase 2 (4 個 commit, 每套一個)

git add infra/module_frame/{models,mapper,repository}/module_frame_component_default*.py \
        domain/module_frame/{entity,repository,service}/module_frame_component_default*.py \
        di_containers/module_frame/module_frame_containers.py \
        tests/test_module_frame_component_default_domain_service.py

git commit -m "feat(mf-template-defaults-phase2): add component_default entity + repo + service

Module Frame Component Default 7 個檔案 + DI wire + unit test。
詳見 docs/features/FR-018.2-2605-module-frame-template-defaults-phase2/design.md §2.A。

Co-Authored-By: Claude Sonnet (1M context) <noreply@anthropic.com>"

對另外 3 套同樣方式 commit。


§Tasks — Phase 3: 擴充 ModuleFrameTemplateCopyService

§12

3.1 — 加 4 個 copy step

File: app/module_frame/service/module_frame_template_copy_service.py

既有 copy() 已有 Step 1-3 (control / AO / refdoc)。本期加 Step 4-7。

3.1.1 — Step 4: 複製 leveraged_authorizations(必先,因 components 會 ref)

在既有 Step 3c 結束後(line ~155)+ Step 4 SC update(line ~140)之間插入:

# Step 4 (Bug T Phase 2): 複製 mf leveraged_authorization defaults → ssp_leveraged_authorizations (INSERT)
# 必先於 Step 5 component(component 可能 ref leveraged_authorization_uid)。
# Build mf_la_uid → ssp_la_uid map 給 Step 5 用。
la_uid_map = {}
existing_la = session.execute(text("""
    INSERT INTO oscal.ssp_leveraged_authorizations (
        uid, ssp_id, title, party_uuid, date_authorized, props, remarks,
        tenant_id, created_user, updated_user, is_active
    )
    SELECT
        gen_random_uuid(),
        :ssp_id,
        mfld.title, mfld.party_uuid, mfld.date_authorized, mfld.props, mfld.remarks,
        :tenant_id, :curr_user, :curr_user, true
    FROM compliance.module_frame_leveraged_authorization_defaults mfld
    WHERE mfld.module_frame_id = :mf_id
    RETURNING uid, (SELECT mfld.uid FROM compliance.module_frame_leveraged_authorization_defaults mfld
                     WHERE mfld.module_frame_id = :mf_id
                       AND mfld.title = oscal.ssp_leveraged_authorizations.title LIMIT 1) AS mf_la_uid
"""), {"mf_id": module_frame_id, "ssp_id": ssp_id, "tenant_id": tenant_id, "curr_user": curr_user})
# Build mf_la_uid → ssp_la_uid map
for row in existing_la:
    la_uid_map[str(row.mf_la_uid)] = str(row.uid)
la_count = len(la_uid_map)

Caveat:上面 RETURNING subquery 拿 mf_la_uid 太脆弱(同 title 撞會錯)。改用兩段式:先 SELECT mf-default 進 Python 變數 → loop INSERT 一筆 + 對齊 build map。實際實作 sonnet 寫成兩段式更安全:

# 兩段式更安全版本
from sqlalchemy import text

# Step 4a: SELECT mf-default leveraged
mf_la_rows = session.execute(text("""
    SELECT uid, title, party_uuid, date_authorized, props, remarks
      FROM compliance.module_frame_leveraged_authorization_defaults
     WHERE module_frame_id = :mf_id
     ORDER BY id
"""), {"mf_id": module_frame_id}).mappings().all()

# Step 4b: For each, INSERT into oscal.ssp_leveraged_authorizations + build map
la_uid_map = {}
for mf_la in mf_la_rows:
    result = session.execute(text("""
        INSERT INTO oscal.ssp_leveraged_authorizations (
            uid, ssp_id, title, party_uuid, date_authorized, props, remarks,
            tenant_id, created_user, updated_user, is_active
        )
        VALUES (
            gen_random_uuid(), :ssp_id, :title, :party_uuid, :date_authorized,
            CAST(:props AS jsonb), :remarks,
            :tenant_id, :curr_user, :curr_user, true
        )
        RETURNING uid
    """), {
        "ssp_id": ssp_id,
        "title": mf_la["title"],
        "party_uuid": mf_la["party_uuid"],
        "date_authorized": mf_la["date_authorized"],
        "props": json.dumps(mf_la["props"]) if mf_la["props"] else None,
        "remarks": mf_la["remarks"],
        "tenant_id": tenant_id,
        "curr_user": curr_user,
    }).scalar()
    la_uid_map[str(mf_la["uid"])] = str(result)
la_count = len(la_uid_map)

3.1.2 — Step 5: 複製 components(用 la_uid_map 翻譯 leveraged_authorization_uid)

# Step 5 (Bug T Phase 2): 複製 mf component defaults → ssp_components (INSERT)
# leveraged_authorization_uid 用 la_uid_map 從 mf-default uid 翻成新建的 ssp uid。
mf_comp_rows = session.execute(text("""
    SELECT uid, component_type, title, description, purpose, status,
           leveraged_authorization_uid, props
      FROM compliance.module_frame_component_defaults
     WHERE module_frame_id = :mf_id
     ORDER BY id
"""), {"mf_id": module_frame_id}).mappings().all()

comp_count = 0
for mf_comp in mf_comp_rows:
    translated_la_uid = None
    if mf_comp["leveraged_authorization_uid"]:
        translated_la_uid = la_uid_map.get(str(mf_comp["leveraged_authorization_uid"]))
        if translated_la_uid is None:
            logger.warning(
                "[mf-template-copy] component '%s' refs leveraged_authorization_uid=%s "
                "but no mf_la_uid->ssp_la_uid mapping (mf-default 內 leveraged 已被 user 刪?) — set NULL",
                mf_comp["title"], mf_comp["leveraged_authorization_uid"],
            )
    session.execute(text("""
        INSERT INTO oscal.ssp_components (
            uid, ssp_id, component_type, title, description, purpose, status,
            leveraged_authorization_uid, props,
            tenant_id, created_user, updated_user, is_active
        )
        VALUES (
            gen_random_uuid(), :ssp_id, :component_type, :title, :description, :purpose, :status,
            :leveraged_authorization_uid, CAST(:props AS jsonb),
            :tenant_id, :curr_user, :curr_user, true
        )
    """), {
        "ssp_id": ssp_id,
        "component_type": mf_comp["component_type"],
        "title": mf_comp["title"],
        "description": mf_comp["description"],
        "purpose": mf_comp["purpose"],
        "status": mf_comp["status"],
        "leveraged_authorization_uid": translated_la_uid,
        "props": json.dumps(mf_comp["props"]) if mf_comp["props"] else None,
        "tenant_id": tenant_id,
        "curr_user": curr_user,
    })
    comp_count += 1

3.1.3 — Step 6: 複製 inventory_items(無 cross-ref)

# Step 6 (Bug T Phase 2): 複製 mf inventory defaults → ssp_inventory_items (INSERT, 無 cross-ref)
inv_result = session.execute(text("""
    INSERT INTO oscal.ssp_inventory_items (
        uid, ssp_id, description, props,
        tenant_id, created_user, updated_user, is_active
    )
    SELECT
        gen_random_uuid(),
        :ssp_id,
        mfid.description, mfid.props,
        :tenant_id, :curr_user, :curr_user, true
      FROM compliance.module_frame_inventory_item_defaults mfid
     WHERE mfid.module_frame_id = :mf_id
"""), {"mf_id": module_frame_id, "ssp_id": ssp_id, "tenant_id": tenant_id, "curr_user": curr_user})
inv_count = inv_result.rowcount

3.1.4 — Step 7: UPDATE system_characteristic(SSP 已在 OSCAL project Step 7.5 建 SC 空殼)

# Step 7 (Bug T Phase 2): 複製 mf system_characteristic default → SSP SC (UPDATE in place)
# Step 7.5 已建 SC 空殼 row,這邊 UPDATE 把 mf default 值填進去。
sc_result = session.execute(text("""
    UPDATE oscal.system_security_plans_system_characteristics sc
       SET
         name = COALESCE(mfsc.name, sc.name),
         description = COALESCE(mfsc.description, sc.description),
         system_identifier = COALESCE(mfsc.system_identifier, sc.system_identifier),
         security_sensitivity_level = COALESCE(mfsc.security_sensitivity_level, sc.security_sensitivity_level),
         target_type = COALESCE(mfsc.target_type, sc.target_type),
         scope_description = COALESCE(mfsc.scope_description, sc.scope_description),
         status = COALESCE(mfsc.status, sc.status),
         owner_uid = COALESCE(mfsc.owner_uid, sc.owner_uid),
         updated_user = :curr_user,
         updated_at = now()
      FROM compliance.module_frame_system_characteristic_defaults mfsc
     WHERE mfsc.module_frame_id = :mf_id
       AND sc.system_security_plan_id = :ssp_id
"""), {"mf_id": module_frame_id, "ssp_id": ssp_id, "curr_user": curr_user})
sc_updated = sc_result.rowcount

3.1.5 — 回傳擴充 stats

修改 copy() 最後 return:

return {
    "controls_updated": controls_updated,
    "objectives_updated": objectives_updated,
    # Phase 2 新增:
    "leveraged_inserted": la_count,
    "components_inserted": comp_count,
    "inventory_inserted": inv_count,
    "system_characteristic_updated": sc_updated,
}

3.1.6 — 加 import + tenant_id 取得

檔頭加 import jsontenant_id 取得:caller (start_oscal_project) 需傳進來 — 看既有 caller 怎麼傳,或從 user_context 拿。

3.1.7 — Test TDD

File (新建): tests/test_module_frame_template_copy_service_phase2.py

"""Phase 2 tests for ModuleFrameTemplateCopyService — 確認 Step 4-7 4 個新 copy step."""
# 用 fixtures + mock session 驗證 SQL 跑通 / row count 正確
# 詳細 test code sonnet 依既有 test pattern 寫

3.1.8 — Commit

git add app/module_frame/service/module_frame_template_copy_service.py \
        tests/test_module_frame_template_copy_service_phase2.py

git commit -m "feat(mf-template-defaults-phase2): copy_service 加 Step 4-7

擴充 ModuleFrameTemplateCopyService.copy() 加 4 個新 step:
- Step 4: copy mf_leveraged_authorization_defaults → ssp_leveraged_authorizations
- Step 5: copy mf_component_defaults → ssp_components (用 la_uid_map 翻譯 ref)
- Step 6: copy mf_inventory_item_defaults → ssp_inventory_items
- Step 7: UPDATE ssp.system_characteristic from mf_system_characteristic_defaults

OSCAL project 啟動時,這 4 個 tab default 也從 mf-template 帶到 project SSP。

詳見 design.md §3.1。

Co-Authored-By: Claude Sonnet (1M context) <noreply@anthropic.com>"

§Tasks — Phase 4: BE endpoint 改寫 + 新增

§13

4.0 endpoint inventory

Endpoint 既有 / 新增 改動
GET/POST /module-frame/<uid>/leveraged 既有 改 service 從 mf-default 讀寫
PUT/DELETE /module-frame/<uid>/leveraged/<item_uid> 既有 改 service 從 mf-default 讀寫
GET/PUT /module-frame/<uid>/system-characteristic 既有 改 service 從 mf-default 讀寫
GET/POST /module-frame/<uid>/components 新增 仿 leveraged route
PUT/DELETE /module-frame/<uid>/components/<item_uid> 新增 仿 leveraged route
GET/POST /module-frame/<uid>/inventory 新增 仿 leveraged route
PUT/DELETE /module-frame/<uid>/inventory/<item_uid> 新增 仿 leveraged route
GET /module-frame/<uid>/template-ssp 既有 可廢(FE 改 mf-side endpoint 後不再需要)
§14

4.1 — 改寫 ModuleFrameLeveragedService 從 mf-default 讀寫

File: app/module_frame/service/module_frame_leveraged_service.py (113 行)

整檔重寫,原本 delegate SspLeveragedContextService → 改 delegate ModuleFrameLeveragedAuthorizationDefaultDomainService。

"""ModuleFrame leveraged-authorization service (Bug T Phase 2 — mf-scoped storage)。

之前版本 (Track A, 2026-05-23) 是 hack:mf → profile_id → ssp.get_one → 寫 SSP 子表。
本期 Phase 2 改用 compliance.module_frame_leveraged_authorization_defaults 表
(mf-scoped storage),徹底解掉「廢物 SSP 累積」+「user 寫的看不到」bug。

Used by `/module-frame/<uid>/leveraged` REST endpoints.
"""
import uuid
from typing import Optional

from jedi_common.handler.exception import NotFound
from jedi_common.session.database.db import transaction

from common.code.grc_error_code import GrcErrorCode
from domain.module_frame.entity.module_frame_leveraged_authorization_default_entity import (
    ModuleFrameLeveragedAuthorizationDefaultEntity,
)
from domain.module_frame.service.module_frame_leveraged_authorization_default_domain_service import (
    ModuleFrameLeveragedAuthorizationDefaultDomainService,
)


class ModuleFrameLeveragedService:
    def __init__(
        self,
        module_frame_domain_service=None,
        mf_la_default_domain_service: ModuleFrameLeveragedAuthorizationDefaultDomainService = None,
    ):
        self._mf = module_frame_domain_service
        self._svc = mf_la_default_domain_service

    @transaction
    def list_items(self, module_frame_uid: str) -> list[dict]:
        mf = self._require_mf(module_frame_uid)
        entities = self._svc.get_all_by_mf_id(mf.id)
        return [self._to_dict(e) for e in entities]

    @transaction
    def add_item(self, module_frame_uid: str, payload: dict, user: str) -> dict:
        mf = self._require_mf(module_frame_uid)
        entity = ModuleFrameLeveragedAuthorizationDefaultEntity(
            module_frame_id=mf.id,
            title=payload.get("title", ""),
            party_uuid=payload.get("party_uuid"),
            date_authorized=payload.get("date_authorized"),
            props=payload.get("props"),
            remarks=payload.get("remarks"),
            created_user=user,
            updated_user=user,
        )
        added = self._svc.add(entity)
        return self._to_dict(added)

    @transaction
    def update_item(
        self, module_frame_uid: str, item_uid: str, payload: dict, user: str,
    ) -> dict:
        mf = self._require_mf(module_frame_uid)
        existing = self._svc.get_by_uid(item_uid)
        if existing is None or existing.module_frame_id != mf.id:
            raise NotFound(GrcErrorCode.GRC_SSP_RESOURCE_NOT_FOUND)
        # Apply patch
        for k in ("title", "party_uuid", "date_authorized", "props", "remarks"):
            if k in payload:
                setattr(existing, k, payload[k])
        existing.updated_user = user
        updated = self._svc.update(existing)
        return self._to_dict(updated)

    @transaction
    def delete_item(self, module_frame_uid: str, item_uid: str) -> None:
        mf = self._require_mf(module_frame_uid)
        existing = self._svc.get_by_uid(item_uid)
        if existing is None or existing.module_frame_id != mf.id:
            raise NotFound(GrcErrorCode.GRC_SSP_RESOURCE_NOT_FOUND)
        self._svc.delete_by_uid(item_uid)

    # ─── internals ──────────────────────────────────────────────────
    def _require_mf(self, module_frame_uid: str):
        mf = self._mf.get_module_frame_by_uid(module_frame_uid)
        if mf is None:
            raise NotFound(GrcErrorCode.GRC_MODULE_FRAME_NOT_FOUND)
        return mf

    @staticmethod
    def _to_dict(entity) -> dict:
        return {
            "uid": str(entity.uid) if entity.uid else None,
            "title": entity.title,
            "party_uuid": str(entity.party_uuid) if entity.party_uuid else None,
            "date_authorized": entity.date_authorized.isoformat() if entity.date_authorized else None,
            "props": entity.props,
            "remarks": entity.remarks,
            "created_user": entity.created_user,
            "updated_user": entity.updated_user,
        }

4.1.1 — DI wire 更新

di_containers/oscal/oscal_containers.pymodule_frame_leveraged_service provider 改 deps:

  • 移除 ssp_leveraged_context_service
  • mf_la_default_domain_service (Phase 2 新增的 service)

4.1.2 — Test update

既有 test tests/test_module_frame_leveraged_service.py 要改 — mock 從 SspLeveragedContextService 改 ModuleFrameLeveragedAuthorizationDefaultDomainService。

4.1.3 — Commit

git commit -m "feat(mf-template-defaults-phase2): ModuleFrameLeveragedService 改用 mf-default

之前 hack 走 profile_id → SSP;改用 compliance.module_frame_leveraged_authorization_defaults
mf-scoped 表。

詳見 design.md §4.1。

Co-Authored-By: Claude Sonnet (1M context) <noreply@anthropic.com>"
§15

4.2 — 改寫 ModuleFrameSystemCharacteristicService

File: app/module_frame/service/module_frame_system_characteristic_service.py (89 行)

同 4.1 pattern,改用 mf_sc_default_domain_service

注意 SC 是 每 mf 唯一(schema UNIQUE constraint on module_frame_id) — GET 回單筆 / PUT upsert。

§16

4.3 — 新建 components mf-side endpoint

4.3.1 — 新建 route

File (新建): api/module_frame/routes/module_frame_components_route.py

複製 module_frame_leveraged_route.py (88 行) 改名 + service import:

"""ModuleFrame components routes — `/module-frame/<uid>/components` CRUD."""
from dependency_injector.wiring import Provide, inject
from flask import request
from flask_apispec import MethodResource, doc
from flask_jwt_extended import jwt_required
from jedi_common.session.auth.auth_context import get_user_context

from app.module_frame.service.module_frame_components_service import (
    ModuleFrameComponentsService,
)
from common.util.response_util import return_response
from di_containers.containers import Containers


class ModuleFrameComponentsListResource(MethodResource):
    @doc(description="List components for this module_frame", tags=["ModuleFrame"])
    @jwt_required()
    @inject
    def get(
        self,
        uid: str,
        service: ModuleFrameComponentsService = Provide[
            Containers.oscal_container.module_frame_components_service
        ],
    ):
        result = service.list_items(uid)
        return return_response(True, result)

    @doc(description="Add a component", tags=["ModuleFrame"])
    @jwt_required()
    @inject
    def post(
        self,
        uid: str,
        service: ModuleFrameComponentsService = Provide[
            Containers.oscal_container.module_frame_components_service
        ],
    ):
        user = get_user_context().login_name
        payload = request.get_json(silent=True) or {}
        result = service.add_item(uid, payload, user)
        return return_response(True, result)


class ModuleFrameComponentsItemResource(MethodResource):
    @doc(description="Update a component", tags=["ModuleFrame"])
    @jwt_required()
    @inject
    def put(
        self,
        uid: str,
        item_uid: str,
        service: ModuleFrameComponentsService = Provide[
            Containers.oscal_container.module_frame_components_service
        ],
    ):
        user = get_user_context().login_name
        payload = request.get_json(silent=True) or {}
        result = service.update_item(uid, item_uid, payload, user)
        return return_response(True, result)

    @doc(description="Delete a component", tags=["ModuleFrame"])
    @jwt_required()
    @inject
    def delete(
        self,
        uid: str,
        item_uid: str,
        service: ModuleFrameComponentsService = Provide[
            Containers.oscal_container.module_frame_components_service
        ],
    ):
        service.delete_item(uid, item_uid)
        return return_response(True, None)

4.3.2 — 新建 service

File (新建): app/module_frame/service/module_frame_components_service.py

抄 §4.1 ModuleFrameLeveragedService 改 entity + 多帶 component_type / description / purpose / status / leveraged_authorization_uid field。

4.3.3 — 註冊 route

File: api/module_frame/__init__.py (Line 116~ 區域加新 resource)

from api.module_frame.routes.module_frame_components_route import (
    ModuleFrameComponentsItemResource,
    ModuleFrameComponentsListResource,
)

# (在既有 add_resource 附近加)
api.add_resource(ModuleFrameComponentsListResource,
    '/module-frame/<string:uid>/components')
api.add_resource(ModuleFrameComponentsItemResource,
    '/module-frame/<string:uid>/components/<string:item_uid>')

4.3.4 — DI wire

di_containers/oscal/oscal_containers.pymodule_frame_components_service provider。

4.3.5 — Test

tests/test_module_frame_components_service.py 寫 list / add / update / delete test。

4.3.6 — Commit

§17

4.4 — 新建 inventory mf-side endpoint

同 §4.3 pattern。

§18

4.5 — 廢 template-ssp endpoint (optional follow-up)

FILE: api/oscal/routes/module_frame_template_ssp_route.py

FE 改完不再呼這個 endpoint 後可廢。本期暫不刪(怕別處引用),標 @deprecated 註解 + log warning 即可。


§Tasks — Phase 5: FE 改動(跨 repo compliance-manager-fe)

§19

5.0 — 前置 — 跨 repo 必讀

MANDATORY:開 Phase 5 前 read FE CLAUDE.md(per memory feedback_cross_repo_read_claude_md_first):

head -300 ~/Projects/Billows/Audit-Manager/compliance-manager-fe/CLAUDE.md

確認 FE 規範後再動。

§20

5.1 — 新建 ModuleFrameComponentsLeveragedInventoryTab.vue

File (新建): compliance-manager-fe/src/components/grc/module-frame/ModuleFrameComponentsLeveragedInventoryTab.vue

重要決策:不改既有 SspComponentsLeveragedInventoryTab.vue (1004 行,9 個 API call 寫死 ssp endpoint),新建 mf-only 版本。長期可考慮共用,本期 separate 較安全。

Source:copy 既有 SspComponentsLeveragedInventoryTab.vue 全檔 → 改:

  • props 從 sspUid: StringmfUid: String
  • 9 個 sspService.* API call 改 mfService.* (新建 mfService.js)
  • watch 從 props.sspUidprops.mfUid

5.1.1 — 新建 mfService.js (or 加 method 到既有 service)

File: compliance-manager-fe/src/service/ModuleFrameService.js (若不存在則新建)

加 9 個 method:

  • listMfComponents(mfUid)GET /module-frame/<uid>/components
  • createMfComponent(mfUid, payload)POST /module-frame/<uid>/components
  • updateMfComponent(mfUid, item_uid, payload)PUT /module-frame/<uid>/components/<item_uid>
  • deleteMfComponent(mfUid, item_uid)DELETE /module-frame/<uid>/components/<item_uid>
  • 同模式對 leveraged / inventory 共 9 個

實際 BaseService.js extension pattern 看既有 SspService.js。

5.1.2 — 改 ModuleFrameTemplateEditView.vue

File: compliance-manager-fe/src/views/module_frame/ModuleFrameTemplateEditView.vue

L27 既有 import 改:

// 舊
import SspComponentsLeveragedInventoryTab from '@/components/grc/ssp/SspComponentsLeveragedInventoryTab.vue'
// 新
import ModuleFrameComponentsLeveragedInventoryTab from '@/components/grc/module-frame/ModuleFrameComponentsLeveragedInventoryTab.vue'

L2030 使用處改:

<!-- 舊 -->
<SspComponentsLeveragedInventoryTab v-else
    :ssp-uid="templateSspUid"
    @count-change="(n) => templateComponentsAggregateCount = n" />

<!-- 新 -->
<ModuleFrameComponentsLeveragedInventoryTab v-else
    :mf-uid="moduleFrameUid"
    @count-change="(n) => templateComponentsAggregateCount = n" />

templateSspUid (來自 template-ssp endpoint resolve) 改用 moduleFrameUid (route param)。

templateSspUid / 對應 fetch 邏輯 (useTemplateSsp composable etc.) 可廢(本期可標 // deprecated, removed in next cleanup)。

5.1.3 — Verify FE build

cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
npm run build:DEV
# 預期:build success no error

5.1.4 — Commit FE

git -C ~/Projects/Billows/Audit-Manager/compliance-manager-fe add \
  src/components/grc/module-frame/ModuleFrameComponentsLeveragedInventoryTab.vue \
  src/service/ModuleFrameService.js \
  src/views/module_frame/ModuleFrameTemplateEditView.vue

git -C ~/Projects/Billows/Audit-Manager/compliance-manager-fe commit -m "feat(mf-template-defaults-phase2): new mf-side components/leveraged/inventory tab

新建 ModuleFrameComponentsLeveragedInventoryTab.vue 取代既有
SspComponentsLeveragedInventoryTab reuse — 走 mf-side endpoint
(/module-frame/<uid>/components|leveraged|inventory) 不再用 sspUid。

BE 側對應 endpoint 由 BE repo Phase 4 提供。

詳見 BE design.md §5。

Co-Authored-By: Claude Sonnet (1M context) <noreply@anthropic.com>"

§Tasks — Phase 6: docx import 改寫對 mf source

§21

6.1 — 改 _run_v2_bundle_confirm 對 mf source 走 mf-default

File: app/oscal/service/ssp_docx_import_app_service.py:_run_v2_bundle_confirm() (L791-897)

整段重寫 dispatch — 對 source_type == "module_frame" 改走新 _run_mf_default_confirm() method:

def _run_v2_bundle_confirm(self, parsed_result, effective_source_uid, source_type, user_context, log_ctx):
    """Bug T Phase 2 (2026-05-XX): mf source 改走 mf-default 表,不再用 ensure_shell。"""
    if source_type == "module_frame":
        return self._run_mf_default_confirm(
            parsed_result, effective_source_uid, user_context, log_ctx,
        )
    elif source_type == "project_ssp":
        # 既有 SSP 路徑保留 (project SSP 仍寫 oscal.ssp_*)
        return self._run_v2_bundle_confirm_legacy_ssp(
            parsed_result, effective_source_uid, user_context, log_ctx,
        )
    else:
        logger.warning(
            "_run_v2_bundle_confirm: unknown source_type=%s — skipping",
            source_type,
        )
        return {"written_las": 0, "written_components": 0, "written_inventory": 0, "warnings_added": 0}


def _run_mf_default_confirm(self, parsed_result, mf_uid, user_context, log_ctx):
    """Bug T Phase 2: docx import 對 mf source — 寫 compliance.module_frame_*_defaults
    表,不走 SspShellService.ensure_shell (不建 SSP shell)。

    parsed_result 內 components / leveraged / inventory / system_characteristic 直接
    bulk INSERT / UPSERT 到對應 mf-default 表。
    """
    self._safe_log_info(
        "[mf-default-confirm] enter mf_uid=%s parsed_components=%d parsed_la=%d "
        "parsed_inv=%d sc=%s",
        mf_uid,
        len(parsed_result.get("components") or []),
        len(parsed_result.get("leveraged_authorizations") or []),
        len(parsed_result.get("inventory_items") or []),
        "present" if parsed_result.get("system_characteristic") else "None",
    )

    # Resolve mf_id
    mf = self._mf_domain_service.get_module_frame_by_uid(mf_uid)
    if mf is None:
        raise NotFound(GrcErrorCode.GRC_MODULE_FRAME_NOT_FOUND)

    # Stats
    written_las = 0
    written_components = 0
    written_inventory = 0
    sc_upserted = 0

    # 1. Leveraged authorizations — INSERT (upsert by (mf_id, title) unique)
    for la_dict in (parsed_result.get("leveraged_authorizations") or []):
        # upsert: if exists by (mf_id, title), update; else insert
        existing = self._mf_la_default_domain_service.get_by_mf_and_title(mf.id, la_dict.get("title"))
        # ... apply fields
        # use Domain service add/update
        written_las += 1

    # 2. Components — INSERT (upsert by (mf_id, title, component_type) unique)
    # ... 同 pattern
    
    # 3. Inventory items — INSERT (no unique constraint, always insert new)
    # ... 同 pattern

    # 4. System characteristic — UPSERT (mf_id unique, 1:1)
    # ... 同 pattern

    self._safe_log_info(
        "[mf-default-confirm] done mf_uid=%s written_las=%d written_components=%d "
        "written_inventory=%d sc_upserted=%d",
        mf_uid, written_las, written_components, written_inventory, sc_upserted,
    )
    return {
        "written_las": written_las,
        "written_components": written_components,
        "written_inventory": written_inventory,
        "warnings_added": 0,
    }

完整 implementation 由 sonnet 寫,pattern 同 ssp-side。

§22

6.2 — DI wire

DI 加 4 個 mf-default domain service 進 ssp_docx_import_app_service constructor。

§23

6.3 — Test

tests/test_ssp_docx_import_app_service_mf_source.py 驗 mf source 走 mf-default path 不走 SspShellService。

§24

6.4 — Commit

git commit -m "feat(mf-template-defaults-phase2): docx import 對 mf source 改寫 mf-default 表

ssp_docx_import_app_service._run_v2_bundle_confirm:
- mf source → 新 _run_mf_default_confirm() 寫 compliance.module_frame_*_defaults
- project_ssp source → 既有 path 保留 (寫 oscal.ssp_*)

不再走 SspShellService.ensure_shell — 廢物 SSP 累積 root cause 解掉。

詳見 design.md §6。

Co-Authored-By: Claude Sonnet (1M context) <noreply@anthropic.com>"

§Tasks — Phase 7: Cleanup 既有資料 (mf 343)

§25

7.1 — 寫 cleanup SQL

File (新建): scripts/sql/YYYY-MM-DD-mf-343-data-migration.sql

-- Date: YYYY-MM-DD
-- Bug T Phase 2 cleanup: mf 343 (b8065ad5, 艾爾航空) 資料遷移
--
-- 把 ssp 285 (本次 Bug T 第一輪 fix 後新建) 內的 4 components + 1 LA
-- 遷到 compliance.module_frame_component_defaults + _leveraged_authorization_defaults
-- 然後砍 mf 343 的 6 個 orphan SSP (280-285) 跟其子表
-- 219/228 保留 (legacy OSCAL project SSP)

BEGIN;

-- 1. 遷 285 的 leveraged_authorizations 到 mf-default (YYYY-MM-DD)
INSERT INTO compliance.module_frame_leveraged_authorization_defaults
    (module_frame_id, title, party_uuid, date_authorized, props, remarks, created_user, updated_user)
SELECT
    343, title, party_uuid, date_authorized, props, remarks, created_user, updated_user
  FROM oscal.ssp_leveraged_authorizations
 WHERE ssp_id = 285;

-- 2. 遷 285 的 components 到 mf-default
INSERT INTO compliance.module_frame_component_defaults
    (module_frame_id, component_type, title, description, purpose, status,
     leveraged_authorization_uid, props, created_user, updated_user)
SELECT
    343, component_type, title, description, purpose, status,
    leveraged_authorization_uid, props, created_user, updated_user
  FROM oscal.ssp_components
 WHERE ssp_id = 285;
-- NOTE: leveraged_authorization_uid 仍指 oscal.ssp_leveraged.uid (old) — 之後 mf-default
-- 內 leveraged_authorization_uid 校正 by mfla.title match
UPDATE compliance.module_frame_component_defaults c
   SET leveraged_authorization_uid = mfla.uid
  FROM compliance.module_frame_leveraged_authorization_defaults mfla,
       oscal.ssp_leveraged_authorizations sla
 WHERE c.module_frame_id = 343
   AND c.leveraged_authorization_uid = sla.uid
   AND sla.ssp_id = 285
   AND mfla.title = sla.title
   AND mfla.module_frame_id = 343;

-- 3. 砍 mf 343 的 6 個 orphan SSP (280-285) sys_impl
DELETE FROM oscal.ssp_system_implementations
 WHERE scope_type='ssp' AND scope_id IN (280,281,282,283,284,285);

-- 4. 砍 6 個 SSP (CASCADE 自動清 ssp_components / leveraged / inventory / SC / control_impl)
DELETE FROM oscal.system_security_plans
 WHERE id IN (280, 281, 282, 283, 284, 285);

-- 5. 不動 219/228 (legacy OSCAL project SSP, 保留)

COMMIT;
§26

7.2 — 跑 cleanup SQL (user 拍板)

接手 session 跑前先 surface preview 給 user,user 同意才執行:

PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
  -f scripts/sql/YYYY-MM-DD-mf-343-data-migration.sql
§27

7.3 — Verify

SELECT count(*) FROM compliance.module_frame_component_defaults WHERE module_frame_id=343;
-- 預期 = 4

SELECT count(*) FROM compliance.module_frame_leveraged_authorization_defaults WHERE module_frame_id=343;
-- 預期 = 1

SELECT count(*) FROM oscal.system_security_plans
 WHERE id IN (280, 281, 282, 283, 284, 285);
-- 預期 = 0

SELECT id FROM oscal.system_security_plans WHERE template_module_frame_id=343;
-- 預期:只有 219, 228 (legacy project SSP, 留)

§Tasks — Phase 8: E2E verify + 收尾

§28

8.1 — User 手測 (不跑 playwright)

提醒 user 重啟 BE + FE,然後手測:

  1. 打開 mf 343 template-edit page
  2. 看「系統元件」tab → 應該有 4 個 components (從 mf-default 表來)
  3. 看「外部利用服務」tab → 應該有 1 個 LA
  4. 看「受評標的」tab → 應該有 SC 內容
  5. 看「資產」tab → 0 (本期 mf 343 沒有 inventory)
  6. 點「從文件匯入」上傳同份 docx 再做一次 import
  7. STEP 2 全採用新值 → STEP 3 確認
  8. 回 template-edit 看 4 tab → 應該還是看到資料(不是空)
  9. DB 驗證
    SELECT count(*) FROM oscal.system_security_plans WHERE template_module_frame_id=343;
    -- 應仍 = 2 (219, 228),沒新增 286+
    
    SELECT count(*) FROM compliance.module_frame_component_defaults WHERE module_frame_id=343;
    -- 應 = 4 (依 docx import upsert 對齊既有)
§29

8.2 — Revert Bug T 本期 stop-gap fix(評估)

本期前 session 已 commit 的 Bug T fix(ee470eba — L2 idempotency / L3 self-heal / GRC_409032 error code):

  • L2 idempotency 在 SspShellService.create_shell:可廢(docx import 不再呼叫 SspShellService.create_shell;OSCAL project create flow 不過 SspShellService)
  • L3 self-heal:可保留(對 legacy SspShellService 仍有 self-heal 價值,雖然主要 caller 廢了)
  • GRC_409032 error code:保留(無害)

由接手 session 評估,建議:

  • 保留 L3 self-heal(不主動 revert,未來 caller 仍可用)
  • 保留 L2 idempotency(已 commit,移除反而引入 risk)
  • 保留 error code

但 design.md §11.39 改寫澄清「Bug T 真正 root cause 是 schema 不對稱,本期 Phase 2 才是根治」。

§30

8.3 — 收尾(等 user 下令才做)

per memory feedback_wait_for_user_command_to_close.md

User 說「收尾」/「verify pass 收尾」才執行:

  • design.md docs/features/FR-028-2605-ssp-oscal-alignment/design.md §11.40 新段 (Phase 2 完成紀錄)
  • changelog docs/changelog/YYYY-MM-DD-feat-mf-template-defaults-phase2.md (type=feat,含 schema migration)
  • FIXED-SUMMARY docs/features/FR-018.2-2605-module-frame-template-defaults-phase2/handoff/YYYY-MM-DD-FIXED-SUMMARY.md
  • 對話歷史歸檔
  • memory feedback (1-2 條教訓:schema 對稱性 / 跨 flow contract 對齊)
  • 橫向文件更新:
    • docs/claude/database-schema.md 加 4 張新表
    • docs/claude/frontend-overview.md 加新 mf-side endpoint + 新 FE 元件
    • docs/api/module_frame/api-spec.md 加 4 個新 endpoint 規格

§31

§Open Questions / 風險

風險 緩解
其他 mf (276/313/338/349/356) 也有「過去 docx import 寫到 SSP 子表」的 components/LA 本期只處理 mf 343;其他 mf cleanup 留 follow-up SQL
既有 OSCAL project SSP 內 components 是空(過去從沒帶 default) 本期 Phase 2 後新啟動 project 才會帶;既有 project 需 follow-up 一次性 backfill
FE 兩個 Vue 元件並存(SspComponentsLeveragedInventoryTabModuleFrameComponentsLeveragedInventoryTab 短期接受;長期 follow-up 抽共用 base + apiBase prop
ModuleFrameTemplateCopyService.copy() 加 4 個 step 後失敗 → 既有 control/AO/refdoc copy 也 rollback @transaction 已保證 atomicity,per CLAUDE.md 規範。仍要 verify
docx import 對 mf source upsert key 對齊((mf_id, title) 等 unique) schema unique constraint enforce,duplicate raise IntegrityError; sonnet 寫 upsert 邏輯時 catch
§32

§不在本期 scope

  • ssp_versioning_service 也建 shell 沒 sys_impl 的 contract gap — 留 follow-up
  • 既有 OSCAL project SSP 補 4 個 tab default — 留 follow-up SQL
  • 砍其他 mf (非 343) 的 orphan SSP — 本期只處理 mf 343
  • FE 兩元件抽共用 base — 留 follow-up
§33

§行為規範重要提醒(per CLAUDE.md + memory)

  • 永不切 branch — sonnet 接手前 user 必須先切 feature/mf-template-defaults-phase2 兩個 repo 都切
  • 可自行 commit,不自動 push(per memory feedback_stage_commit_no_ask
  • 改 BE service 層後必提醒 user 重啟 BE(per memory feedback_be_restart_after_service_change
  • 跨 repo 改 FE 必先 read FE CLAUDE.md(§Phase 5.0)
  • DB migration 用 cmmgr 跑(per memory feedback_sql_migration_use_cmmgr)— cm_app 受 RLS 擋
  • destructive operation user 拍板才跑(§Phase 7.2 — cleanup SQL)
  • plan 假設先 verify(§Verify Hypotheses)
  • 收尾必須等 user 下命令才做(§Phase 8.3)
  • 服務都 user 自己起,Claude 不啟動 / 不指示(per memory feedback_user_starts_services
  • 可人工驗證就切人工(§Phase 8.1 不跑 playwright,user 手測)
  • 不要晶晶體(per memory feedback_no_chinglish
  • 使用繁體中文溝通(FE CLAUDE.md 規範)
  • subagent dispatch 必加「git add 顯式檔名,禁用 -am」(per memory feedback_subagent_explicit_git_add
§34

§給 sonnet 的超短 prompt(fresh session 接手用)

請閱讀並執行 spec:
docs/features/FR-018.2-2605-module-frame-template-defaults-phase2/design.md

Goal: 補齊 mf 模板 4 張缺的 default 表 (component / leveraged / inventory /
system_characteristic) — schema 對稱化 + BE service + endpoint + FE 元件
+ docx import 改寫,根治 Bug T「廢物 SSP 累積」+「user 寫的看不到」problem。

跨 repo: compliance-manager-be + compliance-manager-fe (兩 repo 都要 user 先
切 feature/mf-template-defaults-phase2 branch)

按 §Tasks Phase 1~8 順序執行。每 Phase 開頭有 verify 步驟。Phase 5 前必 read
FE CLAUDE.md。Phase 7 cleanup SQL 跑前先給 user review。

每個 phase 完 commit。完工後請 user 拍板才進 Phase 8.3 收尾。然後請前 session
(spec 作者) 用 §驗收 checklist review。
§35

§驗收 Checklist(spec 作者 review 用)


§36

§改動範圍速查(給 reviewer)

BE 新增檔案

  • scripts/sql/YYYY-MM-DD-mf-template-defaults-phase2.sql
  • scripts/sql/YYYY-MM-DD-mf-343-data-migration.sql
  • infra/module_frame/models/module_frame_component_default.py
  • infra/module_frame/models/module_frame_leveraged_authorization_default.py
  • infra/module_frame/models/module_frame_inventory_item_default.py
  • infra/module_frame/models/module_frame_system_characteristic_default.py
  • infra/module_frame/mapper/module_frame_*_default_mapper.py × 4
  • infra/module_frame/repository/module_frame_*_default_repo_impl.py × 4
  • domain/module_frame/entity/module_frame_*_default_entity.py × 4
  • domain/module_frame/entity/module_frame_*_default_query_entity.py × 4
  • domain/module_frame/repository/module_frame_*_default_repo.py × 4
  • domain/module_frame/service/module_frame_*_default_domain_service.py × 4
  • app/module_frame/service/module_frame_components_service.py
  • app/module_frame/service/module_frame_inventory_service.py
  • api/module_frame/routes/module_frame_components_route.py
  • api/module_frame/routes/module_frame_inventory_route.py
  • tests/test_module_frame_*_default_domain_service.py × 4
  • tests/test_module_frame_components_service.py
  • tests/test_module_frame_inventory_service.py
  • tests/test_module_frame_template_copy_service_phase2.py
  • tests/test_ssp_docx_import_app_service_mf_source.py

BE 修改檔案

  • di_containers/module_frame/module_frame_containers.py (新增 4 套 service wire)
  • di_containers/oscal/oscal_containers.py (改 leveraged_service deps + 新增 components/inventory service)
  • api/module_frame/__init__.py (註冊新 2 個 resource)
  • app/module_frame/service/module_frame_leveraged_service.py (改用 mf-default)
  • app/module_frame/service/module_frame_system_characteristic_service.py (改用 mf-default)
  • app/module_frame/service/module_frame_template_copy_service.py (加 Step 4-7)
  • app/oscal/service/ssp_docx_import_app_service.py (dispatch mf → mf-default)

FE 新增檔案

  • src/components/grc/module-frame/ModuleFrameComponentsLeveragedInventoryTab.vue
  • src/service/ModuleFrameService.js (若不存在)

FE 修改檔案

  • src/views/module_frame/ModuleFrameTemplateEditView.vue (改 import + 元件 props)

DB schema 變更

  • compliance.module_frame_component_defaults (新)
  • compliance.module_frame_leveraged_authorization_defaults (新)
  • compliance.module_frame_inventory_item_defaults (新)
  • compliance.module_frame_system_characteristic_defaults (新)

DB 資料遷移

  • mf 343 ssp 285 內容遷到 mf-default
  • 砍 mf 343 ssp 280-285 (6 個 orphan)
  • 保留 mf 343 ssp 219/228 (legacy project)

§37

§參考連結(sonnet 開工 reference)

  • Bug T 完整 root cause: docs/features/FR-028-2605-ssp-oscal-alignment/design.md §11.39
  • Phase 1 reference: docs/features/FR-018-2604-module-frame-template-defaults/ (3 張舊表怎麼建)
  • 既有 ModuleFrameTemplateCopyService.copy() 結構: app/module_frame/service/module_frame_template_copy_service.py
  • 既有 mf control_default 套件 (mirror pattern): 7 個檔案路徑見 §0.4
  • 既有 SspComponentsLeveragedInventoryTab.vue: compliance-manager-fe/src/components/grc/ssp/SspComponentsLeveragedInventoryTab.vue (1004 行)
  • 既有 ModuleFrameTemplateEditView.vue: compliance-manager-fe/src/views/module_frame/ModuleFrameTemplateEditView.vue
  • Memory: ~/.claude/projects/.../memory/ (per CLAUDE.md auto memory section — 各 feedback_*.md 行為規範)