🟢 START HERE — FR-038 import-ssp:資源庫範本 SSP 匯入(設計定稿 + P1 套件寫入核心已完成,接手 P2)

給下個 session 的 prompt:「讀 docs/features/FR-038-2606-oscal-redesign/handoff/2026-06-15-import-ssp-P2-START-HERE-handoff.md,先過「🧭 WHY」+ §0 讀序硬 gate 懂 import-ssp 要解決什麼(design import-ssp-design.md 全讀 + api-contract §2),能答冷接自檢 4 問再碰 code。跑 §6 pre-flight + §7 verify P1,接手 P2(Excel 匯入,Model A+B 兩種 source 落 v2)。」 本檔自包含。狀態為 2026-06-15(設計定稿 + spec review 通過 + P1 套件單元測試綠 + BE BOOT/pytest 持平)。

🔖 交接現況(2026-06-15 換 session 當下)

項目
進度 import-ssp 設計定稿(import-ssp-design.md,user 校對後補正兩模型)+ P1 套件寫入核心 DONE下一棒 = P2(Excel 匯入打通,Model A+B)
主專案 branch / HEAD feature/oscal-refactor / 2dca0f2b
主專案 working tree 乾淨,只有 M pyproject.toml(jedi-oscal-v2 dev path-dep,照規範勿 commit)
套件 branch / HEAD ~/Projects/Jedicogy/module/jedi-python-package feature/oscal-refactor / a930bfd(P1,領先 origin 1,未 push
⚠️ 套件未 commit dev 改動(B1 留下,本 session 沒碰) M catalog_service.pyM framework_service.py?? tests/catalog/test_catalog_service_import.pydev path-dep,BE 重啟即生效;發版等整 feature + user 明示。別搞丟。
跑得起來嗎 create_app() BOOT OK;BE python -m pytest test/ = 56 failed + 50 errors(持平 baseline、零新回歸)+ 9 skipped;套件 tests/io_export/ = 10 passed(7 export + 3 import)
push BE + 套件都未 push,等 user 明示
**未收尾(user 還沒下收尾命令) 本 session 的 Q1(5 增量)+ import-ssp 設計/P1 的 changelog / analysis / SUMMARY / memory / Notion 全都還沒寫**;連前面 Wave 2B(B4.2/B5/匯出/AO-fix)的收尾也還掛著。下次 user 說「收尾」時一起補。

🧭 WHY:import-ssp 要解決什麼 + P2 在大圖位置(先懂才准碰 code)

一句話:合規資源庫(公版範本)原本就有「從文件/Excel 匯入 SSP」功能(新增資源庫從文件建、編輯資源庫時匯入並差異比對再更新),整套 FE + 解析 + 流程 + 差異比對都還在且 FE 已是 v2。FR-038 把 OSCAL 重寫成 v2 schema 後,這功能的寫入層(confirm 把資料存進 DB)還寫舊 v1 的表 → 被 disable。import-ssp 這個 arc = 只換寫入層,讓它存進 v2 OSCAL 的表,其餘不動。

產品情境(白話):

  • 合規資源庫 = 公版範本答案卷(catalog + profile + 範本 SSP),多個專案 clone 它當起點。
  • 顧問/管理員手上常有現成 SSP 文件(Word/Excel)→ 上傳自動填進範本 SSP,不用人工一條條打。
  • 兩種模型(user 2026-06-15 拍板兩種都做):
    • Model A — 編輯既有資源庫source_type=module_frame,import 解析該資源庫的 template_ssp_id 填/更新;再匯入時做差異比對(update-diff)。
    • Model B — 新增資源庫source_type=framework_version,選一個框架版本(提供 catalog)+ 上傳文件 → 建新資源庫(clone catalog→profile→空範本 SSP,= B1 既有 clone_resource_library 能力)→ 再填範本 SSP。每次都建新 = 永遠 create、不需 diff。
    • source_type=ssp = 專案 SSP / Project Planning,範圍外。)

FE 已經是 v2、不用動(證據):compliance-manager-fe/src/views/module_frame/ImportExcelPage.vue + ImportDocxPage.vuemode='create'|'update-mf'sourceType = isCreate ? 'framework_version' : 'module_frame'(第 95 行)、create 模式有 framework picker。路由 module-frame-import-docx-update / module-frame-import-excel-update,頁面 ssp-docx-import-v2/。FE 打的 BE 端點仍是 /ssp-excel-imports/parse+confirm、/ssp-docx-imports/parse+confirm(帶 source_type),這些端點目前 disabled。

為何被卡(v2 翻地基留下的洞):confirm 的寫入核心 domain/oscal/strategy/ssp_write_strategy.py / module_frame_write_strategy.py 還 import 舊 jedi_oscal v1 entity(ControlImplementationEntity / PartyEntity 等)→ 與 v2 撞同一 SQLAlchemy MetaData → 整條 route 被 config/di_modules.py:EXCLUDE_MODULES 擋掉。P1 已把 v2 寫入機具(OscalIoService.import_ssp)做好,P2/P3 把 confirm 改 delegate 到它、re-enable route。

P2 在大圖位置:P1(套件寫入核心)DONE → P2(Excel,Model A+B) → P3(Docx,A+B)→ P4(update-diff,Model A 再匯入)。

冷接自檢 4 問(答不出回 §0 讀序):① import-ssp 落點是哪份 SSP、為什麼是資源庫範本 SSP 不是專案 SSP?② Model A 與 Model B 差在哪、各自 source_type 是什麼?③ 為什麼說「只換寫入層」——哪些重用、哪些重建?④ P1 的 OscalIoService.import_ssp 涵蓋哪些子樹、為什麼是 create-only?


§0 接手讀序(按序,1~2 是硬 gate)

  1. 本檔「🧭 WHY」節 + 通讀本檔
  2. 🔒 gate:import-ssp-design.md全讀 — 兩模型 / source_type 分派 / 架構邊界 D-IS-1~3 / 4 phase / §9 pre-flight 假設)
  3. 🔒 gate:api-contract.md §2(resource-library import-ssp 端點)+ design.md §4.2(OscalIoService)/ §4.4(套件邊界)
  4. P1 成品(套件):~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2/jedi_oscal_v2/app/service/io/oscal_io_service.pyimport_ssp + _import_* helpers(P2 confirm 要呼叫它);測試 tests/io_export/test_oscal_import.py
  5. v1 excel app service(P2 要改寫其 confirm 寫入):app/oscal/service/ssp_excel_import_app_service.pyconfirm_import_confirm_superset_flow(framework_version) / _confirm_update_flow(module_frame))
  6. v1 parser 輸出(P2 adapter 來源):app/oscal/service/excel_parser/ParsedExcel dataclass(types.py

§1 現況:本 session 做了什麼(2026-06-15,全 verified)

本 session 跨兩個 arc:先做完 Q1(接前一棒),再開 import-ssp(設計 + P1)。

Q1 — 準備期 per-AO My Jobs(SHIPPED,commits 見 §11)

5 增量全完成 + verified(前一棒 handoff 2026-06-15-Q1-prep-jobs-START-HERE-handoff.md 的任務):

  • Q1.1 綁定表 workflow_execution_control_mappingao_part_id校正:表其實在 public 非 compliance,ORM DEFAULT_SCHEMA=None)+ seed 收證據 template。
  • Q1.2 AO 推導抽共用 helper app/grc/service/ao_derivation.py(B5 + Q1 共用)。
  • Q1.3 project_start_app_service 同步建 per-AO job(direct insert workflow_execution + job + mapping)。
  • Q1.4 vw_user_job_queue rebind control_* + AO。
  • Q1.5 端到端 smoke(catalog 729 真 L1 mf 412)PASS:17 控制 / 59 job / 指派後 view 查得 control+AO。
  • migration(只套 dev):2026-06-15-fr038-q1-workflow-execution-control-mapping.sql...-q1-seed-collect-evidence-template.sql...-q1-vw-user-job-queue-rebind.sql

import-ssp — 設計定稿 + P1(本檔主題)

  • 設計import-ssp-design.md(brainstorm 收斂 + spec review 通過 + user 校對補正兩模型)。
  • P1 套件寫入核心 DONE(套件 commit a930bfd):OscalIoService.import_ssp(oscal_ssp, *, target_ssp_id, curr_user, mode='create') —— export_oscal('ssp') 的反向,讀 OSCAL ssp dict 寫 v2 SSP 樹。
    • 涵蓋 export round-trip 面:metadata roles/parties + system-characteristics + system-implementation/components + control-implementation/implemented-requirements(SoA 搭 props)。
    • statements / by-components / LA / inventory 不在 P1(export 也不 round-trip;LA/component/inventory 另有 FR-032 v2-bundle 路徑)。
    • create-only:目標 SSP 必須空(非空丟 ValueError)→ 重複匯入/覆寫是 P4。
    • 套件測試 tests/io_export/test_oscal_import.py:round-trip(import 後 export 比對)+ 非空 guard + 未知 target。io_export 全套 10 passed。

§2 ⚠️ 會讓你做錯的陷阱 / 已驗地基(先看)

  1. 綁定表 / SSP schema 落點:jedi_common Base DEFAULT_SCHEMA=None → ORM table 不帶 schema → 走 search_path("$user", public)落在 public,不是 compliance。Q1.1 踩過(handoff 誤導查錯 schema)。查表存在性要查對 schema。
  2. 資源庫範本 SSP 建出來是空的(已驗:mf 412 → template_ssp_id=1481 → 0 control_impl / 0 sys_char / 0 sys_impl / 0 parties,只有 metadata 殼)。→ P1 走 insert;首次匯入=create。Model A 再匯入才需 overwrite/diff(P4)。
  3. export 本身是 MVPOscalIoService._export_ssp 不 round-trip statements/by-components/LA/inventory/users(看 _implemented_requirement_to_json / _build_system_implementation)。P1 import 鏡像此面,所以也不寫那些。P2 adapter 別硬塞 P1 不支援的子樹,會被丟掉。
  4. v2 表結構 ≠ v1:不是 1:1 換 entity。AO 落 oscal.catalog_control_parts(name=assessment-objective)、SoA/方法落 props、SSP 12 子表 shape 改了。寫入一律走 P1 import_ssp,別自己拼 v2 entity。
  5. route disabled 原因 = v1 entity importssp_excel_import_route / ssp_docx_import_route / ssp_scoped_excel_import_routeEXCLUDE_MODULES。re-enable 前必先把 app service / confirm 路徑裡的 from jedi_oscal... v1 import + v1 write strategy(ssp_write_strategy / module_frame_write_strategy)從匯入路徑拔掉,改 delegate P1。沒拔乾淨 → wire 進去 BE boot 撞 MetaData 直接炸。
  6. 中介契約 = OSCAL ssp dict(與 export 同形狀),不是獨立 dataclass。P2 的 adapter 把 ParsedExcel 轉成這個 dict 餵 import_ssp
  7. pytest 用 python -m pytest;BE boot 要補 dummy GITLAB/GITHUB env(見 §6)。
  8. plan 假設先 verify(parser 輸出欄位 / confirm 分派 / source_type)才開工。不晶晶體。

§3 P2 設計(方法,非既定,開工前 pre-flight 驗)

目標:Excel 匯入兩種 source 都落 v2,confirm 改 delegate P1 import_ssp,re-enable excel route。

方法候選(推測,待 pre-flight verify)

  1. adapter「ParsedExcel → OSCAL ssp dict」:新寫一個 adapter(主專案),把 ParsedExcel(controls / parties / system_characteristics / components...)轉成 P1 認的 OSCAL ssp dict(kebab keys,鏡像 export)。SoA → implemented-requirement.props
    • ⚠️ verify:ParsedExcel 實際欄位(讀 app/oscal/service/excel_parser/types.py);control-id 是否對得上目標資源庫的 v2 catalog(對不上 → warning 不擋,記 import_warnings)。
  2. confirm 改寫SspExcelImportAppService._confirm_superset_flow(framework_version=Model B)/ _confirm_update_flow(module_frame=Model A):
    • Model A:解析 module_frame {uid}template_ssp_id → adapter → import_ssp(target_ssp_id, mode='create')
    • Model B:先 clone_resource_library(= B1 能力,從 framework_version clone catalog→profile→空範本 SSP)拿 new module_frame + template_ssp_id → adapter → import_ssp(...)
    • 丟掉 v1 ssp_write_strategy / module_frame_write_strategy 在匯入路徑的呼叫。
  3. re-enable route:把 api.oscal.routes.ssp.ssp_excel_import_routeEXCLUDE_MODULES 拿掉;DI wire ssp_excel_import_app_service(注入 P1 OscalIoService / clone_resource_library 能力);確認 app service 不再 import v1 entity(boot 不炸)。
  4. OscalIoService 注入主專案:套件 OscalIoService 要在主專案 DI container 暴露(目前可能只在套件內 export 用過;確認主專案有 provider,沒有就加)。

直覺推測(待 verify):P2 主力是「拔 v1 寫入 + 接 adapter + re-enable」,量比想像輕(FE/parser/流程/diff UI 都不動)。但「拔乾淨 v1 import 讓 boot 不炸」可能要逐一追 import chain,是最容易卡的點。


§4 開工順位(P2,每段 smoke + 顯式 git add commit)

  1. pre-flight:跑 §6 + §7(確認 P1 在、BE boot、pytest baseline)。
  2. 讀 livessp_excel_import_app_service.py 的 confirm 三分派 + excel_parser/types.pyParsedExcel + clone_resource_library 簽章 + OscalIoService 在主專案 DI 現況。
  3. adapter:寫 ParsedExcel → OSCAL ssp dict(先 Model A 單純路徑跑通)。
  4. confirm 改寫 + 拔 v1_confirm_update_flow(A) 先;_confirm_superset_flow(B) 後(多一步 clone 建殼)。
  5. re-enable route + DI wire:移出 EXCLUDE_MODULES、wire app service、確認 boot 不炸。
  6. smoke(真資料,用 catalog 729 資源庫 mf 412 當 Model A 底材;Model B 選一個 framework_version):上傳 excel → 範本 SSP 填好(查 oscal.ssp_implemented_requirements 等)→ 量 BOOT OK + pytest 不多紅 → 顯式 git add commit。
  7. P3(docx) 比照;P4(update-diff) 最後。

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

檔案 為何
~/Projects/.../jedi-oscal-v2/.../app/service/io/oscal_io_service.py P1 成品 import_ssp,P2 confirm 要呼叫(已完成,讀懂簽章/涵蓋面)
app/oscal/service/ssp_excel_import_app_service.py P2 改寫 confirm 寫入(_confirm_update_flow / _confirm_superset_flow
app/oscal/service/excel_parser/types.py ParsedExcel 欄位(adapter 來源)
新檔 app/oscal/service/...(excel→oscal dict adapter) P2 新增 adapter
config/di_modules.py EXCLUDE_MODULES 移出 excel import route
di_containers/oscal/oscal_containers.py wire ssp_excel_import_app_service + 暴露 OscalIoService
domain/oscal/strategy/ssp_write_strategy.py / module_frame_write_strategy.py v1 寫入,退役(從匯入路徑移除呼叫;不刪檔,確認無 live consumer 後另開 cleanup)

§6 Pre-flight(必跑,可複製貼)

cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
git branch --show-current                      # feature/oscal-refactor
git status --short                             # 僅 ' M pyproject.toml'
git log --oneline -3                            # 2dca0f2b / d6e88221 / dabf1ebc
( cd ~/Projects/Jedicogy/module/jedi-python-package && git log --oneline -1 && git status -sb | head -6 )  # a930bfd P1 + B1 dev 改動還在
set -a; source .env 2>/dev/null; set +a
export GITLAB_API_VERSION=4 GITLAB_URL=http://localhost GITLAB_PRIVATE_TOKEN=dummy GITHUB_PRIVATE_TOKEN=dummy
poetry run python -c "
from dotenv import load_dotenv; load_dotenv()
import eventlet; eventlet.monkey_patch(all=False, socket=True)
import sys; sys.setrecursionlimit(5000)
from core.app_factory import create_app; create_app(); print('BOOT OK')"
poetry run python -m pytest test/ -q -p no:cacheprovider --continue-on-collection-errors 2>&1 | tail -1  # 56 failed,...,50 errors(持平)

.env 在 shell source 會在 JSON 行報 parse error(無害);python script 一律用 load_dotenv()


§7 Verify P1 確實 close(必跑,可複製貼)

cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
export GITLAB_API_VERSION=4 GITLAB_URL=http://localhost GITLAB_PRIVATE_TOKEN=dummy GITHUB_PRIVATE_TOKEN=dummy
# P1 套件單元測試(round-trip + 非空 guard + 未知 target)→ io_export 全套應 10 passed
poetry run python -m pytest ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2/tests/io_export/ -q -p no:cacheprovider 2>&1 | tail -3
# import_ssp 存在且簽章正確
poetry run python -c "from jedi_oscal_v2.app.service.io.oscal_io_service import OscalIoService; import inspect; print(inspect.signature(OscalIoService.import_ssp))"

§8 行為規範重要提醒(適用本棒)

  • 不切 branch(兩 repo 都在 feature/oscal-refactor;branch 不對停下問 user)。
  • 可自行階段性 commit(顯式 git add 檔名、-am;各 repo 分開);push / 收尾 / 套件發版等 user 明示
  • pyproject.toml path-dep 勿 commit;套件 dev 改動 commit 進套件 branch 防搞丟,但不發 Nexus
  • 改 BE 後提醒 user 重啟 BE(無 hot reload);服務 user 自己起。
  • 跨 repo 動 FE 前先讀 FE CLAUDE.md(本棒理論上不動 FE,但若要碰先讀)。
  • 動匯入管線 = 敏感子系統:pre-flight 驗 live 路徑、自己讀關鍵檔(別憑摘要)、多寫入路徑全 wire(memory feedback_preflight_before_touching_pipeline)。
  • 遇到架構/資料模型取捨、需選方案、scope 邊界 → 停下問 user
  • SQL migration(若 P2 有):cmmgr + --single-transaction -v ON_ERROR_STOP=1,新表 GRANT cm_app、收尾 INSERT schema_migrations;本期 migration 只套 dev

§9 收尾流程(整 import-ssp arc 做完 + user 明示才做)

盤點 commits(BE + 套件)→ changelog(type=feat,import-ssp 各 phase)→ analysis(兩模型取捨 / 只換寫入層 / OSCAL-dict 當中介)→ SUMMARY → 回頭更新橫向文件(design §4.2/§4.4 拿掉 import defer 標記、api-contract §2、import-ssp-design 標完工、frontend-overview 若動 payload)→ memory feedback(≥1 條:如「import-ssp 只換寫入層、FE 已 v2」「v1 import route disabled 因 v1 entity import 撞 v2 MetaData」)→ Notion。push 等 user。

本 session 自身的收尾(Q1 + import-ssp 設計/P1 的 changelog / SUMMARY / analysis / memory / Notion)也都還沒做 —— user 還沒下收尾命令,全留著。連更前面 Wave 2B(B4.2/B5/匯出/AO-fix)的收尾也掛著。下次 user 說「收尾」一起補。


§10 不在本期 scope(別順手做)

  • 專案 SSP 匯入source_type=ssp / Project Planning)—— 本 arc 只做資源庫(module_frame + framework_version)。
  • statements / by-components / leveraged-authorizations / inventory 的匯入寫入 —— P1 不支援(export 也不 round-trip);LA/component/inventory 另有 FR-032 v2-bundle 路徑,要接另議。
  • P4 的 update-diff —— P2/P3 先做 create;diff 留 P4。
  • FE 改動 —— FE 已 v2,原則不動。
  • 套件發 Nexus / pyproject.toml 改回 pin —— 整 feature 完 + user 明示。
  • 正式環境 migration(stg/poc/prod)—— 本期所有 migration 只套 dev。
  • Q1 / Wave 2B / import-ssp 的收尾文件 —— 等 user 下收尾命令。

§11 本 session commits

主專案(branch feature/oscal-refactor,未 push;HEAD 2dca0f2b

2dca0f2b docs(FR-038): import-ssp spec 補正 — 兩種資源庫匯入模型 + source_type 分派
d6e88221 docs(FR-038): import-ssp 設計定稿(資源庫範本 SSP 匯入,docx+excel 落 v2)
dabf1ebc feat(FR-038): Wave 2B Q1.4 — vw_user_job_queue rebind control_* + AO
394fe2c1 feat(FR-038): Wave 2B Q1.3 — project_start 同步建 per-AO 準備期 job
7cfe3565 fix(FR-038): Q1.1 綁定表改 ALTER public(非 CREATE compliance)
6429ec5f feat(FR-038): Wave 2B Q1.2 — AO 推導抽共用 helper(ao_derivation)
86a4099e feat(FR-038): Wave 2B Q1.1 — 建 workflow_execution_control_mapping + seed 收證據 template

(更前面 c89063db 起為前一棒,見該 commit。)

套件(~/Projects/Jedicogy/module/jedi-python-package branch feature/oscal-refactor,未 push;HEAD a930bfd,領先 origin 1)

a930bfd feat(oscal-v2): OscalIoService.import_ssp — OSCAL SSP → v2 relational tree (FR-038 P1)
  • 套件另有 B1 未 commit dev 改動:M catalog_service.pyM framework_service.py?? tests/catalog/test_catalog_service_import.py(別搞丟、別發版)。
  • dev DB:Q1 三個 migration 已套 dev(stg/poc/prod 待辦)+ Q1 smoke 殘留已清。

§12 給 fresh session 的超短 prompt

讀 docs/features/FR-038-2606-oscal-redesign/handoff/2026-06-15-import-ssp-P2-START-HERE-handoff.md。
先過「🧭 WHY」+ §0 讀序硬 gate(import-ssp-design.md 全讀 + api-contract §2 + design §4.2/§4.4),
能答冷接自檢 4 問(落點為何資源庫範本 SSP / Model A vs B 與其 source_type / 為何「只換寫入層」/ P1 import_ssp 涵蓋面與為何 create-only)才往下。
跑 §6 pre-flight(BOOT OK + pytest 56f/50e)+ §7 verify P1(io_export 10 passed)。
接手 P2:Excel 匯入兩種 source 落 v2 — adapter(ParsedExcel→OSCAL dict) → confirm 改 delegate 套件 import_ssp(拔 v1 write strategy)→ re-enable excel route → 真資料 smoke + commit。
每段 smoke + 顯式 git add commit。push/收尾/套件發版等 user 明示;不切 branch;pyproject.toml dev path-dep 勿 commit。

冷接可行性自檢 ✅

看本檔 + §0 讀序 + 跑 §6/§7 → 能確認現況(Q1 shipped + import-ssp 設計定稿 + P1 套件寫入核心 done + commits + 套件 dev 改動)、懂 import-ssp WHY(只換寫入層、FE 已 v2、兩模型、為何卡)、知道 P2 怎麼接(adapter + confirm 改寫 + re-enable)、知道地基陷阱(schema 落 public、範本 SSP 空、export MVP 面、拔 v1 import 才不炸)、知道規範界線。不需 user 額外解釋即可開工。