# Phase 2 A1 — Task 0-3 收尾 + 換 session 交接

> **日期**：2026-05-19
> **本檔角色**：A1 task arc partial summary — **T0-T3 完成、T4-T7 待續**，
> 用於換 session 接 T4 之前的進度收口
> **前置文件**：A0.1 SUMMARY (`../ssp-import-export-phase2-A0.1/SUMMARY.md`)
> **狀態**：**A1 50% shipped（5 of 7 tasks）**，blank mode 完整可用

---

## 一句話總結

A1 第一個 session 完成 7 task plan 中的前 4 task（T0 verify + T1 skeleton +
T2 generator base + T3 blank mode），共 **5 個 commits / 17 個檔 / 2850
insertions / 52 個 unit test 全綠**。Blank mode 在配齊 GitLab env 的環境
boot 後可直接 curl 下載 9-sheet xlsx。

---

## 本 session 完成的事

### 1. Phase docs（T0 前）

- 寫 `design-A1.md`（14 段，含 §14 樣板演進與向後相容性 SemVer SOP）
- 寫 `implementation-plan-A1.md`（7 個 task + 30+ test case 預估 + cross-repo
  工作量估算）
- 對齊 user 拍板：控制項 sheet B 模式（profile-scoped 父子 row 結構）+
  下拉用 tenant scope + 01_基本資料 sheet 只列 MF 自有欄位

### 2. T0 Pre-flight Verification（6 項）

| # | 結果 | 影響 |
|---|------|------|
| T0.1 | ⚠️ MF 沒 system_characteristic 鉤稽路徑 | design 改寫 01 sheet 來源為 module_frames |
| T0.2 | ✅ ProfileControlDomainService.get_all + catalog_control lookup 雙段查詢 | 不動主架構 |
| T0.3 | ✅ user_domain_service.get_users(UserQueryEntity()) RLS auto filter | — |
| T0.4 | ⚠️ module_frame_reference_documents 是 MF own（無 tenant pool）| design 改寫 ref_docs lookup source |
| T0.5 | ✅ context_type='module_frame' 既有 pattern | — |
| T0.6 | ✅ openpyxl 3.1.5 dict-style `wb.defined_names['x'] = DefinedName(...)` | — |

### 3. T1 Skeleton（route + app service + DI 空殼）

開工前盤點發現既有 module_frame 已有兩條樣板下載 URL：
- `/template/download` (舊版 MF 整體匯入範例)
- `/control-defaults/template/download` (controls + AO 1-sheet 樣板)

→ A1 endpoint 命名改成 **`/api/1.0/module-frame/<uid>/ssp-import-template`** 避開衝突。

### 4. T2 Generator Base（4 helper + 22 tests）

- `sheet_definitions.py` — `ColumnDef` (含 enum/lookup 互斥 invariant) + `SheetDef`
  + `LookupSource` StrEnum + `ALL_SHEETS` tuple（**51 個欄位 across 8 sheet**）
- `styles.py` — PatternFill / Font / Alignment 常數
- `lookup_builder.py` — `build_lookup_sheet()` 建 hidden sheet + DefinedName
- `data_validation_builder.py` — `build_enum_dv` (含 255 char limit) + `build_named_range_dv`

### 5. T3 Blank Mode（generator 主邏輯 + lookup wiring + 30 tests）

- `generator.py` 完整實作（9 sheet + 6 hidden lookups + DV + freeze panes + 00_說明）
- `header_i18n.py` — 51 個 i18n key 的 zh_Hant_TW / en fallback dict
- App service 撈 3 個 lookup data（users / org_units / ref_docs_mf）
- DI 補 3 個 dep（user_domain_service / org_unit_domain_service / ref_doc_domain_service）

---

## 完整 commit 鏈（A1 5 個 commits）

```
ffd5d63 feat(ssp-import-template): A1 T3 blank mode — generator 主邏輯 + lookup wiring + 30 tests
f68525f feat(ssp-import-template): A1 T2 generator base — sheet defs + styles + lookup + DV helpers
d9d54c2 feat(ssp-import-template): A1 T1 skeleton — route + app service + DI 接通
f59c4ff docs(ssp-import-export-phase2): A1 T0 verify 結果回填 design.md (4 處)
b18db55 docs(ssp-import-export-phase2): A1 phase docs — design + implementation plan
```

---

## 測試結果

| 範圍 | passed |
|------|--------|
| T2: helper base (`tests/test_excel_template_base.py`) | 22 |
| T3: generator blank mode (`tests/test_excel_template_generator_blank.py`) | 16 |
| T3: app service helpers (`tests/test_ssp_import_template_app_service.py`) | 14 |
| **累計** | **52 個 unit test 全綠** |

---

## A1 剩餘工作（T4 / T5 / T6 / T7）

| Task | 主題 | 範圍 | 預估 |
|------|------|------|------|
| T4 | Filled mode（非 07 控制項）| metadata / parties / devices / info_systems / leveraged / ref_docs 預填到 sheets；補 device / info_system / oscal_party 三個 DI；補對應 lookup fetch | 0.5d |
| T5 | 07_控制項與AO sheet | profile-scoped 全 control 列出 + AO 子 row；MF 既有 implementation_statement 預填；父 row 灰底視覺區分 | 0.5d |
| T6 | FE 下載按鈕 | MF 詳細頁加 SplitButton + 下拉（blank / filled）+ axios blob download + i18n | 0.5d |
| T7 | E2E + smoke + changelog | BE smoke / FE e2e cucumber / changelog (feat) / tracker update | 0.5d |

---

## DB / 環境狀態

- **dev DB**：無需新 migration（A0.1 三表結構已 ship）
- **BE boot**：撞 A0.1 follow-up #7 jedi-issue GitLab env 問題（需配 GITLAB_URL /
  GITLAB_API_VERSION / GITLAB_PRIVATE_TOKEN / GITHUB_TOKEN），A1 code 完成但
  完整 boot smoke 留給 user 環境
- **jedi-oscal**：仍 path-dep 模式（A0.1 既有設定，A1 沒動套件層）
- **pyproject.toml**：A0.1 dev-only path-dep 改動仍在 working tree（不 commit）

---

## 關鍵設計 anchor（接手 T4-T5 必看）

### 1. URL contract

```
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'`)
- 不另開 Blueprint
- 跟 `/control-defaults/template/download` (1-sheet) 區隔

### 2. TemplateDataBundle 已預留 T4-T5 欄位

```python
@dataclass
class TemplateDataBundle:
    # 基本（兩 mode 都用）
    mf_uid: str
    mf_name: str
    mode: str
    locale: str
    framework_name: str | None = None
    framework_version: str | None = None
    profile_name: str | None = None
    lookups: dict[LookupSource, list[str]] = field(default_factory=dict)

    # filled mode 才有（T4 / T5 補 row 寫入）
    metadata_row: dict[str, Any] | None = None
    parties_org: list[dict[str, Any]] = field(default_factory=list)
    parties_person: list[dict[str, Any]] = field(default_factory=list)
    devices: list[dict[str, Any]] = field(default_factory=list)
    info_systems: list[dict[str, Any]] = field(default_factory=list)
    leveraged: list[dict[str, Any]] = field(default_factory=list)
    controls_with_aos: list[dict[str, Any]] = field(default_factory=list)
    ref_docs: list[dict[str, Any]] = field(default_factory=list)
```

T4 / T5 只需在 generator `_build_data_sheet` 加 filled row 寫入（讀 bundle 對應 list），
app service `_populate_filled_data` 撈資料填 bundle。

### 3. ALL_SHEETS spec frozen

8 個資料 sheet 結構固定，TEMPLATE_VERSION = `"v1.0.0"`。
T4 / T5 不應動 sheet_definitions.py（除非 design.md §14 SemVer bump）。

### 4. LookupSource 6 個全建（含空）

```python
class LookupSource(StrEnum):
    USERS = "users"           # ✅ T3 已接
    ORG_UNITS = "org_units"   # ✅ T3 已接
    DEVICES = "devices"       # ❌ T4 補
    INFO_SYSTEMS = "info_systems"  # ❌ T4 補
    ORGS = "orgs"             # ❌ T4 補
    REF_DOCS_MF = "ref_docs_mf"    # ✅ T3 已接 (MF own, 非 tenant)
```

T4 補 3 個 DI 後改 `_fetch_lookups()` 內三個 placeholder 即可。

### 5. 控制項 sheet B 模式 contract（T5 必看）

- profile-scoped 全 control 列出（不只 MF 已填的）
- 父 row（control）`statement_id` 空 + `control_id` 填 + `control_name` 填
- 子 row（AO）`statement_id` 填 AO UID + `control_id` 同父 + `objective_id/name` 填
- 已填 MF 既有 `implementation_statement` / `objective_statement` 預填
- 父 row 套 `CONTROL_ROW_FILL` 灰底（已在 styles.py 定義好）

---

## 已知 follow-up / risk

| # | 項目 | 嚴重度 |
|---|------|--------|
| 1 | BE boot 撞 jedi-issue GitLab env 問題 | 環境（A0.1 既有） |
| 2 | DEVICES / INFO_SYSTEMS / ORGS lookup 留空 | 預期，T4 補 |
| 3 | filled mode row 寫入未實作 | 預期，T4 / T5 |
| 4 | FE 下載按鈕未實作 | 預期，T6 |
| 5 | profile → controls 雙段查詢效能（如 NIST 800-53 1000+ controls）| 第一版接受，反映再優化 |
| 6 | 控制項 sheet 父子 row 視覺優化（縮排 / outline / merge）| follow-up，使用者反饋再做 |

---

## 部署 handover

### 對 dev DB

無需 migration — A1 是新 endpoint + 新 generator，不動 schema

### 對 staging / production

- A1 完整 ship 後（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 (T4 / T5 完成後)
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 via openpyxl
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` — 全 5 commits 都遵守
- [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（仍在 working tree）
- [x] T0 verify 結果不符 design 假設時主動修 design.md（不照舊硬幹）
- [x] 既有 endpoint 命名衝突先盤點再開工（不另開 Blueprint）
- [x] DDD 嚴格分層（route → app service → generator pure func；無跨層 DB 存取）
- [x] @transaction 寫對位置（app service public method，不在 generator）

---

## 下一階段建議

### 接手順序（T4 → T5 → T6 → T7）

**T4 適合單獨進**（0.5d）：
- 加 3 個 DI（device / info_system / oscal_party）
- 補 3 個 `_fetch_*_lookup` helper
- 補 `_populate_filled_data` 寫入 6 個非控制項 list（metadata_row / parties_org /
  parties_person / devices / info_systems / leveraged / ref_docs）
- 補 generator `_build_data_sheet` 內 filled row 寫入
- 補 unit test 6-8 case

**T5 比 T4 複雜**（0.5d）：
- profile → controls 雙段查詢
- 父子 row flat list 組裝
- 已填預填 + 父 row 灰底

**T6 / T7 可平行**（0.5d 各）

### 收口時機

T4 + T5 完成 = filled mode e2e 可用 = A1 BE 完整 — 可以做一次 BE smoke。
T6 + T7 完成 = A1 整體 ship — 寫正式 changelog + tracker update + 更新本 SUMMARY V2。

---

## 結語

A1 7-task plan 走完前 4 task（57%），blank mode 純 code 層級已可用。剩 4 個
task 都是已 well-defined 的範圍，design + plan 都有完整 contract，新 session
接手成本低。

T0-T3 task arc 收口。Phase 2 A1 半 ship。
