SSP 差異更新匯入復原 Implementation Plan(V1 體驗整套接回 v2)

狀態(2026-06-16):7 phase 全部實作 + review(spec + code quality 兩段式)+ 單元/整合測試 + BOOT OK + baseline 零新回歸 完成。 另含過夜抓到的兩個真 bug 修復(D1 docx source_type 契約、D3 by-component 敘述被丟)。⚠️ real-DB HTTP smoke 待 user 驗收(專案 266 / ssp 1482)—— 未標 FIXED / shipped。收尾文件見 docs/changelog/2026-06-16-*、SUMMARY(handoff/2026-06-16-import-ssp-restore-SUMMARY.md)、分析(docs/analysis/2026-06-16-ssp-import-restore-w2-architecture-and-bugs.md)。

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

**Goal: 把 FR-011.2 已出貨的「SSP 差異更新匯入」體驗(上傳 docx/excel → 逐項差異比對 → 接受/保留決策 → 人員自動比對 → 手動指派 → 寫入)整套接回 jedi_oscal_v2,驅動目標是讓專案 SSP(piece 3)**能匯入更新;資源庫重複匯入是 by-product。

Architecture: 套件 OscalIoService.import_sspmode='update'(非空 SSP 逐子物件 upsert)+ build_ssp_snapshot(ssp_id)(current 端 OSCAL 快照)。主專案 confirm 流程改成 parse → adapter(parsed→OSCAL dict) → diff(vs snapshot) → 逐項 decision → merge 成最終 OSCAL dict → import_ssp(mode='update')。人員比對在 parse/preview 時對 parsed parties 跑三層配對並回填,confirm 時隨 merge dict 寫入。

Tech Stack: Python 3.11 / Flask-RESTful / SQLAlchemy / dependency-injector / pytest;套件 jedi_oscal_v2(dev 走 poetry path-dep,不 commit pyproject.toml);DDD 分層(api/app/domain/infra)。


§1

⚠️ 架構決策(必讀,與 handoff 字面有一處刻意分歧 — ✅ user 2026-06-15 拍板採 W2)

決策 W:decision 在 OSCAL-dict 層 merge,單一寫入路徑 import_ssp(mode='update')(取代「復活 v1 write strategy」)

  • handoff §B.3 字面:把 ssp_write_strategy / module_frame_write_strategy 改走 v2 SspService 子物件 CRUD、依 decisions 逐子物件寫入(= 復活 v1 那 1000+ 行 strategy)。
  • 本 plan 採 W2:decisions 在「parsed OSCAL dict ↔︎ current snapshot dict」層 merge 成一份最終 OSCAL dict,再呼 import_ssp(mode='update') 一次寫入。
  • 為何分歧:handoff §A.1 本來就要 import_ssp(update) 做「逐子物件 upsert」。若再復活 write strategy 做第二套子物件寫入,等於兩套寫入路徑(套件 upsert + 主專案 CRUD),違反 design.md §3「避免兩套寫入」。W2 讓 import_ssp(update) 是唯一 writer,decision 退化成 dict 選擇/合併(純資料轉換、好測),write strategy 的角色被 merge 步驟取代。
  • W2 仍涵蓋 V1 全部能力:per-control/per-AO 決策 = merge 時逐項選 parsed/current;manual_assignment = merge 前把未匹配段落內容注入 parsed dict 的目標控制項/AO;reconciliation = 在 parsed parties 上回填 match 結果,隨 dict 寫入。
  • 回退條件:若 party 的 matched_user/org 寫入、responsible-party link、或某子物件的「保留現值」語意在 dict 層無法乾淨表達(例如需要 partial-field 級保留),再退回 W1 對該子物件做 strategy CRUD。Phase 4/5 pre-flight 會驗。

v1 ssp_write_strategy / module_frame_write_strategy / i_ssp_docx_write_strategy 不刪檔、停用(design.md §8);本 plan 不 wire 它們進 boot(仍 import v1 jedi_oscal entity,會撞 MetaData)。它們只當 diff/reconcile 邏輯的 port 參照。


§2

📌 已驗證地基(2026-06-15 讀 live code,executor 零脈絡可直接信;欄位級仍以各 phase pre-flight 為準)

套件 jedi_oscal_v2jedi-oscal-v2/jedi_oscal_v2/

  • app/service/io/oscal_io_service.py
    • import_ssp(oscal_ssp, *, target_ssp_id, curr_user, mode='create') :247-320mode!='create' raise :278;非空 guard(_sys_char_repo/_sys_impl_repo/_control_impl_repoget_by_ssp 任一非 None)raise :289-298
    • 寫入順序:_import_system_implementation(先,產 comp_map: {component-uuid → new id})→ roles / parties / system-characteristics / control-implementation。各 _import_* 全 insert-only
    • _import_* 清單:_import_roles :817_import_parties :835_import_system_characteristics :859(含 information-types nested)、_import_system_implementation :926(含 components/leveraged/inventory nested)、_import_control_implementation :988(含 IR + statements + by-components nested)、_import_by_component :1047(dual-parent 擇一:implemented_requirement_idstatement_id)。
    • __init__ 注入 20 repo(_ssp_repo/_sys_char_repo/_sys_impl_repo/_component_repo/_control_impl_repo/_impl_req_repo/_statement_repo/_by_component_repo/_leveraged_repo/_inventory_repo/_info_type_repo/_metadata_repo/_role_repo/_party_repo/_resource_repo/catalog 5 + ar/poam 6)。
    • export builders(snapshot 鏡像來源):_export_ssp / _build_metadata :323 / _build_system_characteristics / _build_system_implementation / _build_control_implementation。OSCAL key 為 kebab-case。
  • app/service/ssp/ssp_service.py完整 CRUD。1:1 子物件:get_system_characteristics(ssp_id) / upsert_system_characteristics(sc) :135get_or_create_system_implementation(ssp_id) :199get_or_create_control_implementation(ssp_id) :212。集合子物件每類 add_/list_/get_/update_/delete_:components / implemented_requirements / by_components / inventory_items / leveraged_authorizations / statements / parties。另 list_*_for_ssplist_components_for_ssp :226 / list_inventory_items :241 / list_leveraged_authorizations :258 / list_implemented_requirements_for_ssp :279)、list_statements(ir_id) :299list_parties(metadata_id) :328deep_clone_ssp :347
  • app/service/ssp/ssp_clone_service.py:寫入順序 + by-component component_id remap pattern(component_id_map.get(bc.component_id, bc.component_id))= update-mode upsert 的寫入順序/remap 參照。
  • 子物件自然 identity(upsert key):IR=(control_implementation_id, control_id);statement=(implemented_requirement_id, statement_id);by-component=(parent_id, component_id);party=(metadata_id, type, name);component=(system_implementation_id, title, type);role=(metadata_id, role_id);sys_char/sys_impl/control_impl 為 1:1 by ssp_id
  • parties 表(infra/model/base/oscal_party.py:14)無 matched_user_id/matched_org_unit_id 欄位 — 只有標準 OSCAL 欄 + props/external_ids/links(jsonb)。⚠️ reconciliation 結果在 v2 要存哪是 Phase 5 pre-flight 決策。
  • 套件 import 測試:tests/io_export/test_oscal_import.pytest_import_ssp_roundtrip / _nonempty_target_raises / _unknown_target_raises)。

主專案 V1 disabled(port 參照,import v1 entity,不可 wire)

  • app/oscal/service/ssp_docx_diff_service.pyDiffStatus = Literal["unchanged","changed","added","gone"]4 值 :8)套在 7 個 section(control 敘述 / per-AO 敘述 / parties / components / leveraged / inventory / system_characteristic)。compute_default_action_for_text :31compute_party_diff :149compute_component_diff :433compute_leveraged_diff :446compute_inventory_diff :459compute_sc_diff :477build_diff_summary :675annotate_parse_result :727(current 端目前讀 v1 entity → dict,要改吃 snapshot dict)。smart default:current 空+parsed 有→added/None;current 有+parsed 空→gone/keep_current;相等→unchanged/None;不等→changed/keep_current。
  • domain/oscal/service/reconciliation/base.py BaseReconciliationService(三層 fallback _try_user_selected_match_try_exact_match_try_normalized_match_try_fuzzy_match_apply_unmatched)、person_reconciler.py(email exact/normalized + domain fuzzy)、organization_reconciler.py(name + suffix fuzzy)、match_method.py MatchMethod(USER_SELECTED/EXACT/NORMALIZED/FUZZY_EMAIL_DOMAIN/FUZZY_NAME_PREFIX/UNMATCHED)。orchestrator domain/oscal/service/party_reconciliation_service.py reconcile(parties, tenant_id) 原地回填 matched_*_id/match_method。其餘 reconciler:catalog_control / assessment_objective / leveraged / system_characteristic / ssp_entity_orchestrator。
  • domain/oscal/strategy/i_ssp_docx_write_strategy.pyImportDecisions{decisions:list[dict], manual_assignments:list[dict], skipped_paragraph_idxs, predicted_controls_user_selection}ImportResult{created,updated,skipped,manual_assigned,manual_skipped,missing_left_blank}decisions[i] = {control_id, action:'use_docx'|'keep_current'|'skip', objectives:[{objective_key, action}]}

主專案 V2 現行管線

  • adapter:app/oscal/service/import_adapter/docx_to_oscal_ssp.py parsed_docx_to_oscal_ssp(parsed_result)->(oscal_dict, warnings)excel_to_oscal_ssp.py parsed_excel_to_oscal_ssp(...)、共用 _common.pysoa_props(applicable, impl_status)maybe_by_component(...)assemble(...))。SoA props key:applicability(applicable/not-applicable)、implementation-status;by-component 狀態 {"state": ...}v2_candidate_loader.py load_candidates_from_catalog(catalog_id, *, control_repo, part_repo)
  • app/oscal/service/ssp_excel_import_app_service.pyconfirm_import :231job.source_type 分派 _confirm_create_resource_library(framework_version) / _confirm_update_module_frame(module_frame);_fill_template_ssp(template_ssp_id, parsed_result, user_context) :343寫死 mode="create",是注入更新模式的點)。_VALID_SOURCE_TYPES={"framework_version","module_frame"}
  • app/oscal/service/ssp_docx_import_app_service.py_VALID_SOURCE_TYPES={"module_frame"} :49confirm_import :223;parse 時對 parties 跑 self._reconciliation.reconcile(預覽級,不回填寫入):324-328
  • app/oscal/service/export/ssp_v2_content_loader.py SspV2ContentLoader.load(ssp_id, *, source_type, source_uid):已把整個 v2 SSP 讀成 SspExportDataModel(snapshot current 端可借此讀法或套件內 _export_ssp 反向)。
  • route + 註冊:config/di_modules.py:13 EXCLUDE_MODULES 現僅含 api.oscal.routes.ssp.ssp_scoped_excel_import_route(專案 excel,dark)+ 框架/AP/AR/profile B 系列;docx/excel 資源庫 route 已 re-enabled。api/oscal/routes/ssp/ssp_scoped_excel_import_route.py(存在)force source_type="ssp"、source_uid 取自 URL、共用 SspExcelImportAppService。同步要在 api/oscalcreate_module() 註冊 blueprint。
  • parse-job:domain/oscal/service/ssp_excel_parse_job_domain_service.py / ssp_docx_parse_job_domain_service.pycreate/get_one/update_status/write_parsed_result/write_error/write_import_summary/deactivateparsed_result JSONB)。
  • catalog title:domain/oscal/repository/i_ssp_catalog_title_query.pyget_titles_by_ssp_id / get_ao_list_by_ssp_id / get_objective_titles_by_ssp_id),impl infra/oscal/repository/ssp_catalog_title_query.py
  • DI:di_containers/oscal/oscal_containers.py 註冊 ssp_excel_import_app_service:339-355)/ ssp_docx_import_app_service:357-387);oscal_io_service / resource_library_app_service / framework_service 為共用 primitive。

§3

🗂️ File Structure(建立/修改)

套件 jedi_oscal_v2(dev path-dep,整弧完才發 Nexus)

  • Modify jedi_oscal_v2/app/service/io/oscal_io_service.pyimport_ssp 加 update mode + 子物件 upsert helper + build_ssp_snapshot
  • Test tests/io_export/test_oscal_import_update.py — update mode upsert + round-trip(新檔)
  • Test tests/io_export/test_oscal_snapshot.pybuild_ssp_snapshot 鏡像 export(新檔)

主專案 — diff / merge / reconcile(接 v2,新檔或改 disabled 檔的 v2 版)

  • Create app/oscal/service/import_diff/ssp_diff_service.py — port 自 ssp_docx_diff_service.py,current 端吃 snapshot dict(不 import v1 entity)
  • Create app/oscal/service/import_diff/decision_merge.py — 依 ImportDecisions 把 parsed dict + snapshot dict merge 成最終 OSCAL dict
  • Create domain/oscal/reconciliation_v2/person_reconciler.py / organization_reconciler.py / base.py / match_method.py(或就地把現有 reconciliation/ 改吃 v2,視 Phase 5 pre-flight)
  • Create app/oscal/service/import_diff/party_reconciliation_v2_service.py — orchestrator 吃 v2 party + 現行 user/org domain service

主專案 — confirm 流程 + route(改)

  • Modify app/oscal/service/ssp_excel_import_app_service.py_VALID_SOURCE_TYPES"ssp";source verify 加 ssp 分支;confirm 加 Model C;_fill_template_ssp 參數化 mode + 接 diff/merge
  • Modify app/oscal/service/ssp_docx_import_app_service.py — 同上 docx 端 "ssp"
  • Modify config/di_modules.py — 移除 ssp_scoped_excel_import_route
  • Modify api/oscal/__init__.py(或 create_module() 所在)— 註冊 ssp_scoped blueprint
  • Modify di_containers/oscal/oscal_containers.py — wire ssp_service / catalog_title_query / snapshot / diff / reconcile_v2 進 import app service
  • 新增 error code:common/code/grc_error_code.py(grep 最大序號續編)

§4

Phase 1 — 套件 import_ssp(mode='update') 子物件 upsert〔A.1〕

Pre-flight(開工先驗,不憑摘要)

Files:

  • Modify: jedi-oscal-v2/jedi_oscal_v2/app/service/io/oscal_io_service.py
  • Test: jedi-oscal-v2/tests/io_export/test_oscal_import_update.py
# tests/io_export/test_oscal_import_update.py
def test_import_ssp_update_upserts_into_nonempty(db_session, seeded_ssp):
    svc = make_oscal_io_service(db_session)
    # 先 create 一份
    svc.import_ssp(SSP_DICT_V1, target_ssp_id=seeded_ssp.id, curr_user="t", mode="create")
    # 同 control_id 改敘述 + 新增一個 control → update mode 不該丟錯
    counts = svc.import_ssp(SSP_DICT_V2, target_ssp_id=seeded_ssp.id, curr_user="t", mode="update")
    irs = svc._impl_req_repo... # 查同一 control_id 只有一筆且敘述= V2
    assert <control AC-1 敘述 == V2 值 and 無重複 and 新 control 已新增>

Run: cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal-v2 && poetry run pytest tests/io_export/test_oscal_import_update.py -v Expected: FAIL unsupported mode 'update'

  • mode 守門放寬至 {'create','update'}(其餘仍 raise)。
  • guard:mode=='create' 才檢查空殼;mode=='update' 跳過。
  • 預設傾向:1:1 子物件 delegate SspService.upsert_system_characteristics / get_or_create_system_implementation / get_or_create_control_implementation,集合子物件用 SspServicelist_* + add_/update_,避免在 OscalIoService 重造 upsert(SspCloneService 已有寫入順序 + by-component remap 可參照)。除非 pre-flight 發現 io_service 直接持 repo 更順手,否則走 SspService。
  • _import_* 抽出「寫一筆」邏輯,加 mode 參數:
    • 1:1(sys_char/sys_impl/control_impl):get_by_ssp 有則 update(套既有欄位)、無則 add。
    • 集合(IR/statement/component/party/leveraged/inventory/by-component):開工前 list 現有 → 建 自然key → entity map → 每筆 parsed 命中則 update、未命中則 add。by-component 沿用 comp_map remap。
  • update mode 仍回 counts(可加 updated/created 細分)。

Run: poetry run pytest tests/io_export/ -v Expected: 既有 create 測試全綠 + 新 update 測試綠

cd ~/Projects/Jedicogy/module/jedi-python-package
git add jedi-oscal-v2/jedi_oscal_v2/app/service/io/oscal_io_service.py jedi-oscal-v2/tests/io_export/test_oscal_import_update.py
git commit -m "feat(oscal-v2): import_ssp update mode — 非空 SSP 逐子物件 upsert(FR-038 import-ssp P4-A1)"

§5

Phase 2 — 套件 build_ssp_snapshot(ssp_id)〔A.2〕

Pre-flight

Files:

  • Modify: jedi-oscal-v2/jedi_oscal_v2/app/service/io/oscal_io_service.py
  • Test: jedi-oscal-v2/tests/io_export/test_oscal_snapshot.py
git add jedi-oscal-v2/jedi_oscal_v2/app/service/io/oscal_io_service.py jedi-oscal-v2/tests/io_export/test_oscal_snapshot.py
git commit -m "feat(oscal-v2): build_ssp_snapshot — current 端 OSCAL 快照(FR-038 import-ssp P4-A2)"

套件改完 → 主專案 BOOT 驗pyproject.toml 已是 path-dep,重啟即生效。跑 §6 BOOT 指令確認 BOOT OK


§6

Phase 3 — 專案 SSP 可匯入(Model C,先直接 update,無 diff)= 首個出貨里程碑

本 phase 完 user 能看到:專案 SSP(ssp 1482)上傳 excel/docx → 內容寫入更新(覆蓋式 upsert)。 本 phase user 還看不到:逐項差異預覽 / 接受保留決策 / 人員自動比對 / 手動指派(Phase 4~6)。這不是終點。

Pre-flight

Files:

  • Modify: config/di_modules.py(移除 EXCLUDE)、api/oscal/__init__.py(註冊 blueprint)
  • Modify: app/oscal/service/ssp_excel_import_app_service.py"ssp" source_type + Model C + _fill_template_sspmode 參數)
  • Modify: app/oscal/service/ssp_docx_import_app_service.py"ssp" source_type + Model C)
  • Modify: di_containers/oscal/oscal_containers.py(wire ssp_service + project resolver 進 import app service)
  • Modify: common/code/grc_error_code.py(新碼:GRC_SSP_NOT_FOUND 若無 / GRC_IMPORT_SSP_NOT_PROJECT_MEMBER 等,grep 最大序號續編)
  • Test: test/test_ssp_excel_import_project_ssp.py(app service 測,logger patch fixture)
# test/test_ssp_excel_import_project_ssp.py
@pytest.fixture(autouse=True)
def _patch_logger(monkeypatch):
    monkeypatch.setattr("app.oscal.service.ssp_excel_import_app_service.logger", logging.getLogger("test"))

def test_confirm_project_ssp_calls_update_mode(svc, ssp_job, mock_oscal_io):
    svc.confirm_import(ssp_job.uid, {}, user_context_manager)
    mock_oscal_io.import_ssp.assert_called_once()
    assert mock_oscal_io.import_ssp.call_args.kwargs["mode"] == "update"
    assert mock_oscal_io.import_ssp.call_args.kwargs["target_ssp_id"] == EXPECTED_LIVING_SSP_ID

Run: pytest test/test_ssp_excel_import_project_ssp.py -v

  • _VALID_SOURCE_TYPES"ssp"
  • source verify 加 source_type=='ssp' 分支:解析 ssp uid → ssp + 權限(manager)。
  • confirm_importif job.source_type == "ssp": return self._confirm_update_project_ssp(...)
  • _confirm_update_project_ssp:解析 living_ssp_id → _fill_template_ssp(ssp_id, parsed_result, user_context, mode="update")
  • _fill_template_sspmode="create" 參數(預設不變,向後相容),把寫死的 mode="create" 改用參數。
  • config/di_modules.py:移除 "api.oscal.routes.ssp.ssp_scoped_excel_import_route"
  • api/oscal/__init__.py(或 create_module 所在):註冊 ssp_scoped(excel) blueprint。
  • docx 端 'ssp' 路徑(reviewer flagged,先釐清再實作)ssp_scoped_excel_import_route.py 只覆蓋 excel scoped 路徑;docx 對應 scoped route 檔。pre-flight 先確認 docx-into-project-SSP 是「沿用既有 docx import route + payload 帶 source_uid + 'ssp' source_type 分支」還是「需新建 docx scoped route」。兩種都要 wire 到,否則 docx 落專案 SSP 只接一半。
  • di_containers/oscal/oscal_containers.py:import app service 注入 ssp_service + project resolver(source verify 用)。

Run(§6 BOOT 指令)→ Expected BOOT OK Run: poetry run python -m pytest test/ -q -p no:cacheprovider --continue-on-collection-errors 2>&1 | grep -E '^(FAILED|ERROR)' | sort > /tmp/p3.txt; comm -13 /tmp/ssprestore_base.txt /tmp/p3.txt Expected: 空輸出(無新增 FAILED/ERROR)

git add config/di_modules.py api/oscal/__init__.py app/oscal/service/ssp_excel_import_app_service.py app/oscal/service/ssp_docx_import_app_service.py di_containers/oscal/oscal_containers.py common/code/grc_error_code.py test/test_ssp_excel_import_project_ssp.py
git commit -m "feat(FR-038): 專案 SSP 檔案匯入(Model C,import_ssp update mode)+ re-enable ssp_scoped route"

§7

Phase 4 — diff 標注 + decision merge(接 v2 snapshot)〔B.1 + B.4〕

本 phase 完 user 能看到:重複/專案匯入時逐項差異(unchanged/changed/added/gone)+ smart default + 接受/保留逐項生效。

Pre-flight

Files:

  • Create: app/oscal/service/import_diff/ssp_diff_service.py(port,current 端吃 snapshot dict)
  • Create: app/oscal/service/import_diff/decision_merge.py
  • Modify: excel/docx import app service — confirm 改 parse → diff(vs snapshot) → decisions → merge → import_ssp(update);preview(get_parse_result)回 annotated diff
  • Test: test/test_ssp_diff_service.py / test/test_decision_merge.py
git add app/oscal/service/import_diff/ app/oscal/service/ssp_excel_import_app_service.py app/oscal/service/ssp_docx_import_app_service.py test/test_ssp_diff_service.py test/test_decision_merge.py
git commit -m "feat(FR-038): SSP 匯入差異標注 + 逐項決策 merge(接 v2 snapshot,import_ssp update)"

§8

Phase 5 — 人員/組織三層比對接 v2〔B.2〕

本 phase 完 user 能看到:匯入的人員/單位自動配到系統 user/org,preview 顯示 match 結果 + method。

Pre-flight(最高槓桿 — 決定 match 結果存哪)

Files:

  • Create/Modify: domain/oscal/reconciliation_v2/(person/org/base/match_method)或就地改 reconciliation/ 吃 v2
  • Create: app/oscal/service/import_diff/party_reconciliation_v2_service.py
  • Modify: import app service — parse/preview 時回填 match 到 parsed parties;adapter 把 match 帶進 OSCAL dict(依 pre-flight 決策)
  • Test: test/test_party_reconciliation_v2.py

§9

Phase 6 — 未匹配段落手動指派〔B.3〕

本 phase 完 user 能看到:docx 解析出的未匹配段落,confirm 時可手動指派到控制項 / AO,內容寫入。

Pre-flight

Files:

  • Modify: app/oscal/service/import_diff/decision_merge.py(merge 前把 manual_assignments 注入 parsed dict 目標控制項/AO)
  • Modify: docx import app service confirm(接 payload.manual_assignments)
  • Test: test/test_manual_assignment_merge.py

§10

Phase 7 — 兩落點端到端 + 收口驗收〔B.5〕

本 phase 完 = 本棒終點:專案 SSP + 資源庫重複匯入兩落點,都走 parse→diff→per-control/per-AO decision→reconcile→manual assign→update,端到端通。

Pre-flight


§11

🔑 開放決策 / 風險(開工途中遇到回報 user,不自決)

  1. W2 vs W1(架構決策段)— 本 plan 採 W2,待 user/reviewer 確認;Phase 4/5 pre-flight 若發現 dict 層無法乾淨表達 partial 保留,回退該子物件走 W1。
  2. match 結果存哪(Phase 5 pre-flight)— v2 party 無 matched_* 欄位;存 props 還是主專案 mapping 表,影響 adapter + import_ssp。
  3. 人員比對是否保留三層 fuzzy(handoff §10)— 本 plan 預設保留三層;若 user 要簡化,砍 _try_fuzzy_match
  4. FR 編號(handoff §10)— 暫掛 FR-038 import-ssp 線;若 user 要另開新 FR,rename 資料夾 + README 登記。
  5. 套件發版時機 — 整弧完 + user 明示才 bump + 推 Nexus;dev 全程 path-dep,pyproject.toml 不 commit。
§12

🧪 Test Strategy

  • 套件(Phase 1/2):tests/io_export/ 單元,update upsert + snapshot 對稱 + create 回歸不變。
  • 主專案(Phase 3~6):app service / diff / merge / reconcile 單元,每個 service test 加 logger patch autouse fixture(memory feedback_test_logger_patch_db_handler,抄 test_ssp_excel_import_app_service.py)。
  • 零回歸基準:每 phase comm -13 /tmp/ssprestore_base.txt <(本次 FAILED/ERROR) 必空(baseline 47f/50e=97 行)。多日實作前先確認 /tmp/ssprestore_base.txt 還在/tmp 被清過要重跑 §6 baseline 指令重建,否則 comm 會吐整串 failure 看似大回歸)。
  • e2e(Phase 7):在 compliance-manager-test,真檔上傳兩落點。
  • 每 phase 收尾:BOOT OK + real-DB smoke(266 / 1482)+ baseline 零回歸 + 顯式 git add commit。