Track B → A → C — SSP Excel round-trip + MF / SSP 結構對齊 — 交接 prompt

目的:讓專案 SSP 支援 Excel 完整匯出匯入 round-trip + MF template-edit 跟 SSP 結構對齊 接手者:新 session 從這份 handoff 直接開工,毋需重看舊對話 建立日期:2026-05-23(前一 session 收尾時寫) branchfeature/ssp-import-export-phase3(沿用,不要切使用者:raymond


0. 第一句 prompt 建議(給接手 session 用)

接手 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 先動。

1. User 真正目標

從專案那邊做匯出匯入 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

2. 已拍板的設計決策(不要再問

議題 決策
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 + SspLeveragedSectionapiBase: 'module-frame' | 'ssp' + scopeUid props
_setup_ssp_system_implementation 擴充 — 既有只 clone main row + items,要再加 clone system_characteristic

3. Track B 詳細 scope

B1 — Excel sheet 定義擴充

檔案app/module_frame/excel_template/sheet_definitions.py

  • 新 sheet 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_SHEETSSHEET_METADATA 後插入 SHEET_SYSTEM_CHARACTERISTIC
  • 樣板版本 v2.1.0 → v2.2.0(加 sheet 屬 MINOR bump, 對齊 design.md §14 SemVer)

B2 — Parser + dataclass

新 dataclassdomain/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.0

ParsedLeveraged 補欄位:加 category: Optional[str] + status: Optional[str]

新增ParsedExcelEntityBundleparsed_system_characteristic: Optional[ParsedSystemCharacteristic] = None(per-SSP 只有一筆,不是 list)

Parser sheet handlerapp/oscal/service/excel_parser/sheet_handlers.py

  • parse_system_characteristic_sheet(ws, errors) -> Optional[dict](單筆,不是 list)
  • 加進 SHEET_NAMES dict

Parser 主幹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

B3 — Reconciler

新檔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)。

B4 — Write strategy

新檔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_leveragedapp/oscal/service/ssp_excel_import_app_service.py:1935 接 category + status

B5 — 樣板下載填值

檔案app/module_frame/service/ssp_import_template_app_service.py

  • 新 method _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 欄位)
  • 主 builder 加 bundle.system_characteristic = self._build_system_characteristic(mf)

B6 — Import write 流程整合

檔案app/oscal/service/ssp_excel_import_app_service.py

  • DI 加 system_characteristic_write_strategy
  • _dict_to_parsed_system_characteristic(d: dict) -> Optional[ParsedSystemCharacteristic]
  • bundle 帶入 parsed_system_characteristic
  • reconciler 跑 user lookup
  • write strategy 寫
  • response 加 system_characteristic_written: bool

B7 — Excel preview / FE 預覽頁

FE 檔案src/views/module_frame/ImportExcelPreviewPage.vue + 新 src/components/grc/ssp-excel-import/SheetPreviewSystemCharacteristic.vue

  • 加 system_characteristic preview tab(單筆顯示,跟其他多筆 sheet preview 不同)
  • 加 leveraged 預覽 category/status 欄位顯示

B8 — Test

  • tests/test_system_characteristic_write_strategy.py
  • tests/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)

B9 — Changelog(Track B ship 後)

docs/changelog/YYYY-MM-DD-feat-ssp-excel-system-characteristic-and-leveraged-structured.md


4. Track A 詳細 scope

A1 — BE MF system-characteristic 端到端

檔案

  • 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 endpoints
  • api/module_frame/__init__.py 加 add_resource /module-frame/<uid>/system-characteristic
  • di_containers/module_frame/module_frame_container.py 加 provider

注意SspSystemCharacteristicContextService 可能尚未抽出 (need to check)。若沒有就先抽(mirror SspResourcesContextService 抽法)。

A2 — BE MF leveraged 端到端

檔案

  • app/module_frame/service/module_frame_leveraged_service.py — mirror MF resources pattern
  • api/module_frame/routes/module_frame_leveraged_route.py — GET / POST / PUT / DELETE
  • api/module_frame/__init__.py/module-frame/<uid>/leveraged
  • DI 加 provider
  • ensure_sys_impl_main_id(前一輪做的 auto-create, 4595dae)— MF 對應 SSP 不一定有 main row

A3 — FE SspBasicSection generalize

檔案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 }  // 取代既有 sspUid

apiRoot computed → ${base}/${scopeUid}/system-characteristic

A4 — FE SspLeveragedSection generalize

檔案src/components/grc/ssp/SspLeveragedSection.vue

同 A3 加 apiBase + scopeUid props,endpoint 走 apiRoot/leveraged/...

A5 — MF template-edit 加 2 個 tab

檔案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 順序)

A6 — i18n

檔案

  • src/config/locales/i18n/zh-tw/module-frame.json + en/
  • tab_audit_subject / tab_leveraged (含 count)
  • ssp-edit.json 已有的 i18n key 不變(複用)

A7 — 啟動專案 clone system_characteristic

檔案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

A8 — Changelog(Track A ship 後)

docs/changelog/YYYY-MM-DD-feat-mf-template-edit-add-audit-subject-and-leveraged-tabs.md

A9 — Spec sync

  • design-C5.md §10 加段落:「對齊 MF template-edit」
  • design-C7.md §10 加段落:「MF endpoint + UI tab」
  • README.md tracker 加 row:「Track A: MF / SSP 結構對齊」

5. Track C 詳細 scope

C1 — 專案 SSP tab 加匯出匯入按鈕

檔案src/components/grc/ssp/SspTabPanel.vue

在 SSP tab 頂部加兩個按鈕(manager only):

  • 「下載 SSP Excel」→ 呼 GET /ssp/<uid>/excel-template(既有 C2.2 endpoint, 待確認
  • 「匯入 SSP Excel」→ open import dialog → 走既有 SspExcelImportAppService flow

注意:先確認 /ssp/<uid>/excel-template/ssp/<uid>/excel-import 是否已在 C2.2 ship。沒 ship 要先補 BE endpoint。

C2 — Import dialog 復用

src/views/module_frame/ImportExcelPreviewPage.vue 抽 ImportExcelDialog 元件給 SSP tab 用,或新建一個 SspImportExcelDialog wrap 既有預覽元件。

C3 — Changelog + Spec sync

  • changelog: YYYY-MM-DD-feat-project-ssp-excel-roundtrip-entry.md
  • design-C5.md §10 加 Track C 段落

6. 既有 task arc 錨點(接手前先了解)

既有 commits range(不需動,僅供參考)

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(不要再動套件

已 sync 的 spec / plan

  • 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 Reconciliation
  • docs/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 撤除紀錄

SUMMARY 路徑

docs/features/FR-011.3-2605-ssp-edit-in-project/handoff/2026-05-23-phase-c-d-SUMMARY.md

Tracker

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


7. 規範速查(接手 session 必看

操作規範

規範 說明
禁止切 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)

8. 工作流提案

Track B 開工順序

  1. B1 Excel sheet 定義(含 SHEET_SYSTEM_CHARACTERISTIC + SHEET_LEVERAGED status column + ALL_SHEETS 加入 + 樣板 v2.2.0 bump)
  2. B2 parser + dataclass(ParsedSystemCharacteristic + ParsedLeveraged.category/status + parser sheet handler)
  3. B3 reconciler(user lookup)
  4. B4 write strategy(new + 修 leveraged)
  5. B5 樣板下載填值(_build_system_characteristic + 修 _build_leveraged)
  6. B6 import 流程整合(DI + dict mapper + bundle)
  7. B7 FE preview 頁
  8. B8 tests
  9. B9 ship + changelog

Track A 開工順序

  1. A1 BE MF system-characteristic service + route + DI
  2. A2 BE MF leveraged service + route + DI
  3. A7 _setup_ssp_system_implementation 擴充 clone SC(提前做給後續測試用)
  4. A3 + A4 FE generalize(SspBasicSection + SspLeveragedSection apiBase props)
  5. A5 MF view 加 2 tab + count emit
  6. A6 i18n
  7. A8 ship + changelog
  8. A9 spec sync

Track C 開工順序

  1. C1 + C2 FE 入口按鈕 + dialog
  2. C3 changelog + spec sync

9. 已知潛在地雷(接手前先看)

  1. SspSystemCharacteristicContextService 可能尚未抽出(C2.1 抽 SspResourcesContextService 時順手抽了 leveraged + resources 兩個,SC 沒抽)— Track A1 第一步 grep 確認,沒抽就先抽 mirror SspResourcesContextService 的 pattern
  2. _setup_ssp_system_implementation 擴充 SC clone 後,要重跑 dev 既有測試 SSP 啟動流程,確認 main row + items + SC 都 clone 過去(dev DB 連 192.168.50.188:25432 / guidant_ai_dev
  3. Track C 需確認 /ssp/<uid>/excel-template + /ssp/<uid>/excel-import endpoint 是否在 C2.2 已 ship — 如果沒 ship 要先補 BE endpoint
  4. leveraged_write_strategy 修 provider 映射時,要同時清掉「title=provider」的舊 hack(既有 SSP UI CRUD SspLeveragedContextService 已用獨立 provider 欄位,但 write strategy 還是舊路徑)— 兩邊要對齊
  5. 樣板版本 v2.2.0 bump 後,舊 Excel template (v2.1.0 / v2.0.0) 匯入時 parser 對缺 sheet 要容錯(mirror SHEET_MISSING non-blocking 處理 — 看 parse_leveraged_sheet 範例)

10. 收尾流程

Track B / A / C 各 ship 後

  1. Commit 用對應 type(feat / fix / tweak)依決策樹判斷
  2. 等 user 說「收尾」/「summary」才 batch 寫 changelog
  3. spec sync(design-C5/C7 加 Reconciliation 段)
  4. tracker README.md 加 row
  5. 三 track 全 ship 後寫 final SUMMARY 到 docs/features/FR-011.3-2605-ssp-edit-in-project/handoff/YYYY-MM-DD-track-bac-SUMMARY.md

push 永遠等 user 指示,handoff 跑完不自動 push。