# 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，盡量無歧義 |

---

## §0 接手讀序

按本順序 read，再開工：

1. **本文件** — root cause + 完整實作 plan
2. **既有 Phase 1 spec**：`docs/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 path**：`app/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 開工。

---

## §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 永遠是空

---

## §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 元件）

---

## §3 新表 schema 設計

### 3.1 `compliance.module_frame_component_defaults`

mirror `oscal.ssp_components`：

```sql
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`：

```sql
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`：

```sql
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 唯一）：

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

---

## §Pre-flight Command（開工前必跑）

```bash
# 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 乾淨
```

---

## §Verify Hypotheses（開工前必跑）

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

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

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

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

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

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

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

---

## §2.A `module_frame_component_default` 完整模板（第一套 — sonnet copy-modify 用）

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

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

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

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

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

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

```python
# 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 插入。

---

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

---

## §2.E Tests

### 2.E.1 — 加單元測試

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

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

```bash
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, 每套一個)

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

## 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）之間插入：

```python
# 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 寫成兩段式更安全：

```python
# 兩段式更安全版本
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）

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

```python
# 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 空殼）

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

```python
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 json`。`tenant_id` 取得：caller (`start_oscal_project`) 需傳進來 — 看既有 caller 怎麼傳，或從 user_context 拿。

### 3.1.7 — Test TDD

**File** (新建): `tests/test_module_frame_template_copy_service_phase2.py`

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

```bash
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 改寫 + 新增

## 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 後不再需要） |

## 4.1 — 改寫 ModuleFrameLeveragedService 從 mf-default 讀寫

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

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

```python
"""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.py` 內 `module_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

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

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

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

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

```python
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.py` 加 `module_frame_components_service` provider。

### 4.3.5 — Test

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

### 4.3.6 — Commit

## 4.4 — 新建 inventory mf-side endpoint

同 §4.3 pattern。

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

## 5.0 — 前置 — 跨 repo 必讀

**MANDATORY**：開 Phase 5 前 read FE CLAUDE.md（per memory `feedback_cross_repo_read_claude_md_first`）：

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

確認 FE 規範後再動。

## 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: String` 改 `mfUid: String`
- 9 個 sspService.* API call 改 mfService.* (新建 mfService.js)
- watch 從 `props.sspUid` 改 `props.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 改：
```js
// 舊
import SspComponentsLeveragedInventoryTab from '@/components/grc/ssp/SspComponentsLeveragedInventoryTab.vue'
// 新
import ModuleFrameComponentsLeveragedInventoryTab from '@/components/grc/module-frame/ModuleFrameComponentsLeveragedInventoryTab.vue'
```

L2030 使用處改：
```vue
<!-- 舊 -->
<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

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

### 5.1.4 — Commit FE

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

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

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

## 6.2 — DI wire

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

## 6.3 — Test

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

## 6.4 — Commit

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

## 7.1 — 寫 cleanup SQL

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

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

## 7.2 — 跑 cleanup SQL (user 拍板)

接手 session 跑前先 surface preview 給 user，user 同意才執行：

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

## 7.3 — Verify

```sql
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 + 收尾

## 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 驗證**：
   ```sql
   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 對齊既有)
   ```

## 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 才是根治」。

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

---

## §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 元件並存（`SspComponentsLeveragedInventoryTab` 跟 `ModuleFrameComponentsLeveragedInventoryTab`） | 短期接受；長期 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 |

## §不在本期 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

## §行為規範重要提醒（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`）

## §給 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。
```

## §驗收 Checklist（spec 作者 review 用）

- [ ] Phase 1: 4 張新表存在於 `compliance` schema (8 row return on table list query)
- [ ] Phase 2: 4 套 entity/repo/service 各 7 個檔案 + DI wire + unit test 通過
- [ ] Phase 3: `ModuleFrameTemplateCopyService.copy()` 含 Step 4-7 + test 通過
- [ ] Phase 4: 4 個 endpoint (2 改 + 2 新) 都從 mf-default 讀寫 + test 通過
- [ ] Phase 5: FE 新建 `ModuleFrameComponentsLeveragedInventoryTab.vue` + `ModuleFrameService.js` + `ModuleFrameTemplateEditView.vue` 改 import + build success
- [ ] Phase 6: docx import 對 mf source 走 `_run_mf_default_confirm` + test 通過
- [ ] Phase 7: Cleanup SQL 跑完 (mf 343 內容遷到 mf-default、orphan SSP 砍掉)
- [ ] Phase 8: User 手測通過 (template-edit 4 tab 看到內容, 重做 docx import 不累積新 SSP)
- [ ] Full pytest pass (no regression)
- [ ] FE npm run build:DEV pass
- [ ] 沒有新的 orphan SSP 累積（重複 docx import 後 verify）
- [ ] 收尾文件齊（design.md / changelog / FIXED-SUMMARY 等，等 user 下令才寫）

---

## §改動範圍速查（給 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)

---

## §參考連結（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 行為規範)
