# Phase 2 A1 — BE Partial Ship 收口

> **日期**：2026-05-19
> **本檔角色**：A1 task arc 第二份 summary，承接 [SUMMARY.md](SUMMARY.md)（T0-T3 partial），涵蓋 T4 + T5 + partial T7（BE smoke + changelog + tracker）= BE 端全部
> **狀態**：**A1 BE partial shipped（7-task plan 完成 5+部分 = 6/7 effective）**
> **剩下**：T6 FE 下載按鈕 + T7 完整 E2E（cucumber + user 端 BE smoke）

---

## 一句話總結

承接上 session T0-T3（5 commits / 52 test），本 session 完成 T4 filled mode 非控制項 6 領域 + T5 控制項與AO 父子 row + partial T7（BE smoke + changelog + tracker），共 **3 commits / +1455 / -65 / 71 test 全綠**。A1 BE 端到此完整 ship；剩 T6 FE 下載按鈕 + T7 完整 E2E 留後續 session。

---

## 本 session 完成的事

### T4 — Filled mode 非控制項 sheet（commit `c3de9fa`）

7 個 pre-flight verify 全跑完，確認：
- `device_container.device_domain_service.get_devices(DeviceQueryEntity())`
- `information_system_container.information_system_domain_service.get_information_systems(InformationSystemQueryEntity())`
- `oscal_container.oscal_party_domain_service.get_all(PartyQueryEntity(party_type='organization'))`
- `oscal_container.oscal_responsible_party_domain_service.get_all(ResponsiblePartyQueryEntity(context_type='module_frame', context_id=mf.id))`
- `oscal_container.system_implementation_item_domain_service.get_all(SspSystemImplementationItemQueryEntity(scope_type='module_frame', scope_id=mf.id))`

實作範圍：
- 補 3 個 `_fetch_*_lookup` helper（devices `name (ip)` / info_systems `abbr - name` / orgs `party.name`）
- `_populate_filled_data()` 6 領域寫 bundle：metadata_row（mf 自有欄位）/ parties_org+person（rp link → party_uuid → party 分組）/ devices（hardware + join Device for IP/OS）/ info_systems（component family + join InformationSystem for owner）/ leveraged（leveraged-authorization + party join）/ ref_docs（mf own）
- generator `_build_data_sheet` 加 `_write_filled_rows()` + `_select_filled_rows()` sheet→bundle attr 對應 map
- DI 解循環依賴：`module_frame_container` 加 3 個 `DependenciesContainer()` slot；main `containers.py` device 直接傳 + oscal/info_system 用 `override_providers` (mirror line 175 既有 pattern)
- 13 個新 test（generator filled 6 + app service helpers 7）

### T5 — 07_控制項與AO sheet（commit `419907d`）

4 個 verify 跑完，確認：
- `profile_service.get_profile_by_uid(uid)` 一次拿到 eager-loaded profile→profile_controls→catalog_control→assessments tree（mirror 既有 `module_frame_template_import_service._load_profile_controls`）
- `ModuleFrameControlDefaultDomainService.get_all(...)` + entity `control_identifier` / `implementation_status` / `implementation_description`
- `ModuleFrameControlObjectiveDefaultDomainService.get_all(...)` + entity `(control_identifier, statement_identifier)` 為複合 key

實作範圍：
- `_build_controls_with_aos()` 父子 flat row 組裝
- `_load_existing_ctrl_map` / `_load_existing_obj_map` 對應預填 lookup
- generator `_write_filled_rows` 加 SHEET_CONTROLS 分支：父 row（statement_id=None）套 CONTROL_ROW_FILL F2F2F2 灰底覆蓋整 row、子 row 維持白底
- 3 個 service DI（profile_service / control_default / control_objective_default — 都已在 mf_container wired，不必動 main containers.py）
- 6 個新 test（5 個 app service + 1 個 generator 灰底）

### Partial T7 — BE smoke + changelog + tracker（commit `46b8999`）

BE smoke 結果：

| Smoke | 結果 |
|---|---|
| T2-T5 全 71 test | ✅ 全綠 |
| module_frame_write_strategy regression（19 test）| ✅ 通 |
| module_frame_write_strategy_v2_parties regression（20 test）| ⚠️ 1 個 pre-existing fail（驗 T4 commit `c3de9fa` 也 fail，非 T5 regression）|
| Import sanity check | ✅ 通 |
| 完整 Containers instantiate | ⚠️ 撞 A0.1 follow-up #7 jedi-issue GitLab env（user 端問題）|

文件交付：
- 新 changelog: `docs/changelog/2026-05-19-feat-mf-ssp-import-template-download.md`（含 8 commits 鏈 + API spec + 行為差異 + 父子 row contract + TEMPLATE_VERSION SemVer + DI 解法 + 部署 handover）
- Tracker update: `docs/features/FR-011.2-2605-ssp-import-export-phase2/README.md` A1 row 改 "BE partial shipped (5/7)" + 完整 commit 鏈 + ship 範圍與未完項說明

---

## 完整 commit 鏈（A1 跨兩 session 共 9 commits）

```
46b8999 docs(ssp-import-export-phase2): A1 partial T7 — BE smoke + changelog + tracker    ← 本 session
419907d feat(ssp-import-template): A1 T5 07_控制項與AO sheet — profile-scoped 父子 row + MF defaults 預填 + 6 tests  ← 本 session
c3de9fa feat(ssp-import-template): A1 T4 filled mode 非控制項 sheet — 6 領域預填 + DI + 12 tests  ← 本 session
f14a942 docs(ssp-import-export-phase2): A1 partial summary — T0-T3 收口 + 換 session 交接   ← 上 session
ffd5d63 feat(ssp-import-template): A1 T3 blank mode — generator 主邏輯 + lookup wiring + 30 tests  ← 上 session
f68525f feat(ssp-import-template): A1 T2 generator base — sheet defs + styles + lookup + DV helpers  ← 上 session
d9d54c2 feat(ssp-import-template): A1 T1 skeleton — route + app service + DI 接通  ← 上 session
f59c4ff docs(ssp-import-export-phase2): A1 T0 verify 結果回填 design.md (4 處)  ← 上 session
b18db55 docs(ssp-import-export-phase2): A1 phase docs — design + implementation plan  ← 上 session
```

**8 commits 未 push** — user 後續決定 push 時機。

---

## 測試結果（累計到 A1 BE 收口）

| 範圍 | passed | 備註 |
|------|--------|------|
| T2 helper base | 22 | T2 commit `f68525f` |
| T3 generator blank | 16 | T3 commit `ffd5d63` |
| T3 app service base | 14 | T3 commit `ffd5d63`，T4 後擴成 19 → T5 後 24 |
| T4 generator filled (含 T5 1 個 controls) | 7 | T4 commit `c3de9fa` 6 → T5 commit `419907d` +1 |
| T4 app service filled | 9 | T4 commit `c3de9fa`，含 lookup / build_metadata_row / build_parties / build_devices / build_leveraged |
| T5 app service controls | 5 | T5 commit `419907d` — TestBuildControlsWithAos |
| **累計** | **71** | **0.4s 完成、全綠** |

---

## A1 7-task plan 完成度

| Task | 主題 | 狀態 | commit |
|------|------|------|--------|
| T0 | Pre-flight verification（6 項）| ✅ | `f59c4ff` design.md 回填 |
| T1 | Skeleton — route + app service + DI 空殼 | ✅ | `d9d54c2` |
| T2 | Generator base — openpyxl helper 共用層 | ✅ | `f68525f` |
| T3 | Blank mode 完整實作 | ✅ | `ffd5d63` |
| T4 | Filled mode 非控制項 sheet（6 領域）| ✅ | `c3de9fa` |
| T5 | 07_控制項與AO sheet（父子 row + 預填）| ✅ | `419907d` |
| T6 | FE 下載按鈕 | ❌ | 留後續 session — compliance-manager-fe repo |
| T7 | E2E + smoke + changelog | ⏳ partial | `46b8999`（BE smoke + changelog + tracker 完成；剩 cucumber e2e + 完整 BE smoke）|

**有效完成度：6/7 ≈ 86%**（T7 BE 部分計入）

---

## DB / 環境狀態

- **dev DB**：無 migration（A1 是新 endpoint + 新 generator，不動 schema）
- **BE boot**：撞 A0.1 follow-up #7 jedi-issue GitLab env 問題（已知環境問題；BE code 完整可運作，user 端配齊 GitLab env 後可跑完整 smoke）
- **jedi-oscal**：仍 path-dep 模式（A0.1 既有，A1 沒動套件層）
- **pyproject.toml**：A0.1 dev-only path-dep 改動仍在 working tree（不 commit，整 Phase 2 收口時還原）

---

## 關鍵設計 anchor

### 1. URL contract（不變，T3 起 frozen）

```
GET /api/1.0/module-frame/<uid>/ssp-import-template?mode=blank|filled&locale=zh_Hant_TW
```

走既有 module_frame Blueprint (`url_prefix='/api/1.0'`)，跟 `/template/download` + `/control-defaults/template/download` 三條 URL 共存不衝突。

### 2. TemplateDataBundle 13 個欄位全用上

T0-T3 預留結構，T4 / T5 補實際 row data。bundle 結構在 `app/module_frame/excel_template/generator.py`。

### 3. ALL_SHEETS spec frozen at TEMPLATE_VERSION = "v1.0.0"

8 個資料 sheet 結構固定，欄位增刪走 design.md §14 SemVer SOP。A2 parser 必須先讀 `00_說明` R1 的版本字串作為相容性 anchor。

### 4. LookupSource 6 個 source 全用

| Source | 撈法 |
|--------|------|
| `USERS` | `user_domain_service.get_users(UserQueryEntity())` → `"nickname <login_name>"` |
| `ORG_UNITS` | `org_unit_domain_service.get_org_units_menu()` → `name` |
| `DEVICES` | `device_domain_service.get_devices(DeviceQueryEntity())` → `"name (ip)"` |
| `INFO_SYSTEMS` | `is_domain_service.get_information_systems(...)` → `"abbreviation - name"`（沒 abbr 退回 name）|
| `ORGS` | `party_domain_service.get_all(PartyQueryEntity(party_type='organization'))` → `name` |
| `REF_DOCS_MF` | `ref_doc_domain_service.get_all(ModuleFrameReferenceDocumentQueryEntity(module_frame_id=...))` → `title`（MF own，非 tenant pool）|

### 5. 控制項 sheet B 模式 contract（T5 拍板）

- profile-scoped 全 control 列出（不只 MF 已填的）
- 父 row（control）`statement_id=None`, `control_id=cc.control_id`, `control_name=cc.control_title`
- 子 row（AO）`statement_id=str(a.uid)`, `control_id=cc.control_id`（同父）, `objective_id=a.name`（"AC-1_smt.a" 短碼）
- MF defaults 預填：control_default key=`control_identifier`、obj_default key=`(control_identifier, statement_identifier)`
- 父 row 套 `CONTROL_ROW_FILL` F2F2F2 灰底覆蓋整 row；子 row 白底
- A2 parser 識別父子：`statement_id` 為空 → control row、為 AO uid → AO row；兩者皆帶 `control_id` 方便連續性判斷

### 6. DI 循環依賴解法

`oscal_container` 依賴 `module_frame_container`（`containers.py:141`），反向不可注入。解法（mirror line 175 既有 pattern）：

```python
# module_frame_containers.py
class ModuleFrameContainer(containers.DeclarativeContainer):
    device_container = providers.DependenciesContainer()
    oscal_container = providers.DependenciesContainer()
    information_system_container = providers.DependenciesContainer()
    ...

# containers.py 後段（oscal/info_system 建立後）
module_frame_container.override_providers(
    oscal_container=oscal_container,
    information_system_container=information_system_container,
)
```

device_container 早於 module_frame_container 建立，可在 instantiation 時直接傳。

---

## 已知 follow-up / risk

| # | 項目 | 嚴重度 | 狀態 |
|---|------|--------|------|
| 1 | BE boot 撞 jedi-issue GitLab env 問題 | 環境（A0.1 既有） | user 端問題；配齊後可跑完整 smoke |
| 2 | T6 FE 下載按鈕未實作 | 預期 | 留後續 session |
| 3 | T7 完整 E2E cucumber 未寫 | 預期 | 留後續 session |
| 4 | `test_write_parties_refreshes_existing_party_in_place` pre-existing fail | low | 非 A1 引入（T4 commit `c3de9fa` 也 fail）；OSCAL Party domain 改動的 stale test |
| 5 | profile → controls 雙段查詢效能（如 NIST 800-53 1000+ controls） | 第一版接受 | 反饋再優化（design.md §12 已記） |
| 6 | 控制項 sheet 父子 row 視覺優化（縮排 / outline / merge） | follow-up | 第一版用灰底區分，反饋再做（design.md §13 已記） |
| 7 | 樣板 row 上限 MAX_DATA_ROW=200 | low | 大 profile 可能截斷；warning log 已加，足夠第一版 |
| 8 | jedi-oscal Nexus 發版 | 例行 | Phase 2 整體完工再一次性 bump |
| 9 | `pyproject.toml` path-dep 還原 pin Nexus 版本 | 例行 | 整 Phase 2 收口時做 |

---

## 部署 handover

### 對 dev DB
無需 migration — A1 是新 endpoint + 新 generator，不動 schema

### 對 staging / production
- A1 完整 ship 後（T6/T7 完成後）才考慮 deploy
- 完整 BE boot 在已配置 GitLab / GitHub env 環境執行
- jedi-oscal 仍 path-dep，Phase 2 整體完工再一次性 bump

### BE 重啟（user 端配齊 env）

```bash
lsof -ti:8000 | xargs kill -9
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
set -a; source .env; set +a
nohup poetry run python main_app.py > /dev/null 2>&1 &
```

### 完整 smoke 步驟（T7 完整版範本）

```bash
TOKEN="..."  # from dev login
MF_UID="..." # dev DB existing MF uid

# Blank mode
curl -H "Authorization: Bearer $TOKEN" \
     -H "X-Tenant-ID: 102" \
     "http://localhost:8000/api/1.0/module-frame/$MF_UID/ssp-import-template?mode=blank" \
     -o /tmp/template_blank.xlsx

# Filled mode
curl -H "Authorization: Bearer $TOKEN" \
     "http://localhost:8000/api/1.0/module-frame/$MF_UID/ssp-import-template?mode=filled" \
     -o /tmp/template_filled.xlsx

# Verify
poetry run python3 -c "
from openpyxl import load_workbook
for fn in ['/tmp/template_blank.xlsx', '/tmp/template_filled.xlsx']:
    wb = load_workbook(fn)
    visible = [s for s in wb.sheetnames if not s.startswith('_lookup_')]
    print(f'{fn}: total={len(wb.sheetnames)}, visible={len(visible)}')"
```

---

## session 規範遵守清單

- [x] 顯式 `git add <file>`，禁 `-am` / `-A` — 全 3 commits（T4 / T5 / partial T7）都遵守
- [x] 所有 commits 含 `Co-Authored-By: Claude Opus 4.7 (1M context)` footer
- [x] jedi-oscal 沒動，仍 path-dep，未推 Nexus
- [x] `pyproject.toml` dev-only path-dep 改動沒被 commit
- [x] T0 verify 結果不符 design 假設時主動修 design.md（T0 階段做的，本 session 沒新增 design 偏差）
- [x] DDD 嚴格分層（Route 不碰 DB，App Service `@transaction`，Generator 純函式不依賴 DI）
- [x] @transaction 寫對位置（app service public method `generate()`）
- [x] Test 用 mock 包 logger 避開 jedi DBLogHandler 撞 SessionLocal=None（既有 `_patch_logger` autouse fixture + 新加 `_patch_generator_logger` for filled test）
- [x] Pre-flight verify 動工前必驗（T4 V1-V7、T5 V1-V4）
- [x] DI 解循環依賴用既有 pattern（override_providers）+ DependenciesContainer slot

---

## 下一階段建議

### T6 — FE 下載按鈕（0.5d）

切到 `~/Projects/Billows/Audit-Manager/compliance-manager-fe/` repo。
- 加 MF 詳細頁工具列 SplitButton：主按鈕「下載樣板」+ 下拉「空白 / 已填」
- service method `downloadExcelTemplate(uid, mode)` 包 axios `responseType: 'blob'`
- i18n 加 `zh_Hant_TW` / `en` 對應按鈕 / dropdown 文字
- API endpoint constant 加進 `src/config/api/api.js`

實作 anchor 範例見 plan：`docs/features/FR-011.2-2605-ssp-import-export-phase2/implementation-plan-A1.md` Task 6 (line 533-577)

### T7 完整 — E2E + 完整 BE smoke（0.5d）

切到 `~/Projects/Billows/Audit-Manager/compliance-manager-test/` repo。
- 寫 `features/.../mf_excel_template_download.feature`（cucumber + Playwright）
- 兩個 scenario：blank / filled 下載 → openpyxl 驗 9 sheet 結構 + filled row 對齊
- user 端 BE：先配齊 GitLab env（A0.1 follow-up #7），然後跑完整 smoke 驗 curl 兩個 mode

### 後續更大階段

A1 全 ship 後，Phase 2 next stop：
- **A2 — Excel Parser + 解析 API**（中型，待 A1 全 ship）
- 或 **B1 — Docx 樣板 + generator 骨架**（中型，可平行 A1，依賴 A0.1）

---

## 結語

A1 7-task plan BE 端走完 5+部分 task（86% effective）。`feature/ssp-import-export-phase2` branch 累計 8 commits 未 push 涵蓋 T0-T7 BE 全部。71 個 unit test 全綠，blank / filled 兩模式 code 層級完整可用；唯一卡點是 user 端 jedi-issue GitLab env 設定，配齊後立即可做完整 boot smoke。

A1 BE 收口。剩 T6 FE + T7 完整 E2E 留後續 session。
