Phase A5 Implementation Plan — 預覽 UI + Confirm 寫入

Phase:A5(Track A:Excel 匯入收口) 依據:design-A5.md 狀態:plan draft(2026-05-20) 預估:3 session / ~4d(Session A design+plan / B BE / C FE+收尾)


0. Pre-flight Verification(T0,開工 BE 前必驗)

落地偏差進 design-A5 §10。

T0.1 — 既有 confirm schema 影響盤點

grep -rn "SspExcelImportConfirmRequestSchema\|decisions.*overrides" \
  api/oscal/ app/oscal/ tests/

驗證項目:

  • 既有 A2/A4 test 是否依賴「decisions 是 List[Dict] / overrides 是 Dict」彈性 shape
  • 改成結構化 schema 後哪些 test 需更新

T0.2 — Inline 新建 domain service 簽章驗

Domain Service 期望 method 簽章驗
DeviceDomainService .add(entity, user_context) domain/device/service/device_domain_service.py
InformationSystemDomainService .add(entity, user_context) domain/compliance/...
OrgUnitDomainService (jedi-auth) .add(entity, user_context) ~/Projects/Jedicogy/.../jedi-auth/
UserDomainService (jedi-auth) .add(entity, send_invite_flag) 同上

特別檢查:4 個 add() 是否都接得起 @transaction scope、是否需要額外 session injection、回傳 entity 是否含 id。

T0.3 — content_overrides 套用 path 驗

確認 A4 ship 的 _dict_to_parsed_* 5 個 static helper 是否能在 in-memory overlay 後仍跑通(A4 設計時應已支援,但 T0 確認)。

T0.4 — DELETE pattern 驗

grep -A 20 "def discard_parse" app/oscal/service/ssp_docx_import_app_service.py

確認 docx discard pattern:soft delete flag / hard delete / cascade 行為。A5 對齊。

T0.5 — Error code 序號驗

grep -E "GRC_(400|409|412)" common/code/grc_error_code.py | sort -t_ -k2 -n

抓 400 / 409 / 412 最後使用的序號,A5 新加從下一個序號開始。

T0.6 — FE base patterns 驗

確認項目 路徑
BaseService extension compliance-manager-fe/src/service/BaseService.js + 既有 service 範例
Pinia store pattern compliance-manager-fe/src/store/modules/<既有>.js
PrimeVue Dialog / Dropdown 既有 component reference
i18n 用法 compliance-manager-fe/src/locales/zh-tw/

1. Session B — BE 實作(~1.5d)

Task 1 — Confirm Request Schema 定稿(~30 min)

改動api/oscal/serializers/ssp/ssp_excel_import.py

實作:

  • 新增 InlineCreateSchema(row_idx / entity_type / payload)
  • 新增 DecisionSchema(sheet / row_idx / action / matched_id_override / inline_create_row_idx)
  • SspExcelImportConfirmRequestSchemadecisions = List(Nested(DecisionSchema)) / inline_creates = List(Nested(InlineCreateSchema)) / content_overrides = Dict(保留沿 docx pattern)

驗收

  • 既有 A2/A4 test 全綠(A2 confirm test 應該都是空 decisions 直接傳 → 不破壞)
  • 新 schema 驗錯誤 payload 回 400

Commitfeat(oscal): A5 T1 confirm request schema 定稿

Task 2 — _apply_inline_creates helper(~1h)

改動app/oscal/service/ssp_excel_import_app_service.py

實作 _apply_inline_creates(inline_creates, user_context, tenant_id) -> dict[int, dict]

def _apply_inline_creates(self, inline_creates, user_context, tenant_id):
    """
    Returns: {row_idx: {"entity_type": str, "new_id": int}}
    All writes in caller's @transaction; rollback if any fail.
    """
    created = {}
    for ic in inline_creates:
        if ic["entity_type"] == "device":
            entity = DeviceEntity(**ic["payload"], tenant_id=tenant_id)
            created_entity = self._device_domain.add(entity, user_context)
            created[ic["row_idx"]] = {"entity_type": "device", "new_id": created_entity.id}
        elif ic["entity_type"] == "information_system":
            # 同上 pattern
        elif ic["entity_type"] == "org_unit":
            # 透過 jedi-auth org_unit_service.add(...)
        elif ic["entity_type"] == "user":
            # 透過 jedi-auth user_service.add(...) + send_invite=True
    return created

注入 4 個新 domain service 到 __init__

  • device_domain_service
  • information_system_domain_service
  • org_unit_domain_service(jedi-auth)
  • user_domain_service(jedi-auth)

驗收

  • 4 種 entity 都 wire 通
  • DI Container ssp_excel_import_app_service Factory 加 4 個 wiring
  • Inline create 失敗(duplicate / validation)整段 rollback

Commitfeat(oscal): A5 T2 inline_creates handling + 4 domain service DI

Task 3 — _apply_decisions + _apply_content_overrides helper(~45 min)

改動:同 file

實作:

  • _apply_decisions(parsed_result, decisions, inline_create_map) — 遍歷 decisions,對 use_existing / use_inline_new / skip_as_text 三種 action 蓋掉 parsed_result 對應位置的 matched_*_id / match_method
  • _apply_content_overrides(parsed_result, content_overrides) — 蓋掉 parsed_result["metadata"] / parsed_result["controls"][cid]["implementation_description"] / parsed_result["controls"][cid]["objectives"][obj_key] 等欄位

注意:A4 Step 7 _update_parsed_result_with_reconcile 已有 by-key 對齊 helper _iter_paired — A5 _apply_decisions 沿用同 pattern,by (sheet, row_idx) lookup。

驗收:unit test 各 5+ 個 case

Commitfeat(oscal): A5 T3 _apply_decisions + _apply_content_overrides

Task 4 — confirm_import 整合 + transaction 保證(~1h)

改動SspExcelImportAppService.confirm_import

把 T1/T2/T3 串成完整 flow:

@transaction
def confirm_import(self, parse_uid, payload, user_context):
    parse_job = self._parse_job_domain.get_by_uid(parse_uid)
    parsed_result = parse_job.parsed_result

    # Step 1: content_overrides
    self._apply_content_overrides(parsed_result, payload["content_overrides"])

    # Step 2: inline_creates
    tenant_id = user_context.tenant_id
    inline_create_map = self._apply_inline_creates(
        payload["inline_creates"], user_context, tenant_id
    )

    # Step 3: decisions
    self._apply_decisions(parsed_result, payload["decisions"], inline_create_map)

    # Step 4: A4 ship 的 _write_all_data 8-step pipeline
    write_result = self._write_all_data(
        parsed_result, is_superset_flow=..., fw_version=...
    )

    # Step 5: import_summary 補 inline_created_* 計數
    import_summary = {**write_result, **{
        "inline_created_devices": sum(...),
        "inline_created_info_systems": sum(...),
        "inline_created_org_units": sum(...),
        "inline_created_users": sum(...),
        "decision_applied_count": len(payload["decisions"]),
        "skipped_as_text_count": sum(1 for d in payload["decisions"] if d["action"] == "skip_as_text"),
    }}

    return {"parse_uid": parse_uid, "status": "success", "import_summary": import_summary, ...}

驗收:整個 confirm 在單一 @transaction scope;inline create 失敗整段 rollback

Commitfeat(oscal): A5 T4 confirm_import 整合 + atomicity 保證

Task 5 — DELETE /ssp-excel-import/<parse_uid> endpoint(~30 min)

改動

  • api/oscal/routes/ssp/ssp_excel_import_route.py — 加 delete() method 到 SspExcelImportRoute
  • app/oscal/service/ssp_excel_import_app_service.py — 加 discard_parse(parse_uid, user_context) method(沿 docx pattern:soft delete parse job status=discarded)

驗收

  • DELETE 後 GET 同 parse_uid 回 404
  • discarded parse job 不能 confirm

Commitfeat(oscal): A5 T5 DELETE /ssp-excel-import discard endpoint

Task 6 — Error code 新增(~15 min)

改動common/code/grc_error_code.py

可能新增(依 T0.5 verify 結果 + 實作期間需要):

  • GRC_INLINE_CREATE_VALIDATION_FAILED(400xxx)
  • GRC_INLINE_CREATE_DUPLICATE_NAME(409xxx)
  • GRC_PARSE_JOB_DISCARDED(410xxx 或 404xxx;T0 拍板)

Commit:併入 T2 / T5 commit(同主題的小範圍 error code)

Task 7 — BE Pytest 補齊(~2h)

新增 test 檔

tests/test_a5_confirm_schema.py                  # ~8 case
tests/test_a5_inline_create_device.py            # ~5 case
tests/test_a5_inline_create_info_system.py       # ~4 case
tests/test_a5_inline_create_party.py             # ~4 case (org_unit + user)
tests/test_a5_content_overrides.py               # ~6 case
tests/test_a5_decisions.py                       # ~6 case (4 action types + skip_as_text)
tests/test_a5_inline_create_rollback.py          # ~3 case (duplicate / validation / cascade rollback)
tests/test_a5_discard_endpoint.py                # ~3 case
tests/test_a5_confirm_integration.py             # ~5 case (full payload happy path + 主要 error)

累計:~44 個 A5 新 test。

規範

  • 沿 jedi DBLogHandler patch pattern:patch('app.oscal.service.ssp_excel_import_app_service.logger')
  • patch_session_scope@transaction
  • Mock layered domain services(不接真實 DB)

驗收:a2+a3+a4+a5 ssp_excel test 全綠(A4 ship 後 282 個 + A5 新 ~44 個 = ~326 個)

Committest(oscal): A5 T7 unit + integration tests

Task 8 — Session B 收尾(~30 min)

  • Changelog:docs/changelog/2026-05-21-feat-ssp-excel-import-phase2-a5-be.md(type=feat)
  • 跑全套 pytest 確認 0 regression
  • 寫 Session B → C handoff prompt:docs/features/FR-011.2-2605-ssp-import-export-phase2/handoff/2026-05-21-a5-b-to-c.md

Commitdocs(ssp-import-export-phase2): A5 Session B 收尾 - changelog + handoff B→C


2. Session C — FE 實作 + 收尾(~2d)

Task 9 — Service + Pinia Store + Router(~1h)

新增

  • src/service/oscal/SspExcelImportService.js(BaseService extension)
    • parse(formData) → POST /ssp-excel-imports/parse
    • getParseResult(parseUid) → GET /ssp-excel-import/
    • confirm(parseUid, payload) → POST /ssp-excel-import//confirm
    • discard(parseUid) → DELETE /ssp-excel-import/
  • src/store/modules/sspExcelImport.js(Pinia)
    • state:parsedResult / decisions / inlineCreates / contentOverrides
    • actions:loadFromLocalStorage / saveToLocalStorage / clearLocalStorage / setDecision / addInlineCreate / setContentOverride
  • src/router/routes.js:加 /module-frame/import-excel + /module-frame/import-excel/:parseUid

Task 10 — 上傳頁 + ImportExcelPreview.vue 骨架(~1h)

  • 上傳頁:framework + version 兩層 picker(沿 A1 既有 DownloadSspBlankTemplateDialog pattern)→ file upload → POST parse → redirect /:parseUid
  • ImportExcelPreview.vue:9 sheet TabView + 動態 badge(unmatched 數量)

Task 11 — Sheet preview 元件 × 7(~4h)

  • SheetPreviewBasic.vue — 01_基本資料(DataTable 單列 + 編輯)
  • SheetPreviewParties.vue — 02_單位 + 03_參與人員(共用,依 party_type 切換)
  • SheetPreviewDevices.vue — 04_設備(DataTable + unmatched badge + UnmatchedRow component)
  • SheetPreviewInfoSystems.vue — 05_資訊系統
  • SheetPreviewLeveraged.vue — 06_外部利用服務
  • SheetPreviewControls.vue — 07_控制項與 AO(巢狀 control + objectives 結構,較複雜)
  • SheetPreviewRefDocs.vue — 08_程序書

Task 12 — UnmatchedRow + InlineCreateDialog + FuzzyBadge 共用元件(~2h)

  • UnmatchedRow.vue — 顯示 3 個按鈕(選現有 / 新建 / 純文字保留)+ 對應 dropdown / dialog
  • InlineCreateDialog.vue — 通用對話框,依 entity_type 切 form schema(device / info_system / org_unit / user)
  • FuzzyBadge.vue — confidence badge + override 按鈕(4 個 action 選項)

Task 13 — Confirm flow + 確認 modal(~1h)

  • 確認按鈕 → opens modal 顯示總計
  • 點 "確定" → POST confirm with payload from Pinia → 成功 → clear localStorage + redirect MF 詳細頁 + success toast

Task 14 — i18n + UX polish + Manual smoke(~1.5h)

  • src/locales/zh-tw/module-frame.js + src/locales/en/module-frame.js 加 A5 字串
  • 跑 manual smoke:上傳 → 預覽 → 編輯 → unmatched 三路徑 → fuzzy override → confirm → 驗 DB

Task 15 — Session C 收尾 / A5 task arc final SUMMARY(~1h)

按 CLAUDE.md「做 summary 觸發完整收尾」段:

  1. 跑全套 BE pytest 確認 0 regression(最後 sanity check)
  2. 規範文件齊全度:BE changelog (Session B 已寫) + FE changelog 新寫
  3. design §10 reconciliation 補 Session B / C 偏差
  4. Tracker README A5 row 改 BE+FE shipped
  5. 對話歷史 dump 到 docs/conversation-history/<date>/ssp-import-export-phase2-A5/
  6. 產 SUMMARY → docs/features/FR-011.2-2605-ssp-import-export-phase2/handoff/2026-05-XX-a5-SUMMARY.md

Commits(Session C)

  • feat(fe): A5 T9-T13 import-excel preview page
  • feat(fe): A5 T14 i18n + UX polish
  • docs(ssp-import-export-phase2): A5 Session C 收尾 + SUMMARY(含 BE/FE 跨 repo summary)

3. CLAUDE.md 規範對應 Checklist

規範 A5 對應
DDD Route 不查 DB ✅ DELETE / confirm 都走 app_service
App Service @transaction ✅ confirm_import @transaction 包整個 inline_create + write
Repo session lazy ✅ 不動 repo 層
寫入 API 必有角色檢查 ✅ 沿用 A2 既有 jwt + role check
前置條件驗證 ✅ confirm 走 A4 既有檢查(缺 SSP raise GRC_412026 等)
Error code 命名 ✅ T0.5 驗 + T6 新加沿 GRC_<status><3 位序號>
SQL Migration ❌ A5 無 schema 異動
審計欄位 ✅ inline create 走 domain service.add() 走既有 audit fields
顯式 git add ✅ 14 個 commits 都用 explicit list
jedi-* 不動 ✅ Phase 2 完整完工才一次 bump
pyproject.toml dev-path 不 commit ✅ 沿 A4 慣例
跨 repo 切換時提醒 user ✅ Session C FE 切到 compliance-manager-fe repo
階段性 commit 不用問

4. Risk / Rollback

場景 Rollback path
Session B BE 撞 jedi-auth 簽章不對 T0.2 verify 應先抓到;若實作中撞 → §10 補偏差 + 改 helper 簽章
Inline create 寫到一半 fail @transaction 自動 rollback;不留髒資料
FE localStorage 損毀 加 try/catch + 自動清除 + 提示 user 重新上傳
Session C smoke 撞 BE bug 回到 BE 補 hotfix(type=fix changelog)
A5 全 ship 後發現某 entity inline create 缺欄位 補 InlineCreateSchema.payload 子欄位 + FE dialog;不影響既有資料

5. Migration 順序(跨 repo / 跨 commit)

1. BE Session B:
   T1 schema → T2 inline_creates → T3 helpers → T4 confirm 整合 → T5 DELETE → T6 error code → T7 pytest
   ↓ ship
2. BE smoke(可用 curl / postman 驗 BE-only happy path)
   ↓
3. FE Session C:
   T9 service+store → T10 上傳頁 → T11 sheet 元件 → T12 unmatched UI → T13 confirm flow → T14 i18n+smoke
   ↓ ship
4. A5 task arc 收口 SUMMARY

每階段獨立可 ship — partial ship pattern OK(BE 先 ship → smoke → FE)。


6. Plan Verification(plan 寫完自查)