Phase:A1(Track A:Excel 匯入第一階段) 級別:中型(中型 phase — design 段落 + plan,跳過 brainstorm) 狀態:design draft 前置:A0.1 已 ship(
ssp_system_implementations三表結構穩定) 依賴本 phase:A2(Excel Parser)必須拿 A1 樣板對應;A5(預覽 UI Confirm)參考 A1 sheet 結構
Phase 2 目標是把「合規資源庫」9 大資料領域(metadata / 單位 / 人員 / 設備 / 資訊系統 / leveraged / 控制項 / AO / 程序書)打通 Excel 匯入匯出。A1 是入口:
A1 範圍:產出可下載的 Excel 樣板(blank / filled 兩種),不包含上傳 / parse / 匯入回 MF。
mode=blank|filled 切換| 不在 A1 的事 | 哪 phase 處理 |
|---|---|
| Excel 上傳 / 解析 / parse_uid TTL | A2 |
| parties / org-units 鉤稽 matcher 抽共用層 | A3 |
| devices / information_systems / leveraged / controls / AOs 鉤稽 | A4 |
| 預覽 UI + Confirm 寫入 | A5 |
| Excel 匯回 docx parser 對齊 | Phase 2 完工後「統整優化清單」 |
| 控制項 mandatory flag 機制(系統無此概念,B 模式不依賴) | 不做 |
GET /api/1.0/module-frame/<module_frame_uid>/ssp-import-template?mode=blank|filled
| Field | Type | Required | Default | 說明 |
|---|---|---|---|---|
module_frame_uid |
path param, UUID | ✅ | — | 對應 module_frames.uid,blank mode 仍需此 uid 取 profile 範圍 |
mode |
query, enum(blank/filled) |
❌ | blank |
blank:純結構 + 下拉資料源 / filled:MF 既有資料倒進對應 sheet |
locale |
query, enum(zh_Hant_TW/en) |
❌ | user context | 樣板 header 文字語系 |
Response:
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,Content-Disposition: attachment; filename=ssp_template_<mf_short_name>_<mode>_<yyyymmdd>.xlsxGRC_MODULE_FRAME_NOT_FOUND(沿用既有)GRC_FORBIDDENmodule_frames 現有 tenant_id 過濾,自動隔離為什麼下拉資料源也要走 tenant scope:避免使用者下載樣板時看到別 tenant 的 user / device。RLS 已保證。
設計動機 — A1 原始設計只覆蓋 S2/S3 場景(MF 已存在後下載 blank/filled)。但顧問實務有 S1 訪談前場景:MF 尚未建立、要先依 framework_version 下載「全 catalog controls 樣板」帶去訪談,回來後再透過 A2 phase 上傳建 MF + profile。
GET /api/1.0/ssp-import-template?framework_version_uid=<uid>&mode=blank&locale=<locale>
| Field | Type | Required | Default | 說明 |
|---|---|---|---|---|
framework_version_uid |
query, UUID | ✅ | — | 對應 oscal_framework_versions.uid |
mode |
query, enum(blank) |
❌ | blank |
強制 blank(無 MF 沒 filled 概念) |
locale |
query | ❌ | user context | 樣板 header 文字語系 |
Response:
Content-Disposition: attachment; filename=ssp_template_<fw>_<version>_blank_<yyyymmdd>.xlsxframework_version_uid 缺 → GRC_TEMPLATE_FRAMEWORK_VERSION_REQUIREDGRC_TEMPLATE_INVALID_MODEGRC_FRAMEWORK_VERSION_NOT_FOUND控制項清單來源差異:
| Endpoint | 07_控制項與AO sheet source | 範圍 |
|---|---|---|
| §3.1 MF-scoped | mf.oscal_profile.profile_controls → catalog |
subset(profile 已篩過) |
| §3.3 FW-version-scoped | framework_version.catalog → 全 catalog_groups → controls + AOs | superset(未篩 profile) |
Lookup 差異:
| Lookup | §3.1 MF-scoped | §3.3 FW-version-scoped |
|---|---|---|
| USERS / ORG_UNITS / DEVICES / INFO_SYSTEMS / ORGS | tenant RLS auto filter | 同左 |
| REF_DOCS_MF | module_frame_reference_documents(MF own) |
空 list(無 MF context) |
A2 phase 對應計畫:訪談後上傳 Excel → parser 識別 superset → user 在 Excel 內標記「不適用 / 納入 profile」→ 系統依此產生 subset profile + 建 MF。A2 spec 寫此設計時要明確區分「Excel superset upload」vs「MF update upload」兩條 import flow。
00_ ~ 08_)保證 Excel 開啟時 tab 順序穩定_lookup_ 前綴,openpyxl 設 sheet_state="hidden"PatternFill(fgColor="FFEB9C") 黃底(沿用既有 module_frame_import_template.xlsx 慣例)00_說明| Row 範圍 | 內容 |
|---|---|
| R1 | 樣板版本 v1.0.0、產生時間、locale |
| R2 | MF 基本資訊(name / framework / framework_version / profile UUID) |
| R3-R5 | 填寫指引:必填顏色圖例、下拉選單使用、無對應資料時 fallback 為純文字 |
| R6-R10 | 8 個資料 sheet 用途簡介 |
純說明用 sheet,A2 parser 完全忽略此 sheet。
01_基本資料(Module Frame Metadata)對應 compliance.module_frames(合規資源庫表本體)
| Header | 必填 | enum/下拉 | 來源 |
|---|---|---|---|
| 名稱 (name) | ✅ | — | module_frames.name |
| 群組 (group) | ✅ | — | module_frames.group |
| 版本 (version) | ✅ | — | module_frames.version |
| 描述 (description) | — | — | module_frames.description 長文字 |
| Framework 版本 (framework_version_uid) | — | — | module_frames.oscal_framework_version_uid (UUID, 唯讀識別) |
| Profile (profile_uid) | — | — | module_frames.oscal_profile_uid (UUID, 唯讀識別) |
| 頻率 (frequency) | ✅ | — | module_frames.frequency |
| 提供者 (provider) | — | — | module_frames.provider |
filled mode:直接從
module_frames取對應 MF 一筆 row 預填;blank mode 留 1 row 空白等使用者填。設計取捨(T0.1 verify 結論):MF 表沒有 OSCAL system-characteristics 的 FIPS 199 / authorization_boundary / deployment_model 等欄位 — 那些是 SSP-scoped concept。A1 第一版 01 sheet 對齊 MF 自有欄位即可,OSCAL 完整 system metadata 由 SSP 創建階段 UI 處理,不擠在 Excel 樣板。詳見 §14.5「寧多勿少」也是建議刪掉這些不存在的欄位,避免使用者誤以為可以從 Excel 設定。
02_單位(Parties Type = Organization)| Header | 必填 | enum/下拉 | 來源 |
|---|---|---|---|
| 單位名稱 (name) | ✅ | — | 純文字 |
| 簡稱 (short_name) | — | — | 純文字 |
| 聯絡 email | — | — | 純文字 |
| 上級單位 (parent_org) | — | tenant 全部 org_units | named range |
| 角色 (role) | — | system_owner / authorizing_official / ... | enum |
filled:取 MF 既有
oscal_parties(type=organization) +oscal_responsible_parties(context_type='module_frame', context_id=mf.id) 預填
03_參與人員(Parties Type = Person)| Header | 必填 | enum/下拉 | 來源 |
|---|---|---|---|
| ✅ | — | 純文字 (parser 用 email 鉤 user) | |
| 姓名 (name) | ✅ | — | 純文字 |
| 暱稱 (nickname) | — | — | 純文字 |
| 對應系統使用者 (matched_user) | — | tenant 全部 users | named range → _lookup_users |
| 所屬單位 (org_unit) | — | tenant 全部 org_units | named range |
| 角色 (role) | — | system_owner / authorizing_official / poc / ... | enum |
filled:取 MF 既有
oscal_parties(type=person) + responsible_parties
04_設備(Devices)| Header | 必填 | enum/下拉 | 來源 |
|---|---|---|---|
| 設備名稱 (name) | ✅ | — | 純文字 |
| IP | — | — | 純文字 |
| OS | — | — | 純文字 |
| 設備類型 (device_type) | — | server / network / endpoint / iot / ... | enum |
| 對應系統設備 (matched_device) | — | tenant 全部 devices | named range → _lookup_devices |
| 狀態 (status) | — | operational / under-development / disposition | enum (OSCAL status) |
| 用途 (purpose) | — | — | 純文字 |
filled:取 MF 對應的
ssp_system_implementation_items(scope_type='module_frame', implementation_type='hardware')。A0.1 已建好結構,A1 透過system_implementation_main鉤回 items。
05_資訊系統(Information Systems / OSCAL Components)| Header | 必填 | enum/下拉 | 來源 |
|---|---|---|---|
| 系統名稱 (name) | ✅ | — | 純文字 |
| 簡稱 (abbreviation) | — | — | 純文字 |
| 說明 (description) | — | — | 長文字 |
| 對應系統資訊系統 (matched_info_system) | — | tenant 全部 information_systems | named range → _lookup_info_systems |
| 系統類型 (component_type) | — | system / subsystem / service / software / component | enum |
| 狀態 (status) | — | operational / under-development / disposition | enum |
| 系統主 (system_owner) | — | tenant 全部 users | named range |
filled:取 MF 對應
ssp_system_implementation_items(scope_type='module_frame', implementation_type IN ('component','system','subsystem','service','software'))
06_外部利用服務(Leveraged Authorizations)| Header | 必填 | enum/下拉 | 來源 |
|---|---|---|---|
| 服務名稱 (service_name) | ✅ | — | 純文字 |
| 提供者 (provider) | ✅ | — | 純文字 |
| 授權方 (party) | — | tenant 全部 organization parties | named range → _lookup_orgs |
| 授權日期 (date_authorized) | — | — | date |
| 用途 (purpose) | — | — | 長文字 |
filled:取 MF 對應
ssp_system_implementation_items(implementation_type='leveraged-authorization')
07_控制項與AO(Control + Objective Implementations)核心決策(拍板 B + profile-scoped 範圍):以 MF 的 profile 範圍列出該 profile 內全部 control(不是整個 catalog 也不是只填過的),每 control 對應其下 AO 各一 row。
| Header | 必填 | enum/下拉 | 來源 |
|---|---|---|---|
| statement_id | ✅ | — | 既有欄位(pk 用,AO 用 uid,control 留空) |
| control_id | ✅ | — | 預填 control number(如 AC-1) |
| 控制項名稱 (control_name) | — | — | 預填 control title (i18n) |
| AO 編號 (objective_id) | — | — | AO row 才有 |
| AO 名稱 (objective_name) | — | — | AO row 才有 |
| 實作狀態 (impl_status) | — | implemented / partial / planned / alternative / not_applicable | enum |
| 現況描述 (statement) | — | — | 長文字(filled 預填 MF 既有 implementation_statement,未填的留空) |
| 參考程序書 (reference_doc) | — | MF own reference docs | named range → _lookup_ref_docs_mf |
filled 預填邏輯(拍板 B):
- 從 MF.oscal_profile_uid → profile → 該 profile 的 controls list(透過 jedi-oscal
ProfileService/CatalogService)- 每 control 一個父 row,control 下的每個 AO 一個子 row(用空白縮排或父 row control_id 留空的方式區分)
- 已有
module_frame_control_default.implementation_statement的 control row 預填 statement / impl_status;未填的留空但保留 control_id- AO 同邏輯(
module_frame_control_objective_default.statement)
filled 預期 row 數:對齊 profile 的 control 數量(CMMC L1 約 17 個 control + 各 control 下 3-5 個 AO ≈ 70-100 row;NIST 800-53 動輒 200+ 則接近 300-500 row)。Excel 可處理。
08_程序書(Reference Document Pool)| Header | 必填 | enum/下拉 | 來源 |
|---|---|---|---|
| 文件名稱 (doc_name) | ✅ | — | 純文字 |
| 文件編號 (doc_no) | — | — | 純文字 |
| 文件類型 (doc_type) | — | policy / sop / form / ... | enum(若有定義) |
| 版本 (version) | — | — | 純文字 |
| 說明 (description) | — | — | 長文字 |
filled:取 MF 既有
module_frame_reference_document(已 verify 存在,infra/module_frame/models/module_frame_reference_document.py)
from openpyxl.worksheet.datavalidation import DataValidation
# enum 直接列舉(< 255 字元)
dv_status = DataValidation(
type="list",
formula1='"implemented,partial,planned,alternative,not_applicable"',
allow_blank=True,
)
ws.add_data_validation(dv_status)
dv_status.add(f"F2:F{max_row}") # 對應「實作狀態」欄
# 大量資料用 named range(避開 255 字元上限)
dv_users = DataValidation(type="list", formula1="=_lookup_users", allow_blank=True)
ws.add_data_validation(dv_users)
dv_users.add(f"D2:D{max_row}")ws_lookup = wb.create_sheet("_lookup_users")
ws_lookup.sheet_state = "hidden" # Excel 視為隱藏 sheet
ws_lookup["A1"] = "user_dropdown" # column header
for i, user in enumerate(users, start=2):
ws_lookup.cell(row=i, column=1, value=f"{user.nickname} <{user.email}>")
# 建 named range(注意:openpyxl 用 DefinedName)
from openpyxl.workbook.defined_name import DefinedName
wb.defined_names["_lookup_users"] = DefinedName(
name="_lookup_users",
attr_text=f"_lookup_users!$A$2:$A${len(users)+1}"
)6 個 lookup sheet:
_lookup_users/_lookup_org_units/_lookup_devices/_lookup_info_systems/_lookup_orgs/_lookup_ref_docs_mf前 5 個是 tenant scope;
_lookup_ref_docs_mf是 MF own(compliance.module_frame_reference_documentsCASCADE FK to MF,沒 tenant-wide pool — T0.4 verify 結果)。
from openpyxl.styles import PatternFill
HEADER_REQUIRED_FILL = PatternFill(start_color="FFEB9C", end_color="FFEB9C", fill_type="solid")
# 每 sheet 第一 row 為 header,依「必填」標記黃底
for col_idx, col_def in enumerate(sheet_def.columns, start=1):
cell = ws.cell(row=1, column=col_idx, value=col_def.header_i18n[locale])
if col_def.required:
cell.fill = HEADER_REQUIRED_FILLwrite_only mode 或一次 ws.append(row),避免 cell-by-cellExcelTemplateAppService.generate_filled(module_frame_uid, locale, curr_user)
│
│ @transaction
▼
1. 取 MF(domain_service.get_one + tenant check)
2. 取 profile / framework_version (透過 oscal_profile_uid)
3. 並行取:
- parties (oscal_parties + responsible_parties for MF context)
- devices (ssp_system_implementation_items scope=module_frame, type=hardware)
- info_systems (同上, type IN component family)
- leveraged (同上, type=leveraged-authorization)
- control_defaults (module_frame_control_default by mf_id)
- ao_defaults (module_frame_control_objective_default by mf_id)
- ref_docs (module_frame_reference_document by mf_id)
4. profile-scoped control list (jedi-oscal ProfileService / CatalogService)
5. 取下拉資料源(tenant scope):
- users (jedi_auth user repo, status > -2)
- org_units (org_unit repo)
- devices (public.devices)
- info_systems (compliance.information_systems)
- reference docs (compliance.reference_documents 若有 tenant pool;或 fallback 用 MF own)
6. 呼叫 ExcelTemplateGenerator.generate(data_bundle, locale) → BytesIO
7. return BytesIO + filename
走同一個 generator path,但跳過 step 3-4(不撈 MF 既有資料),只保留 step 5(下拉資料源仍需 tenant scope)。
走 SspImportTemplateAppService.generate_by_framework_version(framework_version_uid, locale) 路徑:
1. fw_version = oscal_framework_version_domain_service.get_by_uid(uid)
→ None or no catalog → raise NotFound(GRC_FRAMEWORK_VERSION_NOT_FOUND)
2. catalog = fw_version.catalog(eager-loaded)
3. lookups via _fetch_lookups_without_mf()(REF_DOCS_MF 強制 [])
4. controls_with_aos via _build_controls_with_aos_from_catalog(catalog.id):
for g in catalog_group_domain_service.get_all(catalog_id=catalog.id):
for c in catalog_control_domain_service.get_all(group_id=g.id):
父 row (statement_id=None)
for ao in catalog_control_assessment_domain_service.get_all(catalog_control_id=c.id):
子 row (statement_id=str(ao.uid))
5. 父子 row 全 impl_status/statement=None(superset 無 MF defaults)
6. generator.generate(bundle) → bytes
7. filename via _build_filename_by_framework_version(fw_name, version, 'blank')
控制項去重:同 control_id 跨多 group 出現只保留第一次(seen_control_ids set)。
| Sheet | 來源 schema.table |
|---|---|
| 01_基本資料 | compliance.module_frames(MF 自有欄位 — T0.1 verify 結論,不走 system_characteristic) |
| 02_單位 / 03_人員 | oscal.oscal_parties + oscal.oscal_responsible_parties (context_type='module_frame') |
| 04_設備 / 05_資訊系統 / 06_外部利用服務 | oscal.ssp_system_implementation_items (scope_type='module_frame', scope_id=mf.id) + ssp_system_implementations main |
| 07_控制項與AO | compliance.module_frame_control_default + compliance.module_frame_control_objective_default + jedi-oscal profile→catalog controls |
| 08_程序書 | compliance.module_frame_reference_document |
要 verify 的事:基本資料對 MF 的鉤稽。A0.1 design §2.1 提到
system_characteristics是 SSP-scoped;MF 是否有對應 system_characteristics?或 MF 自己有獨立 metadata 欄位?這項需在 implementation plan 的 Phase 0 step 補確認。
| Layer | 檔案 | 職責 |
|---|---|---|
| API | api/module_frame/excel_template_router.py |
HTTP route,呼叫 app service,回 file stream |
| App Service | app/module_frame/excel_template_app_service.py |
@transaction orchestration,跨 domain service 取資料,呼叫 generator |
| Generator | app/module_frame/excel_template_generator.py |
純 openpyxl 邏輯,不碰 DB,input data bundle → BytesIO |
| Domain | (沿用既有)domain/module_frame/services/ 等 |
既有 module_frame / oscal domain service |
| Infra | (沿用既有)— |
Generator 拆成獨立檔,testable without DB — 給 data bundle dict / dataclass,直接驗 BytesIO 內容。
# di_containers/module_frame/module_frame_containers.py 加:
excel_template_app_service = providers.Factory(
ExcelTemplateAppService,
module_frame_domain_service=module_frame_domain_service,
party_domain_service=party_domain_service,
system_implementation_main_domain_service=...,
system_implementation_item_domain_service=...,
control_default_domain_service=control_default_domain_service,
ao_default_domain_service=ao_default_domain_service,
ref_doc_domain_service=ref_doc_domain_service,
user_domain_service=user_domain_service, # tenant scope lookup
org_unit_domain_service=org_unit_domain_service,
device_domain_service=device_domain_service,
info_system_domain_service=info_system_domain_service,
profile_domain_service=profile_domain_service,
catalog_domain_service=catalog_domain_service,
)Header 文字走 babel _() 翻譯函式 + zh_Hant_TW / en .po 檔。lookup 值(user nickname / device name)不翻譯,原樣輸出。
| 元件 | 位置 | 改動 |
|---|---|---|
| MF 詳細頁工具列 | compliance-manager-fe/src/views/.../ModuleFrameDetail.vue(待 verify 路徑) |
加「下載樣板」按鈕 + 下拉(blank / filled) |
| API service | src/service/.../ModuleFrameService.js |
downloadTemplate(uid, mode) 包 axios responseType: 'blob' |
| i18n | src/locales/zh-TW.json / en.json |
加按鈕文字、下拉選項 |
按鈕 disable 條件:MF.oscal_profile_uid 為 null(理論上不會,必填)。
| Repo | 工作 |
|---|---|
| BE(主) | route / app service / generator / DI / unit test |
| FE | MF 詳細頁按鈕 + service method + i18n |
| jedi-* | 不動。讀現有 ProfileService / CatalogService / OscalPartyService 等 |
| test | E2E 一條:「下載 blank → 開啟驗 9 sheet 結構」+「下載 filled → 驗 row 數對齊 MF 既有資料」 |
| changelog | docs/changelog/YYYY-MM-DD-feat-mf-ssp-import-template-download.md |
寫 implementation plan 前要 verify:
| # | 假設 | 驗證方式 |
|---|---|---|
| 1 | MF ↔︎ system_characteristics 鉤稽路徑 | grep module_frame.*system_characteristic 或追 SSP versioning service 找 MF 怎麼 init system_characteristic |
| 2 | profile → controls 取法 | jedi-oscal ProfileService.get_profile_controls(profile_uid) 或類似 method |
| 3 | tenant scope user list 查法 | 是否有現成 UserDomainService.list_by_tenant() 或要新建 |
| 4 | reference docs pool 範圍 | MF own (module_frame_reference_document) vs tenant pool — 確認用哪個 |
| 5 | responsible_parties context_type 是否支援 'module_frame' |
A0.1 design §6 列了,但要驗 enum / 程式碼有支援 |
| 6 | openpyxl defined_names API |
openpyxl 3.1.x 是用 DefinedName class,verify 寫法 |
每項在 implementation plan task 0 列具體 grep / read 步驟。
| 項目 | 影響 | 緩解 |
|---|---|---|
| profile 內 control 數量超大(NIST 800-53 約 1000 個 controls) | Excel row 數巨大、產出慢 | 第一版接受,若客戶反映改用分 sheet by-family(AC / AU / IA / ...) |
| filled mode 控制項 sheet 控制項 ↔︎ AO 父子 row 視覺化 | 使用者看不出 hierarchy | 用 control_id 在 AO row 留空 + 縮排 / 不同底色標記;A2 parser 用 control_id 連續性判斷父子 |
| openpyxl defined_names API 版本差異 | 0.x / 1.x / 3.x API 不同 | Pre-flight verify openpyxl 3.1.5 API |
| 跨 schema 跨 service 取資料的 transaction 邊界 | 部分撈失敗時是否整體 rollback | A1 是 read-only,沒寫入;單 @transaction scope 即可,撈不到的 sheet 出空白 + warning log |
| filled mode 01_基本資料 sheet 來源已釐清 | 已解:MF 表沒此鉤稽路徑,01 sheet 改對齊 module_frames 自有欄位 |
00_說明 R1)的後續向後相容機制 — 詳見 §14A1 樣板上線後,未來必然會因為實務反饋調整欄位(加 / 刪 / 改)。這節定義變更類型、成本、與 A2 parser 的相容性 contract。
sheet_definitions.py 集中定義(§7.1)— 全部欄位 spec 在一個 dataclass tuple,加減改不必散落動 generator / app service / route00_說明 R1(§4.2)— 標 v<MAJOR>.<MINOR>.<PATCH>,作為 A2 parser 判斷如何解析的 contract anchor| 變更類型 | 範例 | bump | A2 parser 影響 | 估時 |
|---|---|---|---|---|
| 加欄位 | 04_設備 加「序號 serial_no」 | minor (v1.0.0 → v1.1.0) |
相容讀新欄位 | < 30 分鐘 |
| 刪欄位 | 04_設備拿掉「OS」 | minor or major | 拒絕舊樣板 / 忽略多欄位(看政策) | < 1 小時 |
| 改欄位語意 | enum 值改名(operational → active) |
major (v1.x → v2.0.0) |
分版本解析 + 舊資料 migration | 半天以上 |
| 版號變動 | 觸發條件 | A2 parser 行為 |
|---|---|---|
PATCH(v1.0.0 → v1.0.1) |
header 文字 / 註解 / 顏色 / 樣板說明調整,欄位結構不變 | 完全相容,不必改 parser |
MINOR(v1.0.0 → v1.1.0) |
加 optional 欄位 / 加 enum value / 加 sheet | parser 向下相容讀(舊 parser 讀新樣板:忽略未知欄位) |
MAJOR(v1.x → v2.0.0) |
刪欄位 / 改欄位語意 / 刪 sheet / 拆 sheet / 必填欄位變更 | parser 必須分版本處理,或拒絕舊樣板(提示重新下載) |
不論哪種變更,落地步驟:
sheet_definitions.py 對應 ColumnDef / SheetDefTEMPLATE_VERSION = "v1.x.x" 常數).po 翻譯(zh_Hant_TW + en)feat 或 tweak,模組 [ssp-import-template]?template_version=v1)— YAGNI,第一版單版號即可A2 parser 開工時必須做的事(implementation-plan-A2 會涵蓋):
00_說明 R1 取樣板版本字串>=1.0.0, <2.0.0)BadRequestError + 提示重下對應版本下一步:implementation-plan-A1.md 已產出,準備進 Task 0 (Pre-flight Verification)。
實作過程中對原始 design 的偏離 / 擴增紀錄(保留決策軌跡)。
Finding:T6 fix2-C 把 SHEET_PERSONS role enum 從 OSCAL 字串改 ParticipantRole 4 值(manager/reviewer/auditor/viewer),但這暴露了既有跨 domain 不一致問題:
SSP 概念(OSCAL) GRC 概念(系統權限)
───────────── ─────────────
oscal_responsible_parties project_participants
.role_id (free string) .role (ParticipantRole enum)
↑ ↑
docx import 寫入 project 建立 / 編輯時寫入
無 validation / normalize 強制 4 值之一
現況問題:
module_frame_write_strategy.py:303 + ssp_write_strategy.py:316 直接吃 parsed.role or "" 寫入,無 enum validationoscal_responsible_parties.role_id 可能是雜亂混合:OSCAL 字串 / 中文 / 隨便填A2 parser contract(必做):
manager / reviewer / auditor / viewer 之一oscal_responsible_parties.role_id 仍為字串,但保證內容對齊系統 enum未來考量(不在 A2 範圍,更大設計議題):
對應 issue:docs/issues/pending/2026-05-19-person-role-cross-domain-inconsistency.md
偏離項:T6 BE+FE shipped 後使用者反饋 5 點 UX 問題,1 輪 polish 解決。
| # | 修補 | 機制 |
|---|---|---|
| 1 | FE Dialog 框架名跑版 | DownloadSspBlankTemplateDialog 兩 dropdown 改 vertical stack |
| 2 | UID 欄位 user 看到困惑 | ColumnDef.hidden=True + column_dimensions[X].hidden(user 看不到,parser 仍可讀)— 01 sheet framework_version_uid/profile_uid + 07 sheet statement_id |
| 3 | 訪談時要手 key email/name | ColumnDef.autofill_from=<helper_key> + lookups_helpers bundle field + _apply_autofill_formulas 寫 INDEX/MATCH 公式(blank mode only,可 override) |
| 4 | 07 sheet 看不出哪要填 | impl_status / statement 改 required=True(header 自動黃底) |
| 5 | 08 程序書欄位系統沒對應 | 移除 doc_no / doc_type / version(schema 對齊 module_frame_reference_documents 實際欄位 title + description) |
TEMPLATE_VERSION bump v1.0.0 → v1.1.0:lookup sheet 多欄擴增(parser 介面不變仍兼容)+ 08 sheet 刪欄位(pre-prod 可接受)。
對 A2 parser 影響:
load_workbook(data_only=True) 讀 cached value;以 cell 純值為主,不解析公式偏離項:原始 §3 只設計 MF-scoped 一個 endpoint,T6 實作時擴增為兩個 endpoint。
驅動原因:FE 整合 T6 時釐清使用者實際工作流程,發現顧問訪談前場景(S1)需要未綁定 MF 的樣板下載。原始 design 假設「先有 MF 才下載」不符實務。
改動範圍:
GET /api/1.0/ssp-import-template?framework_version_uid=&mode=blankTemplateDataBundle.mf_uid / mf_name 改 Optional[str](無 MF context 時為 None)00_說明 sheet 在 mf_uid/mf_name None 時顯示「—」generate_by_framework_version method_fetch_lookups_without_mf (REF_DOCS_MF 空 list) + _build_controls_with_aos_from_catalog (catalog 全 controls superset) + _build_filename_by_framework_versionGRC_FRAMEWORK_VERSION_NOT_FOUND (404032) + GRC_TEMPLATE_FRAMEWORK_VERSION_REQUIRED (400064)對 A2 phase 的影響:A2 parser 必須區分兩條 import flow:
A2 implementation plan 必須涵蓋這兩條 path(建議 Excel 樣板 07 sheet 加「是否納入 profile」欄位給顧問標記,或 row 留空判定為不納入)。