| 項目 | 內容 |
|---|---|
| 緣由 | 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,盡量無歧義 |
按本順序 read,再開工:
docs/features/FR-018-2604-module-frame-template-defaults/design.md + implementation-plan.md — 既有 3 張 default 表怎麼建(mirror 同樣 pattern)docs/features/FR-028-2605-ssp-oscal-alignment/design.md §11.39 — 完整 root causeinfra/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 行)app/module_frame/service/module_frame_template_copy_service.py (300+ 行,看 Step 1-3 結構)app/module_frame/service/module_frame_leveraged_service.py (113 行,註解 L6-8 標 Task 11B-3 follow-up — 本期 Phase 2 一併解決)app/oscal/service/ssp_docx_import_app_service.py:_run_v2_bundle_confirm() (~106 行內)compliance-manager-fe/src/views/module_frame/ModuleFrameTemplateEditView.vue L27 import SspComponentsLeveragedInventoryTab,L2030 使用該元件傳 sspUidcompliance-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 開工。
mf(合規資源庫)= 一份「合規模板書」。OSCAL project 啟動時 snapshot 複製成該 project 的 SSP(系統安全計畫)。
| 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) |
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.
resolve_existing_shell 拿最舊 id 的 SSP;POST docx import 用 ensure_shell resolve 失敗 fallback create 新 SSP → user 寫的看不到自己補齊 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)
完工後:
SspComponentsLeveragedInventoryTab.vue 傳 sspUid(改新建 mf-only 元件)compliance.module_frame_component_defaultsmirror 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;compliance.module_frame_leveraged_authorization_defaultsmirror 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;compliance.module_frame_inventory_item_defaultsmirror 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;compliance.module_frame_system_characteristic_defaultsmirror 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;public.module_frames:跨 schema FK 是 soft reference(per CLAUDE.md「跨 schema FK 用 Integer soft reference」)created_user / updated_user audit 欄位:app service 寫入必填(user.login_name)tenant_id / org_unit_id:mf 是 system-level 範本,所有 tenant 共用同個 mf# 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 乾淨# 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),不開工。
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 規範)。
整份 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;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
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:
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
)"每張新表 7 個檔案。完整範例見 §2.A component_default 完整模板(下面)。其他 3 套 mirror 同 pattern。
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
di_containers/module_frame/module_frame_containers.py — 註冊 4 套新 domain service
module_frame_component_default 完整模板(第一套 — sonnet copy-modify 用)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)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_userdomain/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 = titleinfra/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 mdomain/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:
...infra/module_frame/repository/module_frame_component_default_repo_impl.pymirror module_frame_control_default_repo_impl.py (既有 151 行) — copy 改名 + entity / mapper / model import 即可。Sonnet 直接抄那檔改 4 個 import name。
domain/module_frame/service/module_frame_component_default_domain_service.pymirror module_frame_control_default_domain_service.py (既有 60 行) — copy 改名 + repo / entity import 即可。
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 插入。
按 §2.A 同樣 pattern 寫,每套 7 個檔案 + DI wire。Field 列表照 §3.2 ~ §3.4 SQL schema。
每套寫完跑 unit test(§2.E)→ commit 一次(4 commits)。
對每套 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 Truepoetry 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
# 預期:全 passgit 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。
File: app/module_frame/service/module_frame_template_copy_service.py
既有 copy() 已有 Step 1-3 (control / AO / refdoc)。本期加 Step 4-7。
在既有 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)# 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# 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# 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修改 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,
}檔頭加 import json。tenant_id 取得:caller (start_oscal_project) 需傳進來 — 看既有 caller 怎麼傳,或從 user_context 拿。
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 寫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>"| 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 後不再需要) |
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,
}di_containers/oscal/oscal_containers.py 內 module_frame_leveraged_service provider 改 deps:
ssp_leveraged_context_servicemf_la_default_domain_service (Phase 2 新增的 service)既有 test tests/test_module_frame_leveraged_service.py 要改 — mock 從 SspLeveragedContextService 改 ModuleFrameLeveragedAuthorizationDefaultDomainService。
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>"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。
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)File (新建): app/module_frame/service/module_frame_components_service.py
抄 §4.1 ModuleFrameLeveragedService 改 entity + 多帶 component_type / description / purpose / status / leveraged_authorization_uid field。
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>')di_containers/oscal/oscal_containers.py 加 module_frame_components_service provider。
tests/test_module_frame_components_service.py 寫 list / add / update / delete test。
同 §4.3 pattern。
FILE: api/oscal/routes/module_frame_template_ssp_route.py
FE 改完不再呼這個 endpoint 後可廢。本期暫不刪(怕別處引用),標 @deprecated 註解 + log warning 即可。
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 規範後再動。
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 全檔 → 改:
sspUid: String 改 mfUid: Stringprops.sspUid 改 props.mfUidFile: compliance-manager-fe/src/service/ModuleFrameService.js (若不存在則新建)
加 9 個 method:
listMfComponents(mfUid) → GET /module-frame/<uid>/componentscreateMfComponent(mfUid, payload) → POST /module-frame/<uid>/componentsupdateMfComponent(mfUid, item_uid, payload) → PUT /module-frame/<uid>/components/<item_uid>deleteMfComponent(mfUid, item_uid) → DELETE /module-frame/<uid>/components/<item_uid>實際 BaseService.js extension pattern 看既有 SspService.js。
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)。
cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
npm run build:DEV
# 預期:build success no errorgit -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>"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。
DI 加 4 個 mf-default domain service 進 ssp_docx_import_app_service constructor。
加 tests/test_ssp_docx_import_app_service_mf_source.py 驗 mf source 走 mf-default path 不走 SspShellService。
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>"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;接手 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.sqlSELECT 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, 留)提醒 user 重啟 BE + FE,然後手測:
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 對齊既有)本期前 session 已 commit 的 Bug T fix(ee470eba — L2 idempotency / L3 self-heal / GRC_409032 error code):
由接手 session 評估,建議:
但 design.md §11.39 改寫澄清「Bug T 真正 root cause 是 schema 不對稱,本期 Phase 2 才是根治」。
per memory feedback_wait_for_user_command_to_close.md:
User 說「收尾」/「verify pass 收尾」才執行:
docs/features/FR-028-2605-ssp-oscal-alignment/design.md §11.40 新段 (Phase 2 完成紀錄)docs/changelog/YYYY-MM-DD-feat-mf-template-defaults-phase2.md (type=feat,含 schema migration)docs/features/FR-018.2-2605-module-frame-template-defaults-phase2/handoff/YYYY-MM-DD-FIXED-SUMMARY.mddocs/claude/database-schema.md 加 4 張新表docs/claude/frontend-overview.md 加新 mf-side endpoint + 新 FE 元件docs/api/module_frame/api-spec.md 加 4 個新 endpoint 規格| 風險 | 緩解 |
|---|---|
| 其他 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 |
feature/mf-template-defaults-phase2 兩個 repo 都切feedback_stage_commit_no_ask)feedback_be_restart_after_service_change)feedback_sql_migration_use_cmmgr)— cm_app 受 RLS 擋feedback_user_starts_services)feedback_no_chinglish)feedback_subagent_explicit_git_add)請閱讀並執行 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。
scripts/sql/YYYY-MM-DD-mf-template-defaults-phase2.sqlscripts/sql/YYYY-MM-DD-mf-343-data-migration.sqlinfra/module_frame/models/module_frame_component_default.pyinfra/module_frame/models/module_frame_leveraged_authorization_default.pyinfra/module_frame/models/module_frame_inventory_item_default.pyinfra/module_frame/models/module_frame_system_characteristic_default.pyinfra/module_frame/mapper/module_frame_*_default_mapper.py × 4infra/module_frame/repository/module_frame_*_default_repo_impl.py × 4domain/module_frame/entity/module_frame_*_default_entity.py × 4domain/module_frame/entity/module_frame_*_default_query_entity.py × 4domain/module_frame/repository/module_frame_*_default_repo.py × 4domain/module_frame/service/module_frame_*_default_domain_service.py × 4app/module_frame/service/module_frame_components_service.pyapp/module_frame/service/module_frame_inventory_service.pyapi/module_frame/routes/module_frame_components_route.pyapi/module_frame/routes/module_frame_inventory_route.pytests/test_module_frame_*_default_domain_service.py × 4tests/test_module_frame_components_service.pytests/test_module_frame_inventory_service.pytests/test_module_frame_template_copy_service_phase2.pytests/test_ssp_docx_import_app_service_mf_source.pydi_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)src/components/grc/module-frame/ModuleFrameComponentsLeveragedInventoryTab.vuesrc/service/ModuleFrameService.js (若不存在)src/views/module_frame/ModuleFrameTemplateEditView.vue (改 import + 元件 props)docs/features/FR-028-2605-ssp-oscal-alignment/design.md §11.39docs/features/FR-018-2604-module-frame-template-defaults/ (3 張舊表怎麼建)app/module_frame/service/module_frame_template_copy_service.pycompliance-manager-fe/src/components/grc/ssp/SspComponentsLeveragedInventoryTab.vue (1004 行)compliance-manager-fe/src/views/module_frame/ModuleFrameTemplateEditView.vue~/.claude/projects/.../memory/ (per CLAUDE.md auto memory section — 各 feedback_*.md 行為規範)