目的:讓專案 SSP 支援 Excel 完整匯出匯入 round-trip + MF template-edit 跟 SSP 結構對齊 接手者:新 session 從這份 handoff 直接開工,毋需重看舊對話 建立日期:2026-05-23(前一 session 收尾時寫) branch:
feature/ssp-import-export-phase3(沿用,不要切) 使用者:raymond
接手 docs/features/FR-011.3-2605-ssp-edit-in-project/handoff/2026-05-23-track-bac-excel-roundtrip.md
這份 handoff,照 Track B → A → C 順序開工。Track B 先動。
「從專案那邊做匯出匯入 SSP,讓 user 可以用 Excel 編輯 SSP 的部分」。
實現路徑拆 3 track(B → A → C):
| Track | 主題 | 工作量 | 為何這個順序 |
|---|---|---|---|
| B | Excel 鏈條補完(system_characteristic 全新 + leveraged 結構化端到端) | BE 大 | 地基層,沒有 Excel 端到端不通就無法 ship C |
| A | MF template-edit 跟 SSP 結構對齊(補 受評標的 + 外部利用服務 兩 tab) | BE 中 + FE 中 | 對齊 + 提供 admin 預設「樣板值」入口 |
| C | 專案 SSP tab 加匯出匯入按鈕 | FE 小 | 把 B 的能力 expose 給 end user |
| 議題 | 決策 |
|---|---|
| system_characteristic 放 MF 的語意 | 「樣板預設值」(mirror 既有 devices / info_systems 池 pattern);新建 SSP 啟動時 clone 一份過來,user 進專案後再依實際填寫;MF 那邊存的不是「實際系統描述」,是「建議的預設結構」 |
| MF / SSP tab 結構對齊目標 | 兩邊都 7 tab:受評標的 / 責任單位 / 責任人員 / 設備 / 資訊系統 / 外部利用服務 / (MF: 適用控制項 + 程序書管理 / SSP: 不重複出現) |
Excel 樣板加 01b_受評標的 新 sheet |
獨立 sheet,不塞進 metadata(避免跟 MF 框架 metadata 混淆語意) |
| MF leveraged BE endpoint | mirror SSP /ssp/<uid>/leveraged pattern → 新增 /module-frame/<uid>/leveraged;內部 thin wrapper 呼 SspLeveragedContextService(不重寫 CRUD 邏輯) |
| MF system_characteristic BE endpoint | mirror SSP /ssp/<uid>/system-characteristic pattern → 新增 /module-frame/<uid>/system-characteristic |
| FE Section component | 對齊 Phase B 4 個 panel pattern — SspBasicSection + SspLeveragedSection 加 apiBase: 'module-frame' | 'ssp' + scopeUid props |
_setup_ssp_system_implementation |
擴充 — 既有只 clone main row + items,要再加 clone system_characteristic |
檔案:app/module_frame/excel_template/sheet_definitions.py
SHEET_SYSTEM_CHARACTERISTIC(sheet_name="01b_受評標的")
system_name (required) / security_sensitivity_level (enum: low/moderate/high) / status (enum: under-development/operational/under-major-modification/disposition/other) / target_type / scope_description / authorization_boundary / owner_login_name (lookup_source=USERS — 拿 login_name,BE 端對 matched_user_id)SHEET_LEVERAGED 補 column:加 status ColumnDef(enum: operational/under-development/under-major-modification)ALL_SHEETS 在 SHEET_METADATA 後插入 SHEET_SYSTEM_CHARACTERISTIC新 dataclass:domain/oscal/parser/ssp_intermediate.py
@dataclass
class ParsedSystemCharacteristic:
system_name: str # required
security_sensitivity_level: Optional[str] = None # low/moderate/high
status: Optional[str] = None
target_type: Optional[str] = None
scope_description: Optional[str] = None
authorization_boundary: Optional[str] = None
owner_login_name: Optional[str] = None # parser 拿到的 login_name
matched_user_id: Optional[int] = None # reconciler 配對後的 user_id
match_method: MatchMethod = MatchMethod.UNMATCHED
match_confidence: float = 0.0ParsedLeveraged 補欄位:加 category: Optional[str] + status: Optional[str]
新增:ParsedExcelEntityBundle 加 parsed_system_characteristic: Optional[ParsedSystemCharacteristic] = None(per-SSP 只有一筆,不是 list)
Parser sheet handler:app/oscal/service/excel_parser/sheet_handlers.py
parse_system_characteristic_sheet(ws, errors) -> Optional[dict](單筆,不是 list)SHEET_NAMES dictParser 主幹:app/oscal/service/excel_parser/parser.py:67 區段加
system_characteristic=sh.parse_system_characteristic_sheet(
sh.get_ws(wb, sh.SHEET_NAMES["system_characteristic"]), validation_errors,
),ParsedResult (types.py) 加 system_characteristic: Optional[dict] = None
新檔:domain/oscal/service/reconciliation/system_characteristic_reconciler.py
SystemCharacteristicReconciler — 從 owner_login_name 配對 User.login_name → 填 matched_user_id。
Mirror 既有 leveraged_reconciler.py 結構(最簡 reconciler,只解一條 user lookup)。
新檔:domain/oscal/service/write_strategy/system_characteristic_write_strategy.py
SystemCharacteristicWriteStrategy — 寫 oscal.system_security_plan_characteristic(per-SSP 單筆 upsert)。
注意:跟其他 write strategy 不同(其他都是 list item 寫到 ssp_system_implementation_items),這個是寫 per-SSP 單一 row 到 SC 表。
實作 pattern:先 get_by_ssp_id(ssp_id),存在 → update,不存在 → add。entity 用 SspSystemCharacteristicEntity (jedi-oscal)。
LeveragedWriteStrategy 修正:domain/oscal/service/write_strategy/leveraged_write_strategy.py:21
# 修法(B4.2)
def _parsed_to_item_payload(self, parsed: ParsedLeveraged) -> dict:
return {
"name": parsed.service_name,
"provider": parsed.provider, # ← 改寫到 provider 欄位(之前錯寫到 title)
"category": parsed.category, # ← 新增
"status": parsed.status, # ← 新增
"purpose": parsed.purpose,
"party_uuid": parsed.matched_party_uuid,
"date_authorized": parsed.date_authorized,
}_dict_to_parsed_leveraged:app/oscal/service/ssp_excel_import_app_service.py:1935 接 category + status
檔案:app/module_frame/service/ssp_import_template_app_service.py
_build_system_characteristic(mf) -> Optional[dict]:從 MF 對應 template SSP 拉 oscal.system_security_plan_characteristic 一筆 → 組 dict(含 owner_login_name from User.get_by_id(owner_user_id).login_name)_build_leveraged(items):加 category + status + provider(改從 item.provider 取,不是 party.name — entity 已有獨立 provider 欄位)bundle.system_characteristic = self._build_system_characteristic(mf)檔案:app/oscal/service/ssp_excel_import_app_service.py
system_characteristic_write_strategy_dict_to_parsed_system_characteristic(d: dict) -> Optional[ParsedSystemCharacteristic]parsed_system_characteristicsystem_characteristic_written: boolFE 檔案:src/views/module_frame/ImportExcelPreviewPage.vue + 新 src/components/grc/ssp-excel-import/SheetPreviewSystemCharacteristic.vue
tests/test_system_characteristic_write_strategy.pytests/test_leveraged_write_strategy_provider_category.py(補 category + status + provider 對 entity 的映射)tests/test_ssp_excel_import_system_characteristic_e2e.py(end-to-end with mock)docs/changelog/YYYY-MM-DD-feat-ssp-excel-system-characteristic-and-leveraged-structured.md
檔案:
app/module_frame/service/module_frame_system_characteristic_service.py — mirror module_frame_ssp_resources_service.py pattern(mf_uid → resolve ssp_id → 呼 SspSystemCharacteristicContextService 的方法)api/module_frame/routes/module_frame_system_characteristic_route.py — GET / PUT endpointsapi/module_frame/__init__.py 加 add_resource /module-frame/<uid>/system-characteristicdi_containers/module_frame/module_frame_container.py 加 provider注意:
SspSystemCharacteristicContextService可能尚未抽出 (need to check)。若沒有就先抽(mirror SspResourcesContextService 抽法)。
檔案:
app/module_frame/service/module_frame_leveraged_service.py — mirror MF resources patternapi/module_frame/routes/module_frame_leveraged_route.py — GET / POST / PUT / DELETEapi/module_frame/__init__.py 加 /module-frame/<uid>/leveragedensure_sys_impl_main_id(前一輪做的 auto-create, 4595dae)— MF 對應 SSP 不一定有 main row檔案:src/components/grc/ssp/SspBasicSection.vue
加 props(mirror Phase B 4 個 panel):
apiBase: { type: String, default: 'ssp', validator: v => ['module-frame', 'ssp'].includes(v) }
scopeUid: { type: String, required: true } // 取代既有 sspUidapiRoot computed → ${base}/${scopeUid}/system-characteristic
檔案:src/components/grc/ssp/SspLeveragedSection.vue
同 A3 加 apiBase + scopeUid props,endpoint 走 apiRoot/leveraged/...
檔案:src/views/module_frame/ModuleFrameTemplateEditView.vue
加 2 個 TabPanel:
tab_audit_subject(受評標的)→ mount <SspBasicSection api-base="module-frame" :scope-uid="moduleFrameUid" />tab_leveraged(外部利用服務)→ mount <SspLeveragedSection api-base="module-frame" :scope-uid="moduleFrameUid" :on-count="(n) => leveragedCount = n" />放位置:放在「資訊系統」後、「程序書管理」前(mirror SSP tab 順序)
檔案:
src/config/locales/i18n/zh-tw/module-frame.json + en/tab_audit_subject / tab_leveraged (含 count)檔案:app/project/service/oscal_project_service.py:533 _setup_ssp_system_implementation
擴充加 system_characteristic clone:
# Step 1.5 — clone system_characteristic from template SSP
template_sc = self._sc_domain_service.get_by_ssp_id(template_ssp.id)
if template_sc:
new_sc = SspSystemCharacteristicEntity(
ssp_id=new_ssp_id,
name=template_sc.name,
# ... 所有欄位 pass-through
)
self._sc_domain_service.add(new_sc)DI 補注 system_characteristic_domain_service。
docs/changelog/YYYY-MM-DD-feat-mf-template-edit-add-audit-subject-and-leveraged-tabs.md
檔案:src/components/grc/ssp/SspTabPanel.vue
在 SSP tab 頂部加兩個按鈕(manager only):
GET /ssp/<uid>/excel-template(既有 C2.2 endpoint, 待確認)SspExcelImportAppService flow注意:先確認
/ssp/<uid>/excel-template跟/ssp/<uid>/excel-import是否已在 C2.2 ship。沒 ship 要先補 BE endpoint。
src/views/module_frame/ImportExcelPreviewPage.vue 抽 ImportExcelDialog 元件給 SSP tab 用,或新建一個 SspImportExcelDialog wrap 既有預覽元件。
YYYY-MM-DD-feat-project-ssp-excel-roundtrip-entry.md| Commit | 主題 |
|---|---|
0960070 |
C3.PR2 DROP table |
143f354 |
C3.PR2 拆 Python stack |
4595dae |
auto-create system_implementation main row(Track A2 會用到 ensure_sys_impl_main_id) |
4fe8595 (FE) |
MF parties picker cache 統一 |
d9a4128 |
C3.PR2 + auto-create changelog + tracker |
ebd4f72 |
spec sync (C3/C5/C7/plan-C3/api-spec) |
8d80972 (FE) |
Phase C/D batch changelog |
f1c60a0 (FE) |
Phase E cleanup |
18a1110 |
leveraged 結構化欄位 BE 端到端(provider/category 既有實作,但 Excel chain 沒接,這次補) |
8de7991 (jedi-oscal) |
leveraged entity 加 provider + category(不要再動套件) |
docs/features/FR-011.3-2605-ssp-edit-in-project/design-C3.md §11 — C3.PR2 已 ship 紀錄docs/features/FR-011.3-2605-ssp-edit-in-project/design-C5.md §10 — Phase D Reconciliation 全紀錄docs/features/FR-011.3-2605-ssp-edit-in-project/design-C7.md §10 — leveraged 結構化 Phase C Reconciliationdocs/features/FR-011.3-2605-ssp-edit-in-project/implementation-plan-C3.md T11 — PR2 plan 矯正紀錄docs/api/project/api-spec.md §2.2 — project-device endpoint 撤除紀錄docs/features/FR-011.3-2605-ssp-edit-in-project/handoff/2026-05-23-phase-c-d-SUMMARY.md
docs/features/FR-011.3-2605-ssp-edit-in-project/README.md — 含 Phase C 全 8 子題 + Phase D / B-generalize / Settings / Issue 1 BE / Bug fix / E cleanup / Auto-create / MF picker cache 共 14 row
| 規範 | 說明 |
|---|---|
| 禁止切 branch | git checkout/switch 一律不執行。current branch = feature/ssp-import-export-phase3,發現不對停下問 user,不自己 fix |
| commit 可自做 | 階段性 commit 直接做不問;push 永遠等 user 指示 |
| 不可寫憑證進版控 | 密碼 / token / secret 絕對不寫進任何 commit 檔案。連線資訊只寫 host / port / db / 帳號,密碼一律「請查 .env」 |
| 改 jedi- 套件先走 path dep* | dev 階段把 pyproject.toml 的套件改 path 形式;不每改必 bump version;feature 完成 + user 指示才正式發版推 Nexus |
pyproject.toml 不要 commit dev 改動 |
目前 pyproject.toml working tree 有 M(jedi-oscal path dep),等 jedi-oscal 正式發版才一起還原 |
| 規範 | 說明 |
|---|---|
| changelog 收尾才 batch 寫 | 不要每個 commit 都停下寫;commit message 寫詳細即可;user 說「收尾」/「總結」/「告一段落」/「summary」/ phase 自然結束才一次性寫 |
| subagent 平行修改也要 changelog | 即使透過 subagent 批次修改,每個獨立主題都要有對應 changelog |
| 變更後主動 sync spec | feature 完工要回頭 update design-*.md 的 Reconciliation 段(mirror design-C5/C7 §10 pattern) |
| DDD 嚴格分層 | api 不直接 import ORM model 或 get_session;app service @transaction 開 scope;repo session lazy property |
@transaction 必加 |
每個 app service public method 都要 @transaction;helper(caller 已在 scope)不重複加但 docstring 標示「caller 必須在 @transaction scope 內」 |
| Error code 用 GrcErrorCode + 標準 exception | 禁 raise ValueError;新 code 命名 GRC_<HTTP><序號> |
| 可翻譯欄位 update 必傳 locale | HTTP 用 str(get_locale());background job 用 getattr(user_context, "locale", None) or "zh_Hant_TW" |
| subagent dispatch prompt 必加 git add 顯式檔名 | 禁用 git add -am(會 sweep 不相關修改) |
| 項目 | 值 |
|---|---|
| Dev DB | 192.168.50.188:25432 / guidant_ai_dev(host + port + db 名一組記住,不是 localhost, 漏 host 或 -p 25432 會 connection refused 像 DB 當機) |
| Migration 跑誰 | cmmgr 帳號(密碼 jedi@123!)— cm_app 受 RLS 擋 |
| BE log 位置 | log/app.log(專案根目錄,不要靠 lsof 找 stdout) |
| Dev 登入帳號 | blsadmin / Billows@123!(manual)或 blsit / Billows@123!(pytest) |
| BE 改 service 後 | 必提醒 user 重啟 BE(沒 hot reload) |
_setup_ssp_system_implementation 擴充 clone SC(提前做給後續測試用)_setup_ssp_system_implementation 擴充 SC clone 後,要重跑 dev 既有測試 SSP 啟動流程,確認 main row + items + SC 都 clone 過去(dev DB 連 192.168.50.188:25432 / guidant_ai_dev)/ssp/<uid>/excel-template + /ssp/<uid>/excel-import endpoint 是否在 C2.2 已 ship — 如果沒 ship 要先補 BE endpointSspLeveragedContextService 已用獨立 provider 欄位,但 write strategy 還是舊路徑)— 兩邊要對齊SHEET_MISSING non-blocking 處理 — 看 parse_leveraged_sheet 範例)Track B / A / C 各 ship 後:
docs/features/FR-011.3-2605-ssp-edit-in-project/handoff/YYYY-MM-DD-track-bac-SUMMARY.mdpush 永遠等 user 指示,handoff 跑完不自動 push。