SSP Doc Parser — Release Notes

Branch: feat/ssp-doc-parser (BE + FE) — 自 2026-04-26 起累積至 2026-04-30 完成 Last BE commit: 332a256 · Last FE commit: 1f66640

1. 需求概述

讓使用者把既有的 docx 格式 SSP(System Security Plan)/合規程序文件, 自動帶入到 Guidant AI 的合規資源庫 / 專案規劃 / 編輯模板等模組, 省去逐控制項手動複製貼上的成本。

支援三條入口:

入口位置 UI 觸發 mode 用途
新增合規資源庫 (ModuleFrame.vue Step 1) 「從 docx 匯入」按鈕 full (preselect-new) 上傳後自動勾選 baseline 控制項 + 預先填入現況,建立資源庫時一併寫入
編輯現況與程序書 (ModuleFrameTemplateEditView.vue) 「批次維護」 → 「從 docx 匯入」 statement_only 寫入既有 module_frame 的 control_defaults / objective_defaults
專案規劃 (ProjectPlanningView.vue) 「批次維護」 → 「從 docx 匯入」 statement_only 寫入既有 SSP 的 control_implementations / objectives

2. 功能行為

解析邏輯

  • H3 標題識別:以 Heading 3 樣式的段落為主,加上 _promote_pseudo_h3 容錯(作者忘了套樣式但段落以 framework ID 開頭時自動推升)
  • 跨版本 ID 配對:CMMC 1.0 (AC.L1-b.1.i) ↔︎ CMMC 2.0 (AC.L1-3.1.1) 自動透過 fuzzy 名稱比對對接,雙語標題 + [FCI DATA]【FCI 資料】 雜訊都會 被剝除後再比
  • typo 容錯:catalog 內若有 typo(如 Malicious Code Proection), fuzzy 採 max(token_set_ratio(raw), ratio(cleaned), ratio(ascii_only)) 仍能正確命中
  • 現況抓取:H3 區段內第一條 Word 自動編號 list item 起算才是現況; 英中描述、Objective 表格、紅字註記都自動排除
  • AO 分類:每條 list item 內以 / ; 切 clause,每 clause 帶 (letter) 自動分配;多 letter(如 (b)(c)(d))同 clause 同時掛多個 AO
  • Markdown 輸出:control 級保留 1./2./... 編號,前端 markdown renderer 直接呈現有序清單

寫入策略

  • 寫入時 implementation_status 自動設為 'implemented'(control + AO 兩層)
  • 編輯模式 candidates 從 catalog 載入(含真實 control_name + AO letter keys), 並透過 oscal_profile.include_controls 過濾,docx 提到 baseline 之外的 control 不會偷偷建 default row
  • 不會修改 oscal_profile.include_controls(baseline 本身),只 upsert *_defaults / control_implementations 兩張表

UI 行為

  • 三個入口共用同一套 SspDocxImportDialog
  • mode='statement_only'preselect-new 都改用簡化版 PreselectSummary (勾選清單 + AO 預覽 + 一鍵套用),只有 SSP mode='full' 路徑保留舊版 diff/drag-drop UI(per-control 衝突解決有實質意義時)
  • framework 自動從 context(project / module_frame)推導,dialog 內不再 顯示 framework 下拉
  • 預覽畫面把 unmatched 段落分兩類:「疑似漏掉的控制項」(顯示)vs 「敘述性內容」(只計數),避免雜訊干擾使用者
  • dialog_title 不再顯示 SSP 字樣(內部 i18n key 不變)

3. 上版步驟

Step 1 — 跑 DB migration(依序)

psql -h <DB_HOST> -p 25432 -U cmmgr -d <DB_NAME> -f scripts/sql/ssp_docx_parse_jobs_migration.sql
psql -h <DB_HOST> -p 25432 -U cmmgr -d <DB_NAME> -f scripts/sql/ssp_docx_parse_jobs_source_uid_nullable.sql
psql -h <DB_HOST> -p 25432 -U cmmgr -d <DB_NAME> -f scripts/sql/ssp_docx_parse_jobs_add_org_unit_id.sql
psql -h <DB_HOST> -p 25432 -U cmmgr -d <DB_NAME> -f scripts/sql/2026-05-01-fix-ssp-docx-parse-jobs-rls.sql

⚠️ 必用 cmmgr,不要用 cm_app(被 RLS 擋住會 INSERT 0 rows 靜默失敗)

驗證 table 跟 RLS policy:

\d compliance.ssp_docx_parse_jobs
SELECT polname, polcmd FROM pg_policy
WHERE polrelid = 'compliance.ssp_docx_parse_jobs'::regclass;
-- 應看到 4 條 policy(select / insert / update / delete)

Step 2 — Backend 部署

  • Pull feat/ssp-doc-parser → merge / deploy
  • 必跑 poetry install:本次新增兩個 PyPI 公開套件(commit ee14231
    • python-docx (>=1.2.0,<2.0.0) — 讀 .docx 結構 + w:numPr XML 偵測
    • rapidfuzz (>=3.14.5,<4.0.0) — 控制項名稱 fuzzy 比對(cross-version、typo 容錯)
    • 沒有 jedi-* 內部套件變動需要進版發佈;jedi-oscal 從 0.0.13 → 0.0.14 是 SSP versioning 順帶升的(commit 2de7359),跟本 feature 無關但同 branch
  • 必須重啟 BE service(parser 邏輯、DI container、ENUM 處理都改了)
  • 重啟用 lsof -i :8000 找實際 listener 後 kill -9,nohup 留下 orphan PID 容易撞 port,請勿只用 pgrep python
lsof -i :8000  # 找出 PID
kill -9 <PID>
# 重新啟動
set -a; source .env; set +a; nohup python main_socketio.py > log/app.log 2>&1 &

Step 3 — Frontend 部署

  • 標準 Vue3 build 流程 (npm run builddist/ deploy)
  • 不需要清 user 端 cache(i18n 改動會自動帶 hash)

Step 4 — 上版後 smoke test

照下列順序跑一遍三條入口(建議用 ASIA-CMMC-SSP-DRAFT-202604.docx 或同類 真實 docx,不要用 dummy):

入口 A:新增合規資源庫

  1. 「合規資源庫管理」→「新增」
  2. Step 1 選 framework + version (e.g. CMMC Level 1)
  3. 點「從 docx 預選控制項」→ 上傳 docx
  4. 預覽畫面看到 matched 控制項 + AO 數 + orphan/noise 摘要 → 套用
  5. Step 2 看到 Tree 自動勾選預選結果(不是全選也不是空)
  6. Step 3 確認 → 建立成功
  7. 進入「編輯現況與程序書」→ 任意 control → 應看到 docx 帶入的 markdown numbered list + 實施狀態為「已實作」
  8. 展開 AO → 應看到對應 letter 的 clause 內容

入口 B:編輯既有 module_frame

  1. 「合規資源庫管理」→ 任一已有資源庫 → 「編輯現況與程序書」
  2. 「批次維護」→「從 docx 匯入」
  3. dialog 內不應看到 framework 下拉(自動帶入)
  4. 上傳 docx → 預覽勾選 → 確認匯入
  5. dialog 自動關閉,頁面 reload,control / AO 內容顯示已更新

入口 C:專案規劃

  1. 進入任一 in-progress 專案的「專案規劃」頁
  2. 「批次維護」→「從 docx 匯入」
  3. 同樣 framework 自動帶入、預覽勾選、確認匯入
  4. SSP 控制項現況已寫入

Step 5 — 異常處理 sanity

  • 上傳非 .docx → 提示「請上傳 .docx 檔案」
  • 上傳 framework 不符的 docx(如 CMMC L1 baseline 但 docx 是 ISO 27001)→ 提示「docx 主要控制項 ID 模式與選擇的 framework 不符」
  • 預覽期間關閉 dialog(按 X)→ 後端 parse_job 應被 discard,DB 內 compliance.ssp_docx_parse_jobs 對應 row 消失

4. 已知限制

限制 說明 follow-up
CMMC 1.0 → 2.0 部分映射 docx 用 1.0 ID merged 標題(如 b.1.ix Manage Visitors & Physical Access),catalog 拆成 2.0 多條(3.10.3/4/5)→ 一條 docx 段落只能配一個 control 使用者匯入後手動補
名稱差異控制項 docx 名「System & File Scanning」vs catalog 名「Periodic Scans」→ fuzzy 比不到 落到 unmatched,使用者進編輯頁手動填
Word 手打編號 若作者沒用 Word 自動編號功能,而是手打 1. 2. 在文字內,parser 抓不到 罕見場景,未支援;可日後在 _is_list_item 加 fallback regex
Catalog typo 仍存在 DB 內 SI.L1-3.14.2 名稱拼成 Malicious Code Proection(少 t) parser 已能容錯命中,但 catalog 資料應修正

5. Rollback 策略

若上版發生重大問題:

  1. FE rollback — 直接 deploy 前一版 build,使用者立刻看不到三個入口
  2. BE rollbackgit revert last 兩個 commit (332a256, 46db6d8) 或 checkout 前一版 release tag,重啟 service
  3. DB rollbackcompliance.ssp_docx_parse_jobs table 是新建的,留著 不影響其他模組;若需清乾淨:
    DROP TABLE compliance.ssp_docx_parse_jobs CASCADE;
    module_frame_control_defaults.implementation_status 欄位是既有的, docx 寫入會把 status 從 'unknown''implemented' — 若需還原, 依 docx 匯入時間範圍 reset:
    -- 只示意,請依實際匯入時間調整
    UPDATE compliance.module_frame_control_defaults
    SET implementation_status = 'unknown'
    WHERE updated_at >= '2026-05-01' AND implementation_status = 'implemented';

6. 主要 commit / changelog 索引

BE branch commits

332a256 feat(ssp-docx): real-world parser hardening + edit-mode catalog candidates + 現況 status default
46db6d8 feat(ssp-docx): preselect-new path loads catalog AO keys → AO defaults imported
3f2176e docs(changelog): SSP docx 4 critical fixes + e2e + follow-ups
e817be7 test(ssp-docx): e2e — 新增合規資源庫 + docx 匯入完整流程
c6e63a1 fix(ssp-docx): 4 critical issues — RLS / ENUM / matched current_* / cmmc-l1 2.0 ID regex
3e54d20 feat(ssp-docx): preselect-new loads catalog candidates via oscal_framework_version_uid
9872e47 feat(ssp-docx): _load_candidates loads SSP impls + catalog titles
fe5be86 feat(ssp-docx): support preselect mode (nullable source_uid + confirm payload override)
b689258 feat(ssp-docx): add Marshmallow serializers for SSP docx import API
86a7967 feat(ssp-docx): SspDocxImportAppService — orchestrate parse/preview/confirm/discard
9a43dc7 feat(ssp-docx): SspWriteStrategy — write SSP impl + objectives + info_system metadata
02be8f3 feat(ssp-docx): ModuleFrameWriteStrategy — write module_frame defaults + objectives
29dd24d feat(ssp-docx): IWriteStrategy interface + ImportDecisions/ImportResult DTO
d23c62f feat(ssp-docx): add ssp_docx_parse_jobs migration
(共 40+ commits,完整列表見 git log)

FE branch commits

1f66640 tweak(ssp-docx): unify entry points + simplified preview + AO binding fixes
a63a1bb feat(ssp-docx): preselect summary — expand-all / collapse-all toolbar
6f41622 feat(ssp-docx): preselect-only mode uses simplified PreselectSummary view
4df300f feat(ssp-docx): forward oscal_framework_version_uid to BE
038a47a feat(ssp-docx): preselect mode for new module_frame creation (chain confirm)
5128f7c feat(ssp-docx): Phase I — i18n strings + 3 entry points integration
3327328 feat(ssp-docx): Phase H — drag-drop + conflict modal
0f6928b feat(ssp-docx): Phase G — wizard step 0/1/2 + diff/banner/toolbar
b27e333 feat(ssp-docx): Phase F — API constants + service + Pinia store + dialog shell
(共 15+ commits)

重要 changelog

  • docs/features/FR-022-2604-ssp-doc-parser/spec.md — 完整需求規格
  • docs/features/FR-022-2604-ssp-doc-parser/implementation-plan.md — 階段切分 plan
  • docs/changelog/2026-04-30-fix-ssp-docx-parser-real-world-numbering-and-status.md
  • docs/changelog/2026-04-30-fix-ssp-docx-edit-mode-candidates-from-catalog.md
  • docs/changelog/2026-04-30-tweak-ssp-docx-clause-level-ao-split.md
  • FE docs/changelog/2026-04-30-tweak-template-edit-use-simplified-preview.md
  • FE docs/changelog/2026-04-30-fix-ao-binding-key-and-objectives-i18n.md

7. 上版風險評估

風險 機率 影響 對策
RLS migration 漏跑 高(INSERT 全失敗、看似系統當) Step 1 RLS policy 驗證 SQL;上版 SOP 加 checklist
BE service 沒重啟 高(新 parser 邏輯沒 load) lsof -i :8000 看 PID 確認
既有 module_frame default 被 docx 覆蓋 中(使用者重要的客製化現況遺失) baseline scope guard 已限制不寫 baseline 外的 row;對已存在 row 一律 upsert(會覆蓋)— 上版前可選擇先 dump module_frame_control_defaults
既有 SSP 的 implementation_status 被改 只在 docx 匯入時觸發,不會自動跑;使用者主動操作才會改
catalog typo 配錯到別的 control 已用 ratio(cleaned, name) 而非 partial_ratio 避開 superset 配對;smoke test SI.L1-3.14.2 要確認

8. 後續建議(非阻擋本次上版)

  1. Catalog 資料修正:SI.L1-3.14.2 Malicious Code Proection typo 修成 Protection(DB 既有資料髒污,影響其他模組顯示)
  2. CMMC 1.0 ↔︎ 2.0 ID mapping table 建立(避免每次仰賴 fuzzy 名稱比對)
  3. SSP mode='full' 編輯流程是否也改用 PreselectSummary(目前還是舊 diff UI,使用者反饋後再評估)
  4. Word 手打編號 fallback regex(罕見但有真實案例)

9. 上線後迭代(2026-05-03)

v2 上線後跟 user 跑 smoke 一連串收 feedback + 修補 + UI 重設計。本節累積 所有 post-launch 改動、設計決策變更、後續方向,跟原 design-v2.md 一併閱讀。

9.1 Smoke 修補(資料 / 流程 / DI bug)

主題 問題 修法 Commit
MinIO 存儲 parser 找不到檔案 docx 存 MinIO bucket,parser 用 local path 讀失敗 tempfile 兩線:parser 拿 NamedTemporaryFile、upload 進 MinIO 拿 file_uid 給 FE iframe 34109d2
Adapter registry DI 沒 resolve adapters={"cmmc-l1": cmmc_ssp_adapter} 是普通 dict,provider 不會 auto-resolve → registry.get() 回 Singleton 物件 → .adapt() AttributeError 改用 providers.Dict({...}) 包裝 a09c740
Information-provider 應為 person 不是 org docx 上游廠商欄位實際填的是聯絡人 (姓名+Title+Email),當 organization 永遠 reconcile 不到 org_unit _ORGANIZATION_ROLES = {"responsible-organization"} 只剩這個是 org 012e46a
編輯頁全空(profile_controls = 0) FE 送 include_controls: ["AC.L1-3.1.1", ...] (control_id) 但 add_profile 預期 catalog UID list → 全 miss → 0 個 profile_controls 寫入 baseline_controls 加 catalog_control_uid 欄位、FE 改送 UID b898879
跨 catalog UID 撈錯 第一版 lookup 用 CatalogControlQueryEntity(catalog_id=...) 但 OscalCatalogControl 沒 catalog_id column → 靜默忽略 → 全表掃 → dict 覆蓋拿到別 framework 的 UID candidate 直接帶 catalog_control_uid,沿 groups path 走對的 catalog 6671a25
Confirm 後 412 log 汙染 router.push 觸發 onBeforeUnmount 對已 completed 的 parse_job DELETE → BE 噴 412 confirm 成功後 parseUid.value=null + parsedResult.value=null 阻斷 07d5864 (FE)

9.2 UI / UX 重設計(多輪迭代)

第 0 輪 — v2 上線初版:iframe + 右側 stacked Fieldset (基本資料 fieldset 永遠展開) + 標題列三顆按鈕。

第 1 輪 — 嘗試三欄 outline layout:iframe (30%) / Outline (18%) / content (55%) + scroll-spy + 響應式。User 直接 reject 「好醜」。

最終 — Step wizard 兩階段(commit 2faaa0b,採 user 提的方向):

  • Step 1 上傳:置中卡片 (Card),內含 framework cascaded picker + dropzone,極簡。Parse loading 顯示 ProgressSpinner。
  • Step 2 預覽與確認:iframe 左 (40%) + TabView 右 (60%),三 tab:
    • 基本資料(create mode 才有,第一個 tab)— 垂直單欄 form (max-width 600px 置中),必填 * 紅色,header 帶 ⚠ icon 當必填空
    • 控制項 (15/17) — header 帶實時進度
    • 參與人員 (5) — header 帶人數
  • 確認按鈕 disabled until basicInfoComplete;submit 撞到必填空切回基本資料 tab + toast 缺漏欄位
  • ImportOutline.vue / scroll-spy / 響應式邏輯 / 原 stacked Fieldset

9.3 控制項挑選 / 編輯(commits 12882d03e75c35、其他)

  • per-control checkbox:每個 matched control 卡片標題列加 Checkbox,未勾 → opacity-60 + confirm action='skip'
  • per-AO checkbox:每個 AO row Checkbox,可單獨 skip 該 AO
  • inline edit:主述 / 每個 AO 旁邊 ✎ 按鈕 → 切 Textarea + 顯式「儲存 / 取消」buttons → 寫入 draft.controlOverrides (localStorage)
  • 未匹配控制項可勾選:missing 段每筆加 Checkbox,預設不勾(docx 沒提的由 user 主動加 baseline)
  • BE confirm payload 多 content_overrides 欄位,_dict_to_parsed_docx 後 mutate parsed.matched_controls 再交 strategy

9.4 主述 / AO 內容渲染(commits cc7ff51 / 0a9d399 / b63b1aa

  • BE parser 已輸出 1./2./...\n\n markdown-ish 字串,FE 改 marked + v-html 渲染(編號 list / 段落間距正常顯示)
  • AO 卡片改兩列:
    • 上:catalog 來的「題目」(BE 加 catalog_objective_descriptions 欄位專門帶這個)
    • 下:docx 解析內容(縮排 + 次要色,無 prefix 無 → 箭頭,user 反饋過於 verbose)
  • bold / italic 在 python-docx 那層已 strip,要保留得改 BE 走 runs (follow-up)
  • URL / [text](url) 不渲染為連結(marked link renderer 回 plain text)

9.5 Parties reconcile + UX(commits e99ef4939afeda793d6df

  • parse-time reconcile:之前只 confirm 才 reconcile,FE 預覽永遠看到「未連結」。改成 adapter.adapt 後立刻跑一次,email match 的 person 直接填 matched_user_id
  • 取消連結 / 重新連結 button:已連結時旁邊多兩顆按鈕,未連結時只有「連結帳號」
  • 連結對話框 dynamic header:organization → 「連結到組織單位」、person → 「連結到系統使用者」(之前混在一起叫「系統帳號 / 部門」看不出走 user 還是 org_unit)

9.6 模板編輯頁編輯入口大整併(commits b093592f40ffba

User 反饋:「合規資源庫的編輯 Dialog 是不是也可以省略, 全部統一到編輯頁處理」。 採方案 C:

列表頁簡化

  • v1 編輯 Dialog 廢棄
  • 列表 row「編輯」按鈕 → router.push/module-frame/<uid>/template-edit
  • 列表頁「新增」 dialog 簡化成單頁:砍 Step 3 review、Step 1+2 合併(基本資料 + framework + 折疊式 control tree picker),watch on selectedVersionLevels 自動 fetch catalog

編輯頁集中所有編輯入口 標題列只剩兩顆按鈕(之前 4 顆):

按鈕 內容
✎ 編輯 menu 4 個 menuitem:編輯基本資料 / 編輯適用控制項 / 參與單位 / 人員 / 程序書池
🗄 批次維護 menu 維持原樣(匯出 / 匯入 / 從 docx 匯入)

新元件 / endpoints:

  • EditIncludeControlsDialog.vue(FE)— Tree picker,預勾現有 baseline,diff +/-,移除有 defaults 的 control 跳警告。動態高度 (loading 短、tree 載入後長),save overlay 顯示 spinner。
  • ModuleFramePartiesDialog.vue(FE)— GET / POST / PUT / DELETE 串接 module_frame 的 parties,含 link / unlink / 編輯 / 刪除。
  • ModuleFramePartyService + 4 個 BE endpoints(commit 59963a0):GET / POST / PUT / DELETE /module-frame/<uid>/parties[/<party_uid>]。DI 放在 oscal_container(避免 module_frame ↔︎ oscal 雙向 override 撞 deepcopy 遞迴)。

9.7 進度 hover 顯示未設定(commit 07d5864

編輯頁上方「控制項預設值 100% (15/15)」「評估目標 89% (50/56)」mini-stat 加:

  • hover tooltip:「點擊查看 N 個未設定」
  • click → OverlayPanel 列出未設定的 control_id / AO key + name
  • 點 overlay 內 control → 自動展開 group + 選中 control(scrollIntoView)

9.8 對 Parties「未連結」的處理共識

User 跟 FE 確認:unmatched parties 照常寫入 oscal_parties(含 name / title / email / phone / address 等純文字資料),user_id / org_unit_id 對不到時 NULL,但其他欄位寫入。後續處理:

  • User 在編輯頁 PartiesDialog 手動補連結
  • 重 import 時 BE parse-time reconcile 再跑(user 表新增的 email 自動補上)
  • DB 直接撈純文字資料供 CSV / 報表參考

不會 block confirm,也不會建空殼 user。Confirm 前若有 unmatched 跳警告(待實作)。

9.9 流程模式對照(更新版)

模式 入口 target_uid 寫入位置
create 合規資源庫管理 → 「新增」 → 「從 docx 建立」 無(FE pre-create module_frame after confirm) module_frame_control_defaults + objective_defaults + oscal_parties (context_type='module_frame')
update-mf 編輯現況與程序書 → 批次維護 → 從 docx 匯入 module_frame.uid 同上,但 module_frame 已存在
update-ssp 專案規劃 → 批次維護 → 從 docx 匯入 ssp.uid SSP 的 control_implementation + objectives

module_frame 是模板(template,clone 給專案用),SSP 是專案實例(一個專案 + 一輪 AP = 一個 SSP)。

9.10 後續 follow-up(本次未做)

  • BE parser 走 paragraph runs 保留 bold / italic(目前在 python-docx text 那層已 strip)
  • jedi-oscal BaseRepositoryImpl 對不存在的 filter 欄位應 raise 而不是靜默忽略(防止 9.1 第 5 行那種 bug 再發生)
  • Confirm 前 unmatched parties 跳警告 dialog
  • Update-SSP 模式下 existing-SSP candidate 載入路徑也補 catalog_objective_descriptions(目前只 create / update-mf 有)
  • LeveragedSection.vue / MetadataSection.vue / UnmatchedSection.vue 已停用,待清理
  • Server-side libreoffice 缺 CJK 字型導致 PDF 預覽中文字醜(ops 層問題,需安裝 fonts-noto-cjk)