Phase:A5(Track A:Excel 匯入收口) 依據:design-A5.md 狀態:plan draft(2026-05-20) 預估:3 session / ~4d(Session A design+plan / B BE / C FE+收尾)
落地偏差進 design-A5 §10。
grep -rn "SspExcelImportConfirmRequestSchema\|decisions.*overrides" \
api/oscal/ app/oscal/ tests/驗證項目:
| 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。
確認 A4 ship 的 _dict_to_parsed_* 5 個 static helper 是否能在 in-memory overlay 後仍跑通(A4 設計時應已支援,但 T0 確認)。
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 對齊。
grep -E "GRC_(400|409|412)" common/code/grc_error_code.py | sort -t_ -k2 -n抓 400 / 409 / 412 最後使用的序號,A5 新加從下一個序號開始。
| 確認項目 | 路徑 |
|---|---|
| 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/ |
改動: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)SspExcelImportConfirmRequestSchema:decisions = List(Nested(DecisionSchema)) / inline_creates = List(Nested(InlineCreateSchema)) / content_overrides = Dict(保留沿 docx pattern)驗收:
Commit:feat(oscal): A5 T1 confirm request schema 定稿
改動: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_serviceinformation_system_domain_serviceorg_unit_domain_service(jedi-auth)user_domain_service(jedi-auth)驗收:
ssp_excel_import_app_service Factory 加 4 個 wiringCommit:feat(oscal): A5 T2 inline_creates handling + 4 domain service DI
改動:同 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
Commit:feat(oscal): A5 T3 _apply_decisions + _apply_content_overrides
改動: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
Commit:feat(oscal): A5 T4 confirm_import 整合 + atomicity 保證
改動:
api/oscal/routes/ssp/ssp_excel_import_route.py — 加 delete() method 到 SspExcelImportRouteapp/oscal/service/ssp_excel_import_app_service.py — 加 discard_parse(parse_uid, user_context) method(沿 docx pattern:soft delete parse job status=discarded)驗收:
Commit:feat(oscal): A5 T5 DELETE /ssp-excel-import discard endpoint
改動: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)
新增 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。
規範:
patch('app.oscal.service.ssp_excel_import_app_service.logger')patch_session_scope 開 @transaction驗收:a2+a3+a4+a5 ssp_excel test 全綠(A4 ship 後 282 個 + A5 新 ~44 個 = ~326 個)
Commit:test(oscal): A5 T7 unit + integration tests
docs/changelog/2026-05-21-feat-ssp-excel-import-phase2-a5-be.md(type=feat)docs/features/FR-011.2-2605-ssp-import-export-phase2/handoff/2026-05-21-a5-b-to-c.mdCommit:docs(ssp-import-export-phase2): A5 Session B 收尾 - changelog + handoff B→C
新增:
src/service/oscal/SspExcelImportService.js(BaseService extension)
parse(formData) → POST /ssp-excel-imports/parsegetParseResult(parseUid) → GET /ssp-excel-import/confirm(parseUid, payload) → POST /ssp-excel-import/discard(parseUid) → DELETE /ssp-excel-import/src/store/modules/sspExcelImport.js(Pinia)
src/router/routes.js:加 /module-frame/import-excel + /module-frame/import-excel/:parseUidSheetPreviewBasic.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_程序書UnmatchedRow.vue — 顯示 3 個按鈕(選現有 / 新建 / 純文字保留)+ 對應 dropdown / dialogInlineCreateDialog.vue — 通用對話框,依 entity_type 切 form schema(device / info_system / org_unit / user)FuzzyBadge.vue — confidence badge + override 按鈕(4 個 action 選項)src/locales/zh-tw/module-frame.js + src/locales/en/module-frame.js 加 A5 字串按 CLAUDE.md「做 summary 觸發完整收尾」段:
docs/conversation-history/<date>/ssp-import-export-phase2-A5/docs/features/FR-011.2-2605-ssp-import-export-phase2/handoff/2026-05-XX-a5-SUMMARY.mdCommits(Session C):
feat(fe): A5 T9-T13 import-excel preview pagefeat(fe): A5 T14 i18n + UX polishdocs(ssp-import-export-phase2): A5 Session C 收尾 + SUMMARY(含 BE/FE 跨 repo summary)| 規範 | 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 不用問 | ✅ |
| 場景 | 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;不影響既有資料 |
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)。