對應
design-v2.md預期執行模式:subagent-driven development(每 phase 結束會打 commit) 開工條件:使用者明確說「開工」
| Phase | 範圍 | 預估 commits | 阻擋下一階段? |
|---|---|---|---|
| 0. 前置 | 既有 v1 程式碼盤點、cleanup 順序確認 | 0(純規劃) | ❌ |
| 1. BE Schema | oscal_parties 加 4 欄 + index + GRANT |
1 migration | ✅ |
| 2. BE Parser 分層 | 抽 ISspDocxAdapter interface + CmmcSspAdapter 殼 | 3-4 | ❌(v1 並存) |
| 3. BE Adapter 內容擴充 | CmmcSspAdapter 抓 metadata / parties / leveraged | 4-6 | ❌ |
| 4. BE Write Strategy 擴充 | 寫入 parties / leveraged / metadata(不影響既有 controls 寫入) | 3-4 | ❌ |
| 5. BE Confirm flow 統一 | 接收 v2 payload;create + update 雙 mode | 2-3 | ✅ |
| 6. FE 獨立頁 shell | route + ImportDocxPage.vue + Splitter 佈局 | 2 | ❌ |
| 7. FE Section components | 5 個 SectionComponent + PartyLinkDialog | 5-7 | ❌ |
| 8. FE LocalStorage 同步 | useSspDocxDraft composable | 1 | ❌ |
| 9. FE 補連結 UI | 編輯現況與程序書頁加「參與單位 / 人員」section | 2-3 | ❌ |
| 10. FE 入口替換 | 合規資源庫管理首頁加「從 docx 建立」按鈕 + 拆掉舊 dialog | 2 | ✅ |
| 11. v1 cleanup | 廢棄 SspDocxImportDialog / PreselectSummary / chain confirm | 2-3 | ❌ |
| 12. 整合測試 | smoke + e2e + i18n 補齊 | 2-3 | ❌ |
預估總 commits:30-40 預估時間(subagent-driven,含 review):3-5 個工作天
確認下列 v1 元素的廢棄順序,避免 v2 寫到一半既有 user 的 docx 匯入流程壞掉:
| v1 元素 | v2 廢棄時機 |
|---|---|
SspDocxImportDialog.vue |
Phase 11 |
PreselectSummary.vue |
Phase 11 |
StepUpload/StepPreview/StepResult.vue |
Phase 11 |
| ModuleFrame.vue「從 docx 預選」按鈕 | Phase 10 |
| ModuleFrameTemplateEditView 批次維護 docx 入口 | Phase 10(換 route) |
| ProjectPlanningView 批次維護 docx 入口 | Phase 10(換 route) |
BE mode='full' chain confirm 邏輯 |
Phase 5 |
BE pending_parse_uid 邏輯 |
Phase 5 |
pytest tests/ -k 'docx or write_strategy' -q --collect-only | head -30確認 v2 開工前哪些既有 test 會被影響、哪些可繼續用、哪些要重寫。
# 手動驗證
from jedi_file_upload.service.file_upload_service import FileUploadService
# upload 一份測試 docx → 驗 GET /upload-file/<uid>/preview-as-pdf 可正常 render驗證內容:
Phase 0 不寫 code,只做盤點 + 確認決策
scripts/sql/2026-XX-XX-oscal-parties-extensions.sql:
-- Date: 2026-XX-XX
-- 1. oscal_parties 加 4 欄供 SSP docx import 鉤稽 + 聯絡資料保留 (2026-XX-XX)
ALTER TABLE oscal.oscal_parties
ADD COLUMN IF NOT EXISTS title varchar(255),
ADD COLUMN IF NOT EXISTS email_address text,
ADD COLUMN IF NOT EXISTS telephone_number varchar(50),
ADD COLUMN IF NOT EXISTS address text,
ADD COLUMN IF NOT EXISTS user_id integer, -- soft ref public.members.id
ADD COLUMN IF NOT EXISTS org_unit_id integer; -- soft ref public.org_units.id
-- 2. 加 index (2026-XX-XX)
CREATE INDEX IF NOT EXISTS ix_oscal_parties_email
ON oscal.oscal_parties(email_address);
CREATE INDEX IF NOT EXISTS ix_oscal_parties_user_id
ON oscal.oscal_parties(user_id);
CREATE INDEX IF NOT EXISTS ix_oscal_parties_org_unit_id
ON oscal.oscal_parties(org_unit_id);infra/oscal/models/... 加欄位domain/oscal/entity/oscal_party_entity.py 加 fieldsinfra/oscal/mappers/... 加 mappingdocs/changelog/2026-XX-XX-feat-ssp-docx-v2-oscal-parties-extensions.md
ISspDocxAdapter interfacedomain/oscal/adapter/i_ssp_docx_adapter.py:
from abc import ABC, abstractmethod
class ISspDocxAdapter(ABC):
@abstractmethod
def adapt(
self,
parsed: 'ParsedDocx', # 既有 intermediate
candidates: list,
) -> 'ParsedSsp':
"""Map raw parsed docx structure to OSCAL-aligned ParsedSsp."""ParsedSspdomain/oscal/parser/ssp_intermediate.py:定義 dataclasses:
CmmcSspAdapter 殼domain/oscal/adapter/cmmc_ssp_adapter.py:
class CmmcSspAdapter(ISspDocxAdapter):
def adapt(self, parsed_docx, candidates):
# v2 Phase 2 殼版:先把既有 controls + AOs 直接轉過來
# parties / leveraged / metadata 留 TODO 給 Phase 3
return ParsedSsp(
metadata=..., # TODO Phase 3
parties=[], # TODO Phase 3
leveraged_services=[], # TODO Phase 3
matched_controls=parsed_docx.matched_controls,
unmatched_paragraphs=parsed_docx.unmatched_paragraphs,
information_types=[],
summary=parsed_docx.summary,
framework_specific_props={},
)di_containers/oscal/oscal_containers.py:
cmmc_ssp_adapter = providers.Factory(CmmcSspAdapter)
adapter_registry = providers.Factory(
AdapterRegistry,
adapters={'cmmc-l1': cmmc_ssp_adapter, 'cmmc-l2': cmmc_ssp_adapter}
)DocxParserCore 維持原樣,只是它的輸出(ParsedDocx)會被 adapter 多一層轉換。
tests/test_cmmc_ssp_adapter.py:覆蓋 controls + AOs 直接 passthrough 的情境2026-XX-XX-feat-ssp-docx-v2-adapter-layer.md每個小 step 都有 RED → GREEN test。
CmmcSspAdapter 加 _extract_metadata(structure_paragraphs, tables) -> ParsedMetadata:
Ser. NO[::])CmmcSspAdapter 加 _extract_parties(structure_paragraphs, tables) -> list[ParsedParty]:
CmmcSspAdapter 加 _extract_leveraged_services(tables) -> list[ParsedLeveragedService]:
Leveraged FedRAMP 表需要新增兩個 helper(adapter 注入 dependency):
match_party_to_user(party, tenant_id) -> Optional[int]match_party_to_org_unit(party, tenant_id) -> Optional[int]放在 adapter 接收的 dependency 內,避免 adapter 直接 import infra repos。
從 Introduction 段落抓條列項目:
framework_specific_props['cmmc:fci_types']每小步一個 commit + test:
2026-XX-XX-feat-ssp-docx-v2-adapter-metadata.md2026-XX-XX-feat-ssp-docx-v2-adapter-parties.md2026-XX-XX-feat-ssp-docx-v2-adapter-leveraged.md2026-XX-XX-feat-ssp-docx-v2-adapter-info-types.mdASIA-CMMC-SSP-DRAFT-202604.docx 跑 e2e parse + adapt:
新增方法:
_write_metadata(parsed_metadata, ssp_id, user) 寫 oscal_metadatas_write_parties(parsed_parties, context_type, context_id, user) 寫 oscal_parties + responsible_parties_write_leveraged_services(parsed_services, ssp_id, user) 寫 ssp_system_implementations type='leveraged' + props_write_information_types(parsed_types, ssp_id, user) 寫 oscal_props (cmmc:fci-type)既有的 _upsert_control_default / _upsert_objective_default 不動。
對應 SSP 路徑:
oscal_metadatas (跟 SSP 連結)oscal_parties + responsible_parties (context_type='ssp')對寫入策略補測試:
tests/test_module_frame_write_strategy.py 補 parties / leveraged / metadata 寫入 casetests/test_ssp_write_strategy.py 同上app/oscal/service/ssp_docx_import_app_service.py:
confirm_import() 接受新版 payload(含 ParsedSsp 完整 patch)mode='full' + chain confirm 路徑簡化:BE 同 transaction 內:
@dataclass
class ConfirmImportPayload:
parse_uid: str
mode: Literal['create', 'update']
edits: dict # localStorage 帶過來的 edits patch
# 若 mode='create':附 framework_version_uid + module_frame_basic(name 等)
# 若 mode='update':附 target_uid (module_frame uid 或 ssp uid)
target_uid: Optional[str] = None
framework_version_uid: Optional[str] = None
module_frame_basic: Optional[dict] = None保留既有 confirm_import signature 一段時間,標記 @deprecated,待 Phase 11 移除。
2026-XX-XX-feat-ssp-docx-v2-unified-confirm.mdtest_ssp_docx_import_app_service.py 加 v2 dual-mode testssrc/config/router/index.js:
{
path: '/compliance-resource/import-docx',
name: 'compliance-resource-import-docx',
component: () => import('@/views/compliance-framework/ImportDocxPage.vue'),
props: { mode: 'create-mf' },
},
{
path: '/compliance-resource/:uid/import-docx',
name: 'compliance-resource-update-docx',
component: () => import('@/views/compliance-framework/ImportDocxPage.vue'),
props: route => ({ mode: 'update-mf', targetUid: route.params.uid }),
},
{
path: '/project/:id/ssp-import-docx',
...
props: route => ({ mode: 'update-ssp', projectId: route.params.id }),
},最小可用:
2026-XX-XX-feat-ssp-docx-v2-import-page-shell.md依序實作 5 個 SectionComponent + 1 個 dialog:
讀寫 parsed_result.metadata:4 欄 InputText + DatePicker。
5 個 role 各一卡片,狀態 ✓/⚠ 顯示,「補連結」按鈕呼叫 PartyLinkDialog。
DataTable 列出 leveraged services,每行可編輯。
繼承 v1 PreselectSummary 大部分邏輯:
直接搬 v1 PreselectSummary 內的「疑似漏掉的控制項 + 雜訊計數」邏輯。
每個 section 一個 commit。
src/composables/useSspDocxDraft.js:
export function useSspDocxDraft(parseUid) {
const KEY = `ssp_docx_draft:${parseUid}`
const draft = reactive(JSON.parse(localStorage.getItem(KEY) ?? '{}'))
watch(draft, (val) => {
localStorage.setItem(KEY, JSON.stringify(val))
}, { deep: true })
function clear() {
localStorage.removeItem(KEY)
}
return { draft, clear }
}mount 時:
parsed_resultunmount / confirm 後:
draft.clear()2026-XX-XX-feat-ssp-docx-v2-localstorage-draft.mdModuleFrameTemplateEditView.vue 加新 tab/section:
[補連結] 按鈕呼叫 PartyLinkDialog(沿用 Phase 7 的)新 endpoint:
GET /module-frame/<uid>/parties → list with link statusPATCH /oscal-party/<uid>/link → 改 user_id / org_unit_id2026-XX-XX-feat-ssp-docx-v2-party-relink-ui.mdModuleFrame.vue 工具列:
<!-- 移除這整段 -->
<div v-if="!currFrame.uid" class="mb-3 pt-3 border-top-1 surface-border">
<Button :label="t('lang.ssp_docx_import.btn_import_controls_from_docx')" ... />
</div>
ModuleFrameTemplateEditView.vue 批次維護 menu 內「從 docx 匯入」改成 router.push({name: 'compliance-resource-update-docx', params: {uid: moduleFrameUid.value}}),不再開 dialog。
ProjectPlanningView.vue 同上,跳到 SSP route。
2026-XX-XX-feat-ssp-docx-v2-replace-entry-points.mdgit rm src/components/grc/SspDocxImportDialog.vue
git rm src/components/grc/ssp-docx-import/PreselectSummary.vue
git rm src/components/grc/ssp-docx-import/StepUpload.vue
git rm src/components/grc/ssp-docx-import/StepPreview.vue
git rm src/components/grc/ssp-docx-import/StepResult.vuesrc/stores/sspDocxImport.js → 改用 v2 store 或直接刪。
app/oscal/service/ssp_docx_import_app_service.py:
mode='full' 特殊 chain logic移除 wizard / preselect 相關 i18n key。
2026-XX-XX-cleanup-ssp-docx-v1-deprecated.md# scripts/e2e_test_ssp_docx_v2.py
# 1. 上傳 docx
# 2. 驗證 parse_result 內 metadata / parties / leveraged 都抽出
# 3. confirm create → 驗證 module_frame + 所有相關 row 都建好
# 4. 進編輯頁 → 驗證 parties section 顯示正確
# 5. 補連結 → 驗證 user_id / org_unit_id 寫入中英文兩語系所有新 key。
docs/features/FR-022-2604-ssp-doc-parser/release-notes-v2.md。
整合 v2 changelog 索引。
1 ─→ 2 ─→ 3 ─→ 4 ─→ 5 ─→ 10 ─→ 11
↓ ↑
└────→ 6 ─→ 7 ─→ 8 ─┘
↓
9 ─→ 12
關鍵路徑:1 → 2 → 5 → 10 → 11,其他 phase 可平行。
每個 phase 完成後產生:
docs/changelog/最終交付:docs/features/FR-022-2604-ssp-doc-parser/release-notes-v2.md 含完整上版 SOP。