Phase 2 A1 — BE Partial Ship 收口

日期:2026-05-19 本檔角色:A1 task arc 第二份 summary,承接 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)


§1

一句話總結

承接上 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。


§2

本 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 範圍與未完項說明

§3

完整 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 時機。


§4

測試結果(累計到 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 完成、全綠

§5

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 部分計入)


§6

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 收口時還原)

§7

關鍵設計 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_containercontainers.py:141),反向不可注入。解法(mirror line 175 既有 pattern):

# 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 時直接傳。


§8

已知 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 收口時做

§9

部署 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)

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 完整版範本)

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)}')"

§10

session 規範遵守清單


§11

下一階段建議

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)

§12

結語

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。