Bug D — Leveraged Services 寫入 1/4 Handoff(2026-05-25)

項目 內容
緣由 Bug B 收尾後 user retry docx import,發現 mf template-edit「外部利用服務 (1)」但預覽顯示 4 筆。BE 寫了 1 筆,漏 3 筆
Branch feature/ssp-oscal-alignment(兩 repo 一致,user 已 push 到 origin)
Source mf 5eb137c8-84b6-4894-b0dd-a25ae092a8d2 (id=368);SSP shell id=266
接手前必讀 本文件自包含 — 不必另開其他 prompt;按 §0 順序讀本檔 + reference 文件即可
預估時間 1.5-3 小時(追 v2-bundle confirm 完整 path + DB / FE 雙端 verify)

0. 接手讀序(fresh session 必照順序)

  1. 本文件 — Bug D 完整 context(症狀 / verify / 兩條候選 / 開工順位 / 行為規範 / 收尾流程)
  2. 2026-05-25-bug-b-FIXED-SUMMARY.md — Bug B 兩段 fix 收尾 + 教訓(必讀,避免回頭追 Bug B;以及學 silent-failure pattern 教訓)
  3. design.md §11.17 + §11.18 + §11.22~§11.24 — Bug A SspShellService 拆分 / v2-bundle confirm 整體流程 / Bug B 兩段 reconciliation
  4. app/oscal/service/ssp_docx_import_app_service.py:_run_v2_bundle_confirm (~line 703) — Bug D 主嫌路徑
  5. SspImportConfirmService.confirm 實作(grep 找)— LA / Component / Inventory write 核心

讀完後跑 §6 Pre-flight + §7 Verify Bug B → §3 Verify root cause 候選 → §4 開工順位。


1. 症狀

1.1 User-facing

mf 5eb137c8 template-edit 「外部利用服務」tab 顯示 (1) — 1 筆「MDR、病毒碼與威脅情資同步更新」。但 docx 匯入預覽「外部利用服務 (4)」— 4 筆。寫了 1 筆,漏 3 筆

1.2 DB 證據(cmmgr SET app.is_super_admin='t'

-- mf 368 → profile 134 → ssp 266
SELECT id, oscal_profile_uid FROM public.module_frames
 WHERE uid='5eb137c8-84b6-4894-b0dd-a25ae092a8d2';
-- id=368, profile_uid='ad02f785-...'

SELECT id FROM oscal.profiles WHERE uid::text='ad02f785-fab0-45b7-91e7-ca80ac0ea0e8';
-- profile_id=134

SELECT id, uid::text FROM oscal.system_security_plans WHERE profile_id=134;
-- ssp id=266, uid='10829f1d-04f1-469c-988b-3d66e9e27040', status=draft

SELECT id, ssp_id, title FROM oscal.ssp_leveraged_authorizations WHERE ssp_id=266;
-- 20 | 266 | MDR、病毒碼與威脅情資同步更新   ← 只 1 筆,預期 4 筆

1.3 BE log 證據(confirm payload,2026-05-25 01:11:53)

{
  "decisions": [...15 controls...],
  "parties_decisions": [...5 parties...],
  "manual_assignments": [],
  "skipped_paragraph_idxs": [],
  "predicted_controls_user_selection": [...],
  "source_uid": "5eb137c8-84b6-4894-b0dd-a25ae092a8d2",
  "content_overrides": {}
}

完全沒有 leveraged_services / leveraged_authorizations 欄位。BE 寫的 1 筆是 _run_v2_bundle_confirmparse_job.parsed_result 自動寫的(不靠 FE 決策),但只寫 1 筆 — 4 筆少了 3。


2. 前次教訓(避免重蹈覆轍)

Bug B 從 2026-05-20 追到 2026-05-25,跨 2 sessions / 5 天才完整 close。本期 Bug D 別重複以下錯誤:

  1. 「寫成功」計數會騙人parties_written=5 騙了兩段時間,root cause 在 _upsert_responsible_party silent-skip 卻沒在計數體現。Bug D fix 後必查 DB ssp_leveraged_authorizations 真有 4 筆,不要相信 BE log 的 caller 計數
  2. Cross-source consistency — Bug B 第二段就是「Excel 已修,docx 漏修」。Bug D 也要看 Excel 走同一 confirm_service.confirm() 是否有同樣症狀 — 若 Excel OK,比對兩條 path 找 docx 偏差。
  3. 不要假設 user flow — 前次 session 一開始假設 user 走 update mode 修錯地方。Bug D 同樣場景:user 從「合規資源庫 → 新增 → 從文件建立」進 → URL /module-frame/import-docxcreate mode,wizard 跳 Step 3,預覽用 LeveragedSection.vue 不是 diff stepper。
  4. 直面修,不要 caveat 甩鍋 — 把「BE 只寫 1/4」標 caveat 叫 user 手動補另外 3 筆是 anti-pattern。修到 root cause 為止。

3. 兩條 root cause 候選(先 verify 哪邊)

3.1 候選 A — BE 端:v2-bundle confirm 寫 LA 時某條件擋掉 3 筆

該追的 path

步驟 檔案 / 行號 該看什麼
1 ssp_docx_import_app_service.py:_run_v2_bundle_confirm (~line 703) source_type='module_frame' branch 走 ssp_shell_service.ensure_shellconfirm_service.confirm(ssp_id=...)
2 SspImportConfirmService.confirm 實作 處理 LA / Component / Inventory write 的邏輯;leveraged 是否依賴 catalog_id?line 749-755 註解說「catalog_id 是 None — orchestrator's catalog-scoped reconcilers skip gracefully」— 可能 reconciler 過濾掉 3 筆
3 bundle.parsed_leveraged_authorizations 從哪填 docx adapter app/oscal/service/ssp_docx_import_app_service.py:424 _convert(asdict(la)) for la in bundle.parsed_leveraged_authorizations — 找 bundle build 處
4 parsed_ssp.leveraged_services vs bundle.parsed_leveraged_authorizations 兩個概念可能不同 — FE 預覽 4 筆是哪個 source?BE 寫 1 筆是哪個 source?
5 BE log 完整 confirm trail grep "v2-bundle|written_las|confirm_service|Docx v2-bundle confirm" log/app.log

Verify A

# A1: 查最近 parse_job 內 parsed_result 的 leveraged 結構
PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -c "
SET app.is_super_admin='t';
SELECT id, status,
       jsonb_array_length(COALESCE(parsed_result->'leveraged_services', '[]'::jsonb)) AS lev_svcs,
       jsonb_array_length(COALESCE(parsed_result->'leveraged_authorizations', '[]'::jsonb)) AS lev_auths
  FROM oscal.parse_jobs
 ORDER BY id DESC LIMIT 5;
"
# 解讀:
# - lev_svcs=4 + lev_auths=1 → A2 adapter 把 4 services 縮成 1 authorization(候選 A 成立,上游 adapter bug)
# - 兩者都 4 → confirm_service 寫入時擋掉 3(候選 A 成立,下游 write bug)
# - 兩者都 1 → docx parser 抽錯 / dedup(更上游 extractor bug)

# A2: 查 ssp 266 完整 confirm 寫入路徑 BE log
grep -E "ssp_id=266|leveraged|v2-bundle|written_las|written_components|reconciler" \
     ~/Projects/Billows/Audit-Manager/compliance-manager-be/log/app.log | tail -50

3.2 候選 B — FE 端:應該把 leveraged 加進 payload 但漏送

該追的 path

步驟 檔案 / 行號 該看什麼
1 compliance-manager-fe/src/components/grc/ssp-docx-import-v2/SspDocxImportPage.vue onConfirm (~line 500-610) 看是否該組 leveraged_decisions / leveraged_services_decisions 進 payload
2 compliance-manager-fe/src/components/grc/ssp-docx-import-v2/sections/LeveragedSection.vue 預覽顯示 4 筆的元件 — 有沒有 user decision UI?
3 compliance-manager-fe/src/stores/sspDocxImportStore.js 看 store 有沒有 decisions.leveraged_services 結構
4 BE confirm schema _validate_decisions 等是否預期收 leveraged 欄位

Verify B

grep -rn "leveraged_services_decisions\|leveraged_decisions\|decisions.leveraged" \
     ~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/
# 完全沒 hit → 候選 B 成立(FE 從沒實作 leveraged 決策)

grep -n "props.leveraged\|leveraged_services\|emit.*update" \
     ~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/components/grc/ssp-docx-import-v2/sections/LeveragedSection.vue

3.3 推測(待 verify)

直覺:候選 A 概率較高。理由:

  • content_overrides={} 是空的(FE 預設 — 邏輯上 leveraged services 應該透過 content_overrides.leveraged_services 傳,但 user 沒編輯所以空)
  • BE 既有寫 1 筆(不是 0 筆),代表 confirm path 有跑 leveraged write,只是部分 — 那是寫入時 filter
  • Excel/docx 共用 confirm_service.confirm() — Excel 端可能 OK 或同樣有 bug,cross-source check 必跑

但也不排除 A + B 混合:FE 該支援 leveraged decisions 但沒做,BE 寫入也有 bug。


4. 開工順位

  1. 跑 §6 Pre-flight + §7 Verify Bug B(5-10 分鐘)
  2. 跑 §3.1 + §3.2 Verify command 確認候選 A vs B(10 分鐘)— 結果決定後續走 BE 還 FE
  3. 追 root cause 到底
    • 候選 A → trace SspImportConfirmService.confirm 內 LA write 邏輯,找出 3 筆被擋的條件
    • 候選 B → 看 FE Step 3 是否該補 leveraged_decisions payload;BE schema 是否預期收
    • 混合 → 兩邊都修
  4. Fix + DB 雙端 verify:query ssp_leveraged_authorizations WHERE ssp_id=<new_ssp> 真有 4 筆才算 done。不要相信 BE log 的 caller 計數(§2.1 教訓)
  5. 補 design.md §11.25(mirror §11.22~§11.24 寫法,含 root cause 哪條候選成立 / why / 教訓)
  6. commit + Bug D FIXED SUMMARY handoff(mirror 2026-05-25-bug-b-FIXED-SUMMARY.md
  7. 問 user push 規模(永不自動 push)

5. 該讀的檔案 / 預期改動範圍

5.1 BE 端

檔案 為何 read
app/oscal/service/ssp_docx_import_app_service.py:_run_v2_bundle_confirm (~line 703) confirm path 入口,看 catalog_id=None 是否影響 LA write
app/oscal/service/ssp_docx_import_app_service.py:424 附近 bundle.parsed_leveraged_authorizations build 處
SspImportConfirmService.confirm 實作(grep 找) LA / Component / Inventory write 核心邏輯
app/oscal/service/ssp_excel_import_app_service.py:_run_v2_bundle_confirm (~line 1602) Excel 走同一 confirm_service — 看 Excel 有沒有踩 Bug D(cross-source check)

5.2 FE 端

檔案 為何 read
compliance-manager-fe/src/components/grc/ssp-docx-import-v2/SspDocxImportPage.vue:500-610 onConfirm 組 payload 處,看有沒有 leveraged_decisions
compliance-manager-fe/src/components/grc/ssp-docx-import-v2/sections/LeveragedSection.vue 預覽顯示 4 筆 leveraged 的元件
compliance-manager-fe/src/stores/sspDocxImportStore.js 看 decisions 結構是否有 leveraged
compliance-manager-fe/src/composables/useSspLeveraged.js leveraged 相關 composable(如果有)

5.3 改 FE 前必做

跨 repo 改 FE 前必 read ~/Projects/Billows/Audit-Manager/compliance-manager-fe/CLAUDE.md(per memory feedback_cross_repo_read_claude_md_first)。


6. Pre-flight Command(必跑)

# 1. 兩 repo branch
git -C ~/Projects/Billows/Audit-Manager/compliance-manager-be branch --show-current
git -C ~/Projects/Billows/Audit-Manager/compliance-manager-fe branch --show-current
# 預期:兩個都 feature/ssp-oscal-alignment

# 2. 兩 repo working tree
git -C ~/Projects/Billows/Audit-Manager/compliance-manager-be status --short
# 預期:M pyproject.toml only (dev path-dep,永不 stage)
git -C ~/Projects/Billows/Audit-Manager/compliance-manager-fe status --short
# 預期:clean

# 3. local vs origin(user 已 push)
git -C ~/Projects/Billows/Audit-Manager/compliance-manager-be log origin/feature/ssp-oscal-alignment..HEAD --oneline | wc -l
# 預期:0
git -C ~/Projects/Billows/Audit-Manager/compliance-manager-fe log origin/feature/ssp-oscal-alignment..HEAD --oneline | wc -l
# 預期:0

# 4. BE 是否有重啟接 Bug B 第二段 fix (fef5eecf)
lsof -t -i:8000   # 若有 listener,pid → ps -o lstart -p <pid> 看啟動時間
# 若 BE 還沒接 fef5eecf 重啟一次:
# lsof -t -i:8000 | xargs -r kill -9 && python main_socketio.py

# 5. BE test smoke
poetry run pytest tests/test_ssp_docx_import_app_service.py -q
# 預期:全綠

7. Verify Bug B 確實 close(必跑,避免回頭追)

PGPASSWORD='jedi@123!' psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev -c "
SET app.is_super_admin='t';
SELECT id || ' role=' || role_id || ' party=' || party_uuid::text
  FROM oscal.oscal_responsible_parties
 WHERE context_type='module_frame' AND context_id=368;
"
# 預期 5 筆 (id 216-220):
#   216 role=responsible-organization (org)
#   217-220 role=system-user (4 person)

如果 5 筆都在 → Bug B close,繼續 Bug D。 如果 0 筆 → Bug B 沒 close(不該發生但 verify 一下),先回頭挖 §11.22 / §11.24 是否被誰 revert。


8. 行為規範重要提醒(per CLAUDE.md + memory)

  • 永不切 branch:發現 branch 不對停下問 user,不自己 fix(per CLAUDE.md / feedback_no_branch_switch
  • 可自行 commit,不自動 push:user 明確指示才 push(per CLAUDE.md / feedback_stage_commit_no_ask
  • 跨 repo 改 FE 前必 read FE CLAUDE.md(per memory feedback_cross_repo_read_claude_md_first
  • 改 BE service 層後必提醒 user 重啟(per memory feedback_be_restart_after_service_change,BE 沒 hot reload)
  • 改套件先走 poetry path dependency(per memory feedback_jedi_package_dev_path_first,dev 階段不每改必 bump version)
  • DB 兩張表都 verify 才算 done(§2.1 教訓 — caller 計數會騙人)
  • 不要把 bug 標 caveat 甩給 user(Bug B 前 session 的錯誤示範 — 直面修,不迴避)
  • changelog 收尾 batch 寫(user 說「summary / 告一段落」才寫,per CLAUDE.md)
  • 不要晶晶體(per memory feedback_no_chinglish — 動詞 / 連接詞用中文,技術專有名詞保留英文)
  • plan 內 method/欄位假設先 verify 才開工(per memory feedback_plan_vs_reality_verify_first

9. 收尾流程(fix 完後)

  1. design.md §11.25 補 Bug D fix entry(mirror §11.22~§11.24 寫法,含「為何 §11.22~§11.24 fix Bug B 沒順手解 Bug D」分析)
  2. handoff/2026-05-25-bug-d-FIXED-SUMMARY.md 寫短收尾報告(mirror 2026-05-25-bug-b-FIXED-SUMMARY.md 樣式:root cause / commits / DB verify / 教訓 / 殘留)
  3. commit(BE fix + design.md + handoff SUMMARY;FE 若改也獨立 commit)
  4. 問 user 確認 push 規模(本期 Bug D commits + 任何累積的 unpushed)
  5. user 拍板才做 jedi 進版 / 版號對齊 / 改 silent-skip pattern 等 follow-ups

10. 不在 Bug D scope

  • L4 5-tab 結構 spec drift(design.md §4.1 仍 spec 5-tab,本期保持 4-tab)
  • L8 jedi-oscal 進版 + Nexus push(user 拍板)
  • L9 BE/FE 版號 1:1 對齊 v1.1.0 → v1.2.0(同 L8)
  • Phase 5 OSCAL Export(已 deferred)
  • jedi-oscal _upsert_responsible_party silent-skip cleanup(列 jedi 進版時做)
  • Cross-source ParsedParty adapter 共用 helper refactor(Bug B 教訓延伸)
  • mf 367 (fc51dd08) 失敗 import 殘留清理(無害,user 可自行刪)

11. 前次 session(2026-05-25 凌晨)commits 清單

origin/feature/ssp-oscal-alignment 已包含(user 已 push):

BE:
4441d3f4 docs(ssp-oscal-alignment): Bug B 收尾 SUMMARY + Bug D handoff
fef5eecf fix(ssp-oscal-alignment): Bug B 第二段 — docx _dict_to_parsed_parties 補 default role
ba7885d4 docs(ssp-oscal-alignment): design §11.22 + §11.23 — Bug B + Issue 1 reconciliation
8648940d docs(ssp-oscal-alignment): fix phase4 SUMMARY §5 L7 drift — Bug B 改現在優先 fix
375236a9 docs(ssp-oscal-alignment): Bug B + parties stepper handoff for next session

FE:
f31d0cf  fix(ssp-oscal-alignment): Bug B parties default decision + Issue 1 缺欄位 render

更早的 Phase 4 task arc commits (BE 34 + FE 5) 也已 push。


12. 給 fresh session 的超短 prompt(user 用這個貼)

接手 SSP OSCAL Alignment Bug D — docx import 4 leveraged services 只寫 1
筆。完整 context 全在
docs/features/FR-028-2605-ssp-oscal-alignment/handoff/2026-05-25-bug-d-leveraged-services-handoff.md
按本檔 §0 接手讀序執行 — 必跑 §6 Pre-flight + §7 Verify Bug B → §3 verify
候選 → §4 開工。注意 §2 前次教訓 + §8 行為規範。前次 (Bug B 兩段) 已 close。