FR-032 Phase E-a 換 session 接手 handoff — Excel round-trip(系統資產匯入回灌 + 名稱比對)

DONE — 2026-06-04 收尾。E-a 全部完成並驗證(系統資產匯入回灌 + 名稱比對 master、 範本下載填值修復、資產→設備資產改名、設備 + 系統資產 picker round-trip + 雙向回填)。 BE 5 commits(1359ce48 / 743fc2a3 / e51bd809 / 9116e5df / 22929d6e)+ FE 2 commits(8c888e2 / f983e38),皆未 push。收尾報告見同目錄 2026-06-04-ea-SUMMARY.md;偏差紀錄見 design.md §11.11~11.19。

項目 內容
緣由 FR-032 A/B/C/D/F 已完成(系統資產 + 設備鉤稽 CRUD + clone + docx 匯出);本份交接 E-a:Excel 匯入回灌系統資產 + 依名稱比對 information_systems master
BE branch feature/FR-032(compliance-manager-be)
FE branch feature/FR-032(compliance-manager-fe)
套件 repo ~/Projects/Jedicogy/module/jedi-python-package/單一 monorepo,含 jedi-oscal / jedi-device),branch feature/FR-032
接手前必讀 本檔 → design.md(全) → §5 列的匯入管線檔
預估時間 BE 4 層 ~0.5–1 天 + FE preview ~0.5 天
性質 完整縱切,碰專案最敏感的 Excel 匯入管線 — 慢工出細活,每層加測試

§0 接手讀序(按此順序讀)

  1. 本檔(全)
  2. docs/features/FR-032-2606-system-asset-inventory/design.md(重點 §2 設計原則 / §3 資料模型 / §6 匯出 / §10 驗證結果)
  3. common/util/system_asset_snapshot.py核心共用邏輯,E-a 寫入要重用 snapshot_information_system
  4. app/module_frame/excel_template/sheet_definitions.py(樣板 sheet spec,看 SHEET_INVENTORY_ITEMS + ALL_SHEETS
  5. app/oscal/service/excel_parser/sheet_handlers.py(parser,看 parse_inventory_items_sheet
  6. domain/oscal/parser/ssp_intermediate.pyParsedInventoryItem
  7. domain/oscal/service/write_strategy/inventory_item_write_strategy.py(SSP 寫入)
  8. app/oscal/service/ssp_excel_import_app_service.py(MF defaults 寫入,搜 _write_mf_defaults
  9. FE:src/components/grc/ssp-excel-import/SheetPreviewInfoSystems.vue廢棄但可當 preview 範本

§1 現況:FR-032 已完成 A/B/C/D/F

內容 狀態
A 範本層(module_frame)系統資產表 + picker(hybrid soft-ref)
B 啟動專案 clone 帶 asset_type/ref(module_frame_template_copy_service.py Step 6)
C 專案 SSP view 系統資產表 + 編輯(SspInventoryItemsAppService
D1-A docx 匯出獨立「System Assets」表 + 設備表 repoint live 表
F 設備 inventory 鉤稽 jedi-device(device picker,帶 hostname/ip/os)

資料模型(已落地)compliance.module_frame_inventory_item_defaults + oscal.ssp_inventory_items 各有 asset_type('hardware'/'information_system') / ref_type('information_system'/'device') / ref_id(master uid)。DEV 已套 migration scripts/sql/2026-06-04-fr032-inventory-asset-type.sql

E-a 要做什麼:目前 Excel 匯入(資產 sheet)解析的 inventory 列寫死 asset_type='hardware',沒有「系統資產」的匯入路徑。E-a 要讓使用者能從 Excel 匯入系統資產,並依系統名稱比對 compliance.information_systems master 自動鉤稽(設 ref_id),配不到則自由輸入。docx 匯出已有獨立 System Assets 表,Excel 端要對稱補上。


§2 前次教訓(E-a 必看,別重蹈覆轍)

  1. 共用 snapshot 邏輯已存在common/util/system_asset_snapshot.pysnapshot_information_system(info_ds, ref_id)(回 (description, props, ref_id_str))+ snapshot_device。E-a 寫入時重用,不要重寫。
  2. error code 先查名稱再加 — pre-flight 曾漏看 GRC_INFORMATION_SYSTEM_NOT_FOUND 已存在(GRC_404020),差點加重複碼。新 code 先 grep 名稱。
  3. jedi packages 是 monorepo + 安裝形式不一 — jedi-oscal 是 venv symlink(改 source 重啟即生效);jedi-device 是 pin,dev 要改 pyproject.toml 為 path(已改、未 commit)+ user 跑 poetry update jedi-device。E-a 若不動套件就無此問題。
  4. 改 BE service 必提醒 user 重啟(無 hot reload);服務一律 user 自己起,不要附啟動命令。
  5. 每個 sheet 有專屬 preview 元件 — 新增 sheet 一定要配一個 SheetPreview*.vue,否則匯入預覽該 sheet 不顯示。
  6. annotate / 多 call site 要全 wire — 匯入有 MF defaults + SSP 兩條寫入路徑,兩條都要設 asset_type/ref(漏一條那 path 匯入的系統資產退化成 hardware)。

§3 E-a 五層縱切設計 + 各層驗證命令

建議:系統資產走獨立「系統資產」sheet(對稱 UI 兩表 + docx System Assets 表),不要塞進「資產」sheet 加 asset_type 欄(欄位不同:系統是 name/abbreviation/owner/deployment/status,設備是 IP/MAC/FQDN)。

Layer 1 — 樣板 sheet(app/module_frame/excel_template/

  • sheet_definitions.py:新增 SHEET_SYSTEM_ASSETSsheet_name="系統資產"),columns 對齊 docx System Assets:name(required) / abbreviation / description / owner_name / deployment_model(enum) / system_status(enum)。加進 ALL_SHEETS(放在「資產」sheet 後)。
  • header_i18n.py:加新欄位 header i18n key。
  • generator.py:確認新 sheet 會被產生;若匯出樣板需填入現有系統資產(round-trip),找匯出填值邏輯補上(檢查 downloadSspExcelTemplate 'filled' 模式怎麼填其他 sheet)。
  • deployment_model / system_status enum 值對照 jedi_information_system/common/enum/information_system_enum.py(on-premise/cloud/hybrid/co-location;operational/under-development/under-major-modification/disposition/other)。
  • 驗證python -c "from app.module_frame.excel_template.sheet_definitions import ALL_SHEETS; print([s.sheet_name for s in ALL_SHEETS])" 應含「系統資產」。

Layer 2 — parser(app/oscal/service/excel_parser/sheet_handlers.py

  • 仿 parse_inventory_items_sheet(~line 191)寫 parse_system_assets_sheet,回 list[dict]
  • 在 sheet → handler dispatch 處註冊(搜 parser 怎麼依 sheet_name 分派)。
  • 驗證:對一份含「系統資產」sheet 的 xlsx 跑 parser,確認回出列。

Layer 3 — 中間表示 / bundle(domain/oscal/parser/ssp_intermediate.py + app/oscal/service/excel_parser/v2_bundle.py

  • ParsedSystemAsset dataclass(name/abbreviation/description/owner_name/deployment_model/system_status)。
  • bundle 加 parsed_system_assets list。
  • 驗證:bundle 測試確認新 list 有值。

Layer 4 — 寫入 + 名稱比對(核心

  • 兩條寫入路徑都要處理(漏一條 = 該 path 匯入的系統資產退化 hardware):
    • MF defaultsapp/oscal/service/ssp_excel_import_app_service.py_write_mf_defaults(~1714),寫 ModuleFrameInventoryItemDefaultEntity 時設 asset_type='information_system' + ref。
    • SSPdomain/oscal/service/write_strategy/inventory_item_write_strategy.py(~124),寫 InventoryItemEntity 時設 asset_type + ref。
  • 名稱比對 master(E-a 的「a」):對每筆 parsed 系統資產,用 information_system_domain_service.get_information_systems(InformationSystemQueryEntity(name=<sheet name>)) 比對:
    • 配到 → 設 ref_type='information_system', ref_id=<master uid>,並用 snapshot_information_system(info_ds, ref_id) 回填 props(master 為準)。
    • 配不到 → 自由輸入:asset_type='information_system'、props 取 sheet 值、ref 為 null。
    • 建議把「parsed system asset → entity(含比對)」抽成共用 helper(mirror snapshot_information_system 風格),MF + SSP 兩路徑共用。
    • 注意 RLS / tenant:比對 master 要在對的 tenant scope(匯入流程已在 @transaction + tenant context 內,確認 query 不漏 tenant)。
  • error code:ref 比對失敗不該炸(配不到 = 自由輸入,正常路徑);只有「使用者明確給了 ref_id 但查無」才考慮錯誤,但 Excel 匯入通常給名稱不給 uid → 配不到就 fallback,不丟錯。
  • 驗證:app service 測試(mock info_ds):名稱配到 → ref_id 設 + props 來自 master;配不到 → free input。加 logger patch autouse fixture(抄 test_ssp_excel_import_app_service.py:46)。

Layer 5 — FE 匯入 preview(compliance-manager-fe

  • 新增 src/components/grc/ssp-excel-import/SheetPreviewSystemAssets.vue(仿 SheetPreviewInfoSystems.vue 廢棄檔,但欄位對齊新 sheet + 顯示「比對到的 master / 自由輸入」狀態)。
  • 匯入 preview 頁把「系統資產」sheet 接到新元件(搜 preview 頁怎麼 map sheet→component,可能在 ImportExcelPreviewPage.vue 或 store sspExcelImportStore.js)。
  • 驗證:FE serve 後跑一次匯入,preview 顯示系統資產 sheet。

§4 開工順位

  1. pre-flight(§6):對 branch / working tree / origin / pytest smoke
  2. verify A-F 真的在(§7):DB 三欄存在、BE 測試綠
  3. Layer 1 樣板 sheet → 驗證
  4. Layer 2 parser → 驗證
  5. Layer 3 bundle → 驗證
  6. Layer 4 寫入 + 名稱比對(核心,加測試)→ 驗證
  7. Layer 5 FE preview → 驗證
  8. 跨 repo 各自 commit(顯式 git add、禁 -am、不 push)
  9. 問 user push / 收尾

§5 該讀的檔案 / 預期改動範圍

BE

  • app/module_frame/excel_template/sheet_definitions.py(加 SHEET_SYSTEM_ASSETS + ALL_SHEETS)
  • app/module_frame/excel_template/header_i18n.py(header keys)
  • app/module_frame/excel_template/generator.py(產生 + 可能填值)
  • app/oscal/service/excel_parser/sheet_handlers.py(parse_system_assets_sheet + dispatch)
  • app/oscal/service/excel_parser/v2_bundle.py(bundle 加 list)
  • domain/oscal/parser/ssp_intermediate.py(ParsedSystemAsset)
  • app/oscal/service/ssp_excel_import_app_service.py(MF defaults 寫入路徑 ~1714)
  • domain/oscal/service/write_strategy/inventory_item_write_strategy.py(SSP 寫入 ~124)
  • common/util/system_asset_snapshot.py重用 snapshot_information_system;可加 import 比對 helper)
  • 可能 DI:寫入路徑若要 info_system_domain_service,確認已注入(excel import app service 是否已有;沒有就在 di_containers/oscal/oscal_containers.py 補)

FE

  • src/components/grc/ssp-excel-import/SheetPreviewSystemAssets.vue(新)
  • 匯入 preview 頁 / store(接 sheet → 元件)
  • i18n(新欄位 / sheet 名)

§6 Pre-flight Command(必跑)

cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
git branch --show-current          # 應為 feature/FR-032
git status --short                 # pyproject.toml M 是 dev-only(jedi-device path),勿 commit
# pytest smoke(FR-032 既有測試應全綠)
python -m pytest test/test_fr032_mf_inventory_asset_type.py test/test_fr032_ssp_inventory_asset_type.py \
  test/test_fr032_inventory_export_mapping.py test/test_fr032_device_picker.py -q | tail -5
# jedi monorepo branch
cd ~/Projects/Jedicogy/module/jedi-python-package && git branch --show-current   # feature/FR-032

§7 Verify A-F 確實完成(必跑)

cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
# DB 三欄存在(密碼查 .env DB_SECRET,psql 必帶 -p 25432)
PW=$(python3 -c "import json;print([json.loads(l[len('DB_SECRET='):])['rds_master_password'] for l in open('.env') if l.startswith('DB_SECRET=')][0])")
PGPASSWORD="$PW" psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -tA -c "
 SELECT table_name||'.'||column_name FROM information_schema.columns
 WHERE column_name IN ('asset_type','ref_type','ref_id')
   AND table_name IN ('module_frame_inventory_item_defaults','ssp_inventory_items') ORDER BY 1;"
# 應印出 6 列(兩表 × 三欄)

§8 行為規範重要提醒(CLAUDE.md + memory)

  • 不切 branch:發現 branch 不對停下問 user(FR-032 是新 branch,user 自己切的)。
  • 可自行 commit,但 push 永遠等 user 明示;各 repo 分開 commit;顯式 git add 檔名、禁用 -am
  • jedi monorepo:改套件 source;jedi-oscal symlink 即生效、jedi-device 是 pin(已改 pyproject path、待 user poetry update)。E-a 若不動套件就不碰這塊。pyproject.toml 的 jedi-device path 改動是 dev-only,不 commit。
  • 改 BE service 後提醒 user 重啟(無 hot reload);服務 user 自己起。
  • 收尾等 user 明示(changelog / SUMMARY / design §11 / memory 都等「收尾」指令)。
  • 不晶晶體(動詞連接詞用中文,技術名詞保留英文)。
  • 跨 repo 動 FE 前先讀 FE CLAUDE.md
  • SQL migration 用 cmmgr + 日期註解 + GRANT(E-a 大概不需新表,但若有則照規範)。
  • 寫 app service test 加 logger patch autouse fixture(DBLogHandler 雷,抄 test_ssp_excel_import_app_service.py:46)。
  • plan 假設先 verify:本 handoff 列的行號(~1714 / ~124 等)是 2026-06-04 當下,開工先 grep 確認沒位移。

§9 收尾流程(E-a 完成、user 說收尾才做)

  1. design.md 加 §11 E-a reconciliation 段(實作偏差)
  2. changelog docs/changelog/YYYY-MM-DD-feat-fr032-excel-roundtrip.md
  3. 本 handoff 加 > ✓ DONE 標頭
  4. 對話紀錄歸檔(若 user 要)
  5. commit 收尾文件、問 user push

§10 不在 E-a scope(不要順手做)

  • E-b:docx 匯入回灌系統資產(走 per-framework adapter,複雜,獨立階段)
  • OSCAL-native(json/yaml/xml)匯出:讀空 system_implementations,是跟本功能無關的既有深層缺陷(design §6.3 follow-up)
  • docx components/leveraged repoint:同上,本期只做了 inventory
  • G:jedi_information_system 拆套件(獨立 task)
  • device 的 Excel round-trip(F 只做了 picker,沒做 device 的 excel 匯入比對)— 若要可比照系統資產,但不在 E-a,先確認 user

§11 本 arc 已產出 commit(皆未 push)

BE(feature/FR-032

2f167300 feat(FR-032 F): DeviceMenuResponse 加 hostname / os
02ac0662 feat(FR-032 F): 設備 inventory 支援鉤稽 jedi-device(BE)
a49f6b41 fix(FR-032): docx 匯出系統資產獨立成 System Assets 表
fe30ce52 fix(FR-032): ssp_version 匯出 system_implementations 改 getattr
ce71c84a feat(FR-032): docx 匯出 repoint 到 live inventory 表(D1-A)
0afb037e feat(FR-032): SSP 層系統資產 hybrid soft-ref(C)
58100d31 feat(FR-032): 範本層系統資產 hybrid soft-ref(A + clone)
cfcd0345 feat(FR-032): migration 加 inventory asset_type/ref_type/ref_id
0bfa1c0f / f10b8dcc / 420f5e19 docs(FR-032): design + spec sharpen + 對齊 live

(FR-032 BE 測試檔:test/test_fr032_*.py 共 4 檔 23 測試綠)

jedi monorepo(feature/FR-032~/Projects/Jedicogy/module/jedi-python-package

9277d5b feat(device): device menu 加 hostname / os
d3af930 feat(inventory): OscalInventoryItem 加 asset_type/ref_type/ref_id

FE(feature/FR-032

c8bf554 fix(FR-032 F): 設備 dialog「描述」label + 驗證提示改「名稱」
1a97a98 feat(FR-032 F): device picker 帶入 hostname/os + 設備資產文案
68a5de2 feat(FR-032 F): 設備資產 dialog 加 device picker
841c2a9 Revert SspTabPanel 跑版 fix(user 覺得醜,還原原樣式待議)
91b55b4 feat(FR-032): 專案 SSP view 分「資產清冊 + 系統資產」兩表
7338cd9 / 941fb59 fix: 來源欄 Tag 統一 / enum label 顯示
c39138d feat(FR-032): 範本層系統元件 tab 新增「系統資產」表 + picker

已知未決(非 E-a):FE SspTabPanel.vue Excel 按鈕 layout(user 嫌新版醜,已 revert 回原樣式,待議調整);.env.production 是 FE 本機既有改動勿碰。


§12 給 fresh session 的超短 prompt

請閱讀 docs/features/FR-032-2606-system-asset-inventory/handoff/2026-06-04-ea-excel-roundtrip-handoff.md,
按 §0 讀序了解現況,接手做 FR-032 Phase E-a(Excel 匯入回灌系統資產 + 依名稱比對
information_systems master 設 ref_id)。先跑 §6 pre-flight + §7 verify,再依 §4 順位
五層縱切實作(每層加測試)。BE/FE/jedi 在 feature/FR-032。不切 branch、不自動 push、
收尾等我明示。