For agentic workers: 跟著本 plan 一個 task 一個 task 跑;每個 phase 完成跑 test → commit → 進下一 phase。
Goal: Docx 重 import (update mode) 進 diff stepper 時,補完 4 個缺漏 key (system_characteristic / components / leveraged_authorizations / inventory_items) 的 per-row decision 機制,並把 DiffResolutionStep 改成 TabView 容器。
Architecture:
SspDocxDiffService 加 4 個 annotation method + build_diff_summary 擴展。SspDocxImportAppService.confirm_import 加 _apply_v3_decisions filter 在 _run_v2_bundle_confirm 之前把 decision 套到 parsed_result(含 overwrite caveat — keep_current 必須 inject current row 防 delete)。sspDocxImportStore.decisions 加 4 sub-map + buildConfirmPayload 擴展。新增 4 個 *DiffSection.vue + 4 個 *DiffCard.vue(mirror PartyDiffCard pattern)。DiffResolutionStep.vue 重構成 6-tab TabView。Tech Stack: Python 3.11 / SQLAlchemy / pytest / Vue 3 / Pinia / PrimeVue 3.53 (TabView / DataTable / Dropdown / SelectButton)
LeveragedWriteStrategy.write / ComponentWriteStrategy.write / InventoryItemWriteStrategy.write 三者都是「delete all by ssp_id → insert from parsed list」(見 domain/oscal/service/write_strategy/{leveraged,component,inventory_item}_write_strategy.py 各自的 "Overwrite semantics" docstring)。
後果:對 docx 重 import 的 decision filter,正確語意是:
| diff_status × decision | 動作 |
|---|---|
| changed × use_docx | parsed_row 進 final list |
| changed × keep_current | current_row 進 final list(不然被 delete) |
| changed × skip | 同 keep_current(保留現值不被 delete) |
| added × use_docx | parsed_row 進 final list |
| added × skip | drop(不寫) |
| gone × keep_current | current_row 進 final list(防 delete) |
| gone × use_docx | drop(accept delete) |
| unchanged × * | current_row 進 final list(沒 decision 但要保留) |
所以 _apply_v3_decisions 結尾要 set parsed_result[list_key] = final_list,覆寫掉原本 parsed_result 內的 list。
ParsedComponent / ParsedLeveragedAuthorization / ParsedInventoryItem dataclass field name 跟 DB entity field name 不完全一樣。Inject current row 進 final list 時要 mapper:
| Parsed dataclass field | Current entity 來源 | 備註 |
|---|---|---|
ParsedComponent.title |
ComponentEntity.title |
對齊 |
ParsedComponent.component_type |
對齊 | |
ParsedComponent.leveraged_authorization_ref |
反查 entity.leveraged_authorization_uid → LA.title |
用 (uid → title) map reverse-lookup |
ParsedComponent.protocol/port_ranges/security_auth |
entity.props["..."] |
from JSONB |
ParsedLeveragedAuthorization.fedramp_package_id/impact_level/... |
entity.props["..."] |
from JSONB |
ParsedInventoryItem.asset_id/asset_tag/ipv4_address/... |
entity.props["..."] |
from JSONB |
ParsedInventoryItem.implemented_component_refs |
反查 entity.implemented_component_uids → component.title |
用 (uid → title) map |
寫 entity → ParsedXxx dict 的 helper(推薦 stateless static method 在 diff service 或 import app service),複用 mapper logic。
| Key | Primary match | Tiebreaker | 防呆 |
|---|---|---|---|
| Components | (title.strip().lower(), component_type) |
同 (title, type) 重複 → 用第一個(log warning) | title + type 為空 → skip pair |
| LA | title.strip().lower() |
同 title 重複 → 用第一個 | title 為空 → skip pair |
| Inventory | description.strip().lower() |
同 description 重複 → 用第一個 | description 為空 → skip pair |
| SC | 單筆 by ssp_id — 不 match | N/A | N/A |
Mirror SspDocxDiffService.match_parties 的 dual-index pattern。
decisions 是 單一 action(不是 per-row map):system_characteristic_decision: "use_docx" | "keep_current" | "skip"annotated["system_characteristic"] = {current_values, parsed_values, diff_status, default_action},沒 row_uid 也沒 list_apply_v3_decisions 對 SC 是「決定要不要呼叫 _sc_write_strategy.write」 — use_docx → 寫 parsed;keep_current 或 skip 或 unchanged → 不寫(DB 保持現值 — SC 是 upsert by ssp_id 不是 overwrite,不寫等於保留):active-step 還是 :active-index?PrimeVue 3.53 TabView prop 是 :active-index="..." (per 既有 docs),但 PrimeVue Steps 才用 :active-step(per memory feedback_primevue_quirks)。Bug O 用的是 TabView 不是 Steps,所以 :active-index 才對。實作前先 read ~/Projects/Billows/Audit-Manager/compliance-manager-fe/CLAUDE.md 對 TabView 的描述。
每 phase BE 改完跑 pytest pass 後,commit 訊息加 「⚠️ 需重啟 BE pid feedback_be_restart_after_service_change)。
Files:
app/oscal/service/ssp_docx_diff_service.py(既有 290 行,加 ~350 行)tests/test_ssp_docx_diff_service.py(加 ~200 行)def test_component_entity_to_parsed_dict_roundtrip():
from unittest.mock import Mock
from app.oscal.service.ssp_docx_diff_service import SspDocxDiffService
svc = SspDocxDiffService()
entity = Mock(
title="X", component_type="service", description=None, purpose=None,
status="operational", leveraged_authorization_uid=None,
props={"protocol": "TCP", "port_ranges": "443", "security_auth": "OAuth"},
)
out = svc._component_entity_to_parsed_dict(entity, la_uid_to_title={})
assert out["title"] == "X"
assert out["component_type"] == "service"
assert out["protocol"] == "TCP"
assert out["leveraged_authorization_ref"] is None{
"row_uid": str, # current entity uid (matched/gone) or "new-<sha1(title+type)>" (added)
"title": str, # display name
"diff_status": "changed" | "added" | "gone" | "unchanged",
"default_action": "keep_current" | "use_docx" | None,
"current_values": dict | None, # entity → parsed dict
"parsed_values": dict | None,
}{
"diff_status": str,
"default_action": str | None,
"current_values": dict | None,
"parsed_values": dict | None,
}git add app/oscal/service/ssp_docx_diff_service.py tests/test_ssp_docx_diff_service.py
git commit -m "feat(ssp-oscal-alignment): Bug O Phase A — BE diff service 擴展 4 key annotation + summary
- 加 _annotate_components / _leveraged_authorizations / _inventory_items / _system_characteristic
- 加 match_components / leveraged / inventory(mirror match_parties dual-index pattern)
- 加 compute_xxx_diff 4 個 per-row compare
- build_diff_summary 加 4 key counts;annotate_parse_result 加 4 optional param
- 4 個 entity → parsed dict helper(attempt props JSONB 攤平 + LA/component ref 反查)
Tests: tests/test_ssp_docx_diff_service.py 全 PASS
Backward compat: annotate_parse_result 4 個 v3 current_* param 預設 None,既有 caller 不受影響
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Files:
app/oscal/service/ssp_docx_import_app_service.py(confirm_import + 新 method _apply_v3_decisions + _load_current_v3_lists)common/code/grc_error_code.py(加 4 個 invalid decision error code)tests/test_ssp_docx_import_app_service.pyGRC_DOCX_DECISIONS_INVALID_COMPONENT_UID = ("Component decision 引用 diff 沒有的 row_uid", "GRC_400xxx")
GRC_DOCX_DECISIONS_INVALID_LA_UID = ("Leveraged authorization decision 引用 diff 沒有的 row_uid", "GRC_400xxx")
GRC_DOCX_DECISIONS_INVALID_INVENTORY_UID = ("Inventory item decision 引用 diff 沒有的 row_uid", "GRC_400xxx")
GRC_DOCX_DECISIONS_INVALID_SC_ACTION = ("System characteristic decision action 不合法", "GRC_400xxx")decision_map = {d["row_uid"]: d["action"] for d in decisions}annotated[list_key], for each entry decide using above table (Caveat 1),build final_listparsed_result[list_key] = final_listparsed_result["system_characteristic"] = None(會讓 _dict_to_parsed_system_characteristic 回 None,sc write skip)components_decisions = payload.get("components_decisions") or []
la_decisions = payload.get("leveraged_authorizations_decisions") or []
inventory_decisions = payload.get("inventory_items_decisions") or []
sc_decision = payload.get("system_characteristic_decision") # str | None_load_current_v3_lists + 把 4 個 current_* 餵 annotate_parse_result_validate_decisions 之後加 _validate_v3_decisionsgit add app/oscal/service/ssp_docx_import_app_service.py tests/test_ssp_docx_import_app_service.py common/code/grc_error_code.py
git commit -m "feat(ssp-oscal-alignment): Bug O Phase B — BE confirm 接 4 個 v3 decisions filter(含 overwrite caveat)
- confirm_import 讀 4 個新 payload key (components_decisions / la_decisions / inventory_decisions / system_characteristic_decision)
- _load_current_v3_lists 撈 current 4 key 餵給 annotate_parse_result
- _validate_v3_decisions 防 FE 送假 row_uid
- _apply_v3_decisions 把 decision 套到 parsed_result list(注意:3 strategy overwrite 語意 → keep_current 必須 inject current row 防 delete)
- GrcErrorCode 加 4 個 invalid decision code
Tests: 全 PASS
Backward compat: 既有 FE caller 沒送 4 個新 key → 行為 = 全 use_docx(既有行為)
⚠️ 需重啟 BE 讓 fix 生效。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Files:
~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/stores/sspDocxImportStore.js(157 → ~280 行)~/Projects/Billows/Audit-Manager/compliance-manager-fe/tests/... (FE 是否有 store unit test infra?if yes write,if no skip — 待 user 拍板)decisions: {
controls: {},
parties: {},
components: {}, // {row_uid: {action}}
leveraged_authorizations: {},
inventory_items: {},
system_characteristic: { action: null }, // 單筆
}for (const c of this.parseResult?.components || []) {
decisions.components[c.row_uid] = { action: c.default_action ?? (c.diff_status === 'added' ? 'use_docx' : null) }
}
// 同 leveraged / inventory
const scDiff = this.parseResult?.system_characteristic_diff
if (scDiff) {
decisions.system_characteristic = { action: scDiff.default_action ?? (scDiff.diff_status === 'added' ? 'use_docx' : null) }
}_defaultPartyAction)return {
decisions,
parties_decisions,
components_decisions: Object.entries(this.decisions.components).map(([row_uid, d]) => ({row_uid, action: _safeAction(d.action)})),
leveraged_authorizations_decisions: ...,
inventory_items_decisions: ...,
system_characteristic_decision: _safeAction(this.decisions.system_characteristic.action),
content_overrides: this.contentOverrides,
}cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
git add src/stores/sspDocxImportStore.js
git commit -m "feat(ssp-oscal-alignment): Bug O Phase C — FE store 擴展 4 key decisions schema
- decisions 加 components / leveraged_authorizations / inventory_items / system_characteristic 4 sub-map
- initDefaultDecisions 加對應 4 key 預設 action 邏輯
- setXxxDecision / bulkApply / reset 對 4 key 同步擴展
- buildConfirmPayload 加 4 個新 payload key 送 BE
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Files (新):
~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/components/grc/ssp-docx-import-v2/diff/SystemCharacteristicDiffSection.vueComponentsDiffSection.vue / LeveragedAuthorizationsDiffSection.vue / InventoryItemsDiffSection.vueComponentDiffCard.vue / LeveragedAuthorizationDiffCard.vue / InventoryItemDiffCard.vueSystemCharacteristicDiffCard — section 內單筆直接 inline renderconst fields = [
{ key: 'title', label: '名稱' },
{ key: 'component_type', label: '類型' },
{ key: 'status', label: '狀態' },
{ key: 'description', label: '描述' },
{ key: 'purpose', label: '目的' },
{ key: 'leveraged_authorization_ref', label: '關聯外部服務' },
{ key: 'protocol', label: '通訊協定' },
{ key: 'port_ranges', label: 'Port' },
{ key: 'security_auth', label: '驗證方式' },
]const fields = [
{ key: 'title', label: '服務名稱' },
{ key: 'fedramp_package_id', label: 'FedRAMP Package ID' },
{ key: 'impact_level', label: 'Impact Level' },
{ key: 'data_types', label: 'Data Types' },
{ key: 'nature_of_agreement', label: '協議性質' },
{ key: 'authorized_users', label: '授權使用者' },
{ key: 'date_authorized', label: '授權日期' },
{ key: 'remarks', label: '備註' },
]const fields = [
{ key: 'description', label: '描述' },
{ key: 'asset_id', label: 'Asset ID' },
{ key: 'asset_tag', label: 'Asset Tag' },
{ key: 'ipv4_address', label: 'IPv4' },
{ key: 'mac_address', label: 'MAC' },
{ key: 'fqdn', label: 'FQDN' },
{ key: 'hostname', label: 'Hostname' },
{ key: 'software_name', label: '軟體名稱' },
{ key: 'os_name', label: 'OS' },
{ key: 'implemented_component_refs', label: '關聯元件', isList: true },
]<template>
<DiffSectionShell title="受評標的 (System Characteristic)" :badge="badgeText" badge-severity="warning" :collapsed="collapsed" :filter="'all'" @toggle-collapsed="collapsed=!collapsed" @bulk-apply="onBulkApply">
<div v-if="scDiff && scDiff.diff_status !== 'unchanged'" class="sc-card">
<!-- 2-col current/parsed 內 inline render 8 fields (system_name / sensitivity / status / target_type / scope_desc / system_identifier / description) -->
<!-- SelectButton bind store.decisions.system_characteristic.action -->
</div>
<div v-else class="empty-state">
<i class="pi pi-check-circle empty-icon" />
<p>受評標的無差異</p>
</div>
</DiffSectionShell>
</template>cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
git add src/components/grc/ssp-docx-import-v2/diff/
git commit -m "feat(ssp-oscal-alignment): Bug O Phase D — FE 4 個新 DiffSection + DiffCard 元件
- SystemCharacteristicDiffSection (單筆) / ComponentsDiffSection / LeveragedAuthorizationsDiffSection / InventoryItemsDiffSection
- 3 個 DiffCard mirror PartyDiffCard pattern (2-col current/parsed + SelectButton 三 action)
- SC section 不用 DiffCard,section 內 inline render 單筆
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"Files:
~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/components/grc/ssp-docx-import-v2/diff/DiffResolutionStep.vuecd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
git add src/components/grc/ssp-docx-import-v2/diff/DiffResolutionStep.vue
git commit -m "feat(ssp-oscal-alignment): Bug O Phase E — DiffResolutionStep TabView refactor
- 把既有 2 section inline render 改成 6-tab TabView 容器
- 每 tab header 顯示對應 changed/added/gone count badge
- 無衝突 tab 隱藏(避免 user 困惑)
- active tab default = 第一個有 diff 的 tab
- allSkip check 擴展到 7 key 都算
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"-- keep_current 的 row 應該 = current value,不被 docx 改
-- use_docx 的 row 應該 = parsed value
-- skip(added) 的 row 應該 不存在 DBgit add docs/features/FR-028-2605-ssp-oscal-alignment/design.md docs/changelog/2026-05-25-feat-bug-o-diff-stepper-4key-tabview.md docs/features/FR-028-2605-ssp-oscal-alignment/handoff/2026-05-25-bug-o-FIXED-SUMMARY.md
# FE i18n 在 FE repo commit
cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
git add src/locales/
git commit -m "feat(ssp-oscal-alignment): Bug O Phase F — i18n 4 個新 tab 標題"template_module_frame_id entity field wire(Bug K1 follow-up)預估時間:8-12 小時(含 test + commit + E2E verify)