Branch:
feat/ssp-doc-parser(BE + FE) — 自 2026-04-26 起累積至 2026-04-30 完成 Last BE commit:332a256· Last FE commit:1f66640
讓使用者把既有的 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 |
Heading 3 樣式的段落為主,加上 _promote_pseudo_h3 容錯(作者忘了套樣式但段落以 framework ID 開頭時自動推升)AC.L1-b.1.i) ↔︎ CMMC 2.0 (AC.L1-3.1.1) 自動透過 fuzzy 名稱比對對接,雙語標題 + [FCI DATA]【FCI 資料】 雜訊都會 被剝除後再比Malicious Code Proection), fuzzy 採 max(token_set_ratio(raw), ratio(cleaned), ratio(ascii_only)) 仍能正確命中; / ; 切 clause,每 clause 帶 (letter) 自動分配;多 letter(如 (b)(c)(d))同 clause 同時掛多個 AO1./2./... 編號,前端 markdown renderer 直接呈現有序清單implementation_status 自動設為 'implemented'(control + AO 兩層)oscal_profile.include_controls 過濾,docx 提到 baseline 之外的 control 不會偷偷建 default rowoscal_profile.include_controls(baseline 本身),只 upsert *_defaults / control_implementations 兩張表SspDocxImportDialogmode='statement_only' 與 preselect-new 都改用簡化版 PreselectSummary (勾選清單 + AO 預覽 + 一鍵套用),只有 SSP mode='full' 路徑保留舊版 diff/drag-drop UI(per-control 衝突解決有實質意義時)framework 自動從 context(project / module_frame)推導,dialog 內不再 顯示 framework 下拉dialog_title 不再顯示 SSP 字樣(內部 i18n key 不變)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)feat/ssp-doc-parser → merge / deploypoetry 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-oscal 從 0.0.13 → 0.0.14 是 SSP versioning 順帶升的(commit 2de7359),跟本 feature 無關但同 branchlsof -i :8000 找實際 listener 後 kill -9,nohup 留下 orphan PID 容易撞 port,請勿只用 pgrep pythonlsof -i :8000 # 找出 PID
kill -9 <PID>
# 重新啟動
set -a; source .env; set +a; nohup python main_socketio.py > log/app.log 2>&1 &npm run build → dist/ deploy)照下列順序跑一遍三條入口(建議用 ASIA-CMMC-SSP-DRAFT-202604.docx 或同類 真實 docx,不要用 dummy):
CMMC Level 1)compliance.ssp_docx_parse_jobs 對應 row 消失| 限制 | 說明 | 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 資料應修正 |
若上版發生重大問題:
git revert last 兩個 commit (332a256, 46db6d8) 或 checkout 前一版 release tag,重啟 servicecompliance.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';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)
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)
docs/features/FR-022-2604-ssp-doc-parser/spec.md — 完整需求規格docs/features/FR-022-2604-ssp-doc-parser/implementation-plan.md — 階段切分 plandocs/changelog/2026-04-30-fix-ssp-docx-parser-real-world-numbering-and-status.mddocs/changelog/2026-04-30-fix-ssp-docx-edit-mode-candidates-from-catalog.mddocs/changelog/2026-04-30-tweak-ssp-docx-clause-level-ao-split.mddocs/changelog/2026-04-30-tweak-template-edit-use-simplified-preview.mddocs/changelog/2026-04-30-fix-ao-binding-key-and-objectives-i18n.md| 風險 | 機率 | 影響 | 對策 |
|---|---|---|---|
| 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 要確認 |
SI.L1-3.14.2 Malicious Code Proection typo 修成 Protection(DB 既有資料髒污,影響其他模組顯示)mode='full' 編輯流程是否也改用 PreselectSummary(目前還是舊 diff UI,使用者反饋後再評估)v2 上線後跟 user 跑 smoke 一連串收 feedback + 修補 + UI 重設計。本節累積 所有 post-launch 改動、設計決策變更、後續方向,跟原 design-v2.md 一併閱讀。
| 主題 | 問題 | 修法 | 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) |
第 0 輪 — v2 上線初版:iframe + 右側 stacked Fieldset (基本資料 fieldset 永遠展開) + 標題列三顆按鈕。
第 1 輪 — 嘗試三欄 outline layout:iframe (30%) / Outline (18%) / content (55%) + scroll-spy + 響應式。User 直接 reject 「好醜」。
最終 — Step wizard 兩階段(commit 2faaa0b,採 user 提的方向):
ImportOutline.vue / scroll-spy / 響應式邏輯 / 原 stacked Fieldset12882d0、3e75c35、其他)action='skip'draft.controlOverrides (localStorage)content_overrides 欄位,_dict_to_parsed_docx 後 mutate parsed.matched_controls 再交 strategycc7ff51 / 0a9d399 / b63b1aa)1./2./...\n\n markdown-ish 字串,FE 改 marked + v-html 渲染(編號 list / 段落間距正常顯示)catalog_objective_descriptions 欄位專門帶這個)[text](url) 不渲染為連結(marked link renderer 回 plain text)e99ef49、39afeda、793d6df)adapter.adapt 後立刻跑一次,email match 的 person 直接填 matched_user_idb093592、f40ffba)User 反饋:「合規資源庫的編輯 Dialog 是不是也可以省略, 全部統一到編輯頁處理」。 採方案 C:
列表頁簡化
router.push 到 /module-frame/<uid>/template-edit編輯頁集中所有編輯入口 標題列只剩兩顆按鈕(之前 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 遞迴)。07d5864)編輯頁上方「控制項預設值 100% (15/15)」「評估目標 89% (50/56)」mini-stat 加:
User 跟 FE 確認:unmatched parties 照常寫入 oscal_parties(含 name / title / email / phone / address 等純文字資料),user_id / org_unit_id 對不到時 NULL,但其他欄位寫入。後續處理:
不會 block confirm,也不會建空殼 user。Confirm 前若有 unmatched 跳警告(待實作)。
| 模式 | 入口 | 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)。
BaseRepositoryImpl 對不存在的 filter 欄位應 raise 而不是靜默忽略(防止 9.1 第 5 行那種 bug 再發生)existing-SSP candidate 載入路徑也補 catalog_objective_descriptions(目前只 create / update-mf 有)