Date: 2026-06-20 · Branch:
FR-040-AP-improvement決策依據與被排除選項見docs/analysis/2026-06-20-ap-task-control-subject-linkage.mdOSCAL 依據:v1.2.2 schema + 官方範例oscal-content/examples/ap/json/ifa_assessment-plan.json
稽核計畫填寫頁(RoundApAuthoringView.vue,route project-ap-authoring-round)兩件事:
system_menu(group AUDIT_METHOD)驅動,後台可擴充。 見 commit 8de251b6(BE seed) / f1b2052(FE)。受評單位看一條行程即知:幾點 → 稽核員用什麼方法 → 要查哪些控制項 → 要檢查哪些設備/系統 → 哪些人(受評方)要到場 → 稽核方誰會來。據此準備人與物。
| 欄位 | 意義 | 多選 | 選項母體(同步來源) |
|---|---|---|---|
| title / type | 既有 | — | — |
| timing | 時間(幾點) | — | 既有 |
| method | 查核方法 | ✅ | system_menu AUDIT_METHOD |
| controls | 要查哪些控制項 | ✅ | Tab1 受評控制項 勾選集 |
| subjects | 受評對象(設備/系統/人員) | ✅ 跨類型 | Tab2 受評對象 全部項目 |
| participants | 稽核人員(稽核方) | ✅ | SSP parties(OSCAL party 池,與受評對象同源;外部稽核員加為 SSP party) |
控制項/方法用現有欄位;受評對象/參與人員各開一張 link 表(2026-06-21 決策:路線圖有 行程通知/行事曆/儀表板,按人/對象反查行程,join 遠勝 JSONB containment)。建表理由與表結構見 analysis 受評對象/參與人員段。
ap_tasks(一條行程;1 task = 1 activity)
├─ timing (JSONB) 時間
├─ associated_activities (JSONB) [{ activity-uuid }] ← 引用 ap_assessment_activities
│ (subjects / responsible_roles 兩 JSONB 欄改不寫,SoT 移到下面 link 表)
├── ap_task_subjects (新表) 受評對象 (task_id, subject_uuid, subject_type, include)
└── ap_task_participants (新表) 參與人員 (task_id, party_uuid, role_id)
ap_assessment_activities(一筆活動)
├─ related_controls (JSONB) 控制項 {control-selections:[{include-controls:[{control-id}]}]}
├─ props (JSONB) 方法 [{name:"method", value:"EXAMINE"}, …](多選=多筆)
└─ steps (JSONB) 查核步驟(選填,本期不做 UI)
| 要存的 | 表.欄 | OSCAL 路徑 |
|---|---|---|
| 控制項 | ap_assessment_activities.related_controls (JSONB) |
activity.related-controls → control-id |
| 方法(多) | ap_assessment_activities.props (JSONB) |
activity.props name=method |
| 受評對象(多,跨類型) | ap_task_subjects(link 表,SoT) |
task.subjects → subject-uuid + type |
| 參與人員(多) | ap_task_participants(link 表,SoT) |
task.responsible-roles → role-id + party-uuids |
| 行程↔︎活動 | ap_tasks.associated_activities (JSONB) |
task.associated-activities.activity-uuid |
| 時間 | ap_tasks.timing (JSONB) |
task.timing |
method 放 props:官方範例就是
props:[{name:method,value:EXAMINE}],props 是 list 天生多選, 是 OSCAL 對 assessment method 的標準表示,不是硬塞。 受評對象/參與人員改 link 表(非 JSONB):單一資料源 = 表;OSCAL 匯出由 mapper 從表組回task.subjects/task.responsible-roles,匯入時 parse 進表 → 不雙寫、相容性不變。
2026-06-21 修正:原設計誤用專案參與者;user 指出稽核員(含外部)應源自 SSP party。
PUT /ap/{apUid}/tasks — body { tasks: [...] },每條行程:
{
"title": "…", "type": "action", "timing": <既有>, "description": "…",
"methods": ["EXAMINE", "INTERVIEW"], // → activity.props
"controls": ["ac-6.1", "ia-2"], // → activity.related_controls(control-id)
"subjects": [ {"subject_uuid":"…","type":"component"},// → task.subjects
{"subject_uuid":"…","type":"party"} ],
"participants": [ {"party_uuid":"…","role_id":"assessor"} ] // → task.responsible_roles
}
讀取(GET …/ap 內 tasks)還原同形狀(join ap_tasks + ap_assessment_activities)。 全量覆寫沿用現有 set_tasks 策略(一併重建 activity)。
每條行程 row 既有(標題/類型/時間/方法)+ 新增兩個 MultiSelect:
readonly 規則沿用既有(僅 audit_planning 且 auditor/manager 可編)。
完全符合。匯出 OSCAL JSON 時 mapper 組裝:
ap_assessment_activities → local-definitions.activities[](method 已是 props、controls 已是 related-controls)。ap_tasks.associated_activities → task.associated-activities[]。ap_task_subjects(link 表)→ task.subjects[](依 type 群組 include-subjects)。ap_task_participants(link 表)→ task.responsible-roles[](依 role_id 群組 party-uuids)。 匯入時反向 parse 進 link 表。link 表為單一資料源,不再寫 ap_tasks.subjects/responsible_roles JSONB。statement-ids / control-objective-selections)—— 先做控制項層。NIST ifa_assessment-plan.json 的 metadata 內:roles=[{id:"assessor"}]、 parties=[{type:"person", name:"Amy Assessor"}, {type:"organization", name:"IFA"}]、 responsible-parties=[{role-id:"assessor", party-uuids:[Amy]}];task.responsible-roles=[{role-id:"assessor"}]。 → 稽核團隊放在 AP 自己的 metadata.parties(非 SSP、非專案參與者)。SSP 另由 import-ssp 帶入=受評方。
實務流程支撐:SSP 階段常不知道誰來稽核;AP 常由受評公司代填,稽核窗口此時才確定 → 在 AP 階段「就地新增」稽核人員 = OSCAL 標準做法,不是 workaround。
oscal.assessment_plans.metadata_id(1:1)已存在;oscal.parties / oscal.roles 以 metadata_id scope。oscal.parties WHERE metadata_id = AP.metadata_id(= OSCAL AP.metadata.parties)。ap_task_participants(party_uuid, role_id="assessor")(party_uuid 改指 AP party uuid)。oscal.roles 有 assessor(建 AP draft 時 seed,或首次新增 party 時補)。oscal.parties(metadata_id=AP.metadata_id)→ 回 uuid → 自動選入。建真 party 拿 uuid,不存自由文字。ssp_party_app_service 的 party CRUD 形態,只是 metadata_id 來源改 AP(mirror,不另造輪子)。ap_task_participants → task.responsible-roles[](role-id + party-uuids);AP parties → metadata.parties[]; assessor role → metadata.roles[];文件層 metadata.responsible-parties[] 由 mapper 彙整。
本節取代 §3/§5/§7 的「稽核人員 = SSP parties」過渡做法(commit 1e8c231)。過渡版可運作但語意不對,B 落地後切換。
受評單位常需要的不是一句籠統說明,而是「每個查核方法各自要看什麼、怎麼看」,例:
文件檢查:檢視帳號權限總表、FORM4042640 帳號權限審查紀錄表,確認「一人一帳號」。 電腦設定比對:比對電腦使用者帳號設定,確認每項程序與帳號皆具唯一性。
把現在的單一多行「說明」(自由文字 Textarea,過渡 commit 0103997) 升級成結構化、依方法分段的查核指引:受評方一看就知每個方法要準備/檢視什麼,且 OSCAL-native、可被匯出/報表/通知引用。
activity.steps[](required uuid;含 title / description / props…)。官方範例每 step = 一個查核程序步驟 ({uuid, title, description})。本期把 step ↔︎ 查核方法 1:1 綁定:step.props 帶 {name:"method", value:<method value>}。
ap_assessment_activities.steps JSONB(欄位已存在):
[
{ "uuid": "...", "title": "<method value/label>",
"description": "<該方法的查核指引>",
"props": [{ "name": "method", "value": "EXAMINE" }] }
]
activity.props name=method)仍是「這條行程用哪些方法」的權威來源(chip / 統計)。ap_tasks.description(多行 Textarea)保留為「行程整體備註」(選填),與 per-method steps 並存、語意不同。在「查核方法」下方新增一區 「查核指引」:依 tk.methods 動態列出每個已選方法一列(方法 label + 該方法的指引 Textarea)。
tk.stepGuidance: Record<methodValue, string>(method value → 指引文字)。
ap.tasks[].steps → map(step.props.method → step.description)。v-for over tk.methods,每方法一個 Textarea 綁 tk.stepGuidance[method]。"steps": [ { "method": "EXAMINE", "description": "檢視帳號權限總表…" }, … ] // 只帶有指引的方法
steps: [{method, description}](method 由 step.props 還原;title 由 FE i18n 出 label)。set_tasks:每筆 task 把 payload steps → activity.steps,每筆補 uuid(uuid4) + props=[{name:"method",value:method}] + title(= method value,FE 顯示用 i18n) + description。_ap_detail:讀 activity.steps → steps:[{method: <props.method>, description}]。_steps_from_payload(steps) / _steps_to_payload(activity_steps)。_TaskItem 增 steps(fields.List(Nested(method, description)),load_default None)。ap_assessment_activities.steps)。activity.steps 即 OSCAL steps(title/description/props);匯出零轉換。
reviewed-controls / responsible-roles(OSCAL 容許)本期不用。description(整體備註保留)。實作完成(2026-06-21),BE 12 測試綠 / FE build 過,待 user 手測驗收。零套件改動、零 migration (ap_assessment_activities.steps 欄位早已存在,entity/mapper 已雙向接好)。 commit:主專案 + FE FR-040-AP-improvement(未 push)。changelog docs/changelog/2026-06-21-feat-fr040-phase7-ap-task-step-guidance.md。
lang.oscal_role.ssp_party_role i18n(原顯示 raw role-id)。定案(2026-06-21):維持 Phase 7「每方法一條 textarea」為最終形態,不下沉到 AO 層、不加控制項維度。 理由:per-method 已是 OSCAL 標準做法(
activity.steps+propsmethod,官方範例即如此),對齊報表 「檢查方式/檢查方法」結構;AO 的 [a][b][c] 差異 auditor 可在該方法的指引文字內以文字註明,不值得為它 做 3 維 grid(控制項×AO×方法)的 UI 複雜度 + AO 端點。逐列儲存(Part B)亦暫不做,維持全量覆寫。 下方 12.1~12.8 為當初評估 AO 方案的完整軌跡(含被排除原因),保留供未來若報表要 per-AO 自動拆分時重啟參考。 未來反悔條件:若要「那份 per-AO Excel 報表自動填充、按 AO 拆證據/結果」→ 才需重啟 AO 層(見 §12.7 ③)。
Phase 7 把「查核指引」綁在 (行程, 方法) —— 一個方法只能對應一條指引。但實際稽核粒度是 (控制項, AO, 方法):一個控制項有多個 AO(控制目標 [a][b][c]),同一方法用在不同 AO 的 「要看什麼、怎麼看」完全不同(實例:CMMC IA.L1-B.1.V 的 [a] 文件檢查 vs [b] 文件檢查 指引不同)。 現況指引會把同控制項的多個 AO 混成一條。
並行需求:行程現在是全量覆寫、一顆按鈕全存(set_tasks delete-all-recreate)。多列各自編輯時要能 逐列儲存(避免一存覆蓋全部、多人/多次編輯互蓋)。
抉擇結論(2026-06-21,user 拍板):
- 行程粒度:選 A — 行程維持可跨多控制項,指引區改
控制項 → AO → 已選方法分組(非「一行程=一控制項」)。- 指引歸屬:指引內容是「本案/本輪特定的」(引用特定表單/系統),屬 AP/這一輪,不上移到 catalog (catalog 屬性可重用、指引不可重用)→ 仍存
ap_assessment_activities.steps(AP-scoped)。- 本期 scope:①AO 指引 + ②逐列儲存。③帶去 AR + per-AO Excel 報表延後(AR matrix 改走 AO 層 + 報表來源/格式需另釐清,另開 phase)。
OSCAL step.reviewed-controls(AssessmentReviewedControls)可帶 control-selections,其 select-control-by-id 支援 statement-ids[] 指定該控制下的特定 statement/objective。 → 每個 step 可精準 scope 到 (control-id, AO),OSCAL-native,無需自創欄位。 AO 識別碼用全系統 canonical _obj.N(catalog_control_parts.part_id,part_name='assessment-objective'), 與 AR 判定 / 準備期 job 同一套(共用 app/grc/service/ao_derivation.py:derive_ao_pairs,無 AO 走控制層 fallback)。
ap_assessment_activities.steps JSONB,每 step 加 AO scope)// activity.steps[] 每筆 = (控制項, AO, 方法) → 一條指引
{
"uuid": "...",
"description": "<檢查方法/查核指引>",
"props": [{ "name": "method", "value": "EXAMINE" }],
"reviewed-controls": { "control-selections": [
{ "include-controls": [ { "control-id": "ia-1", "statement-ids": ["ia-1_obj.a"] } ] } ] }
}
activity.props(行程用哪些方法、chip/統計);step 是「每 (AO,方法) 的指引」。statement-ids 省略(scope 退到 control-id),等同 Phase 7 行為。AO 清單(新增,FE 畫 grid 用) GET /ap/{apUid}/control-objectives → { "<control_id>": [ { "ao_id":"ia-1_obj.a", "label":"[a]", "prose":"..." }, … ] }
catalog_control_repo.get_by_catalog 組 cc_map → derive_ao_pairs + 取 part title/prose/label。label 來源待 pre-flight 驗(part.props name=label / part_id 尾碼 / prose 首段,擇一)。_part_repo/_catalog_control_repo/_profile_import_repo → 方案(i) 在 AP service 補 wiring;方案(ii) route 借 AR app service 既有方法。pre-flight 決定(傾向 i,read 歸 AP service)。逐列儲存(Part B,取代全量覆寫為主路徑)
POST /ap/{apUid}/tasks 新增一條行程 → 回 { uid, … }PUT /ap/{apUid}/tasks/{taskUid} 更新單條行程(含其 activity + steps + 受評對象/稽核人員 link)DELETE /ap/{apUid}/tasks/{taskUid} 刪一條行程(cascade 其 activity + link)_TaskItem + steps 改成 [{control_id, ao_id|null, method, description}]。PUT /ap/{apUid}/tasks(set_tasks)保留為相容/批次入口;FE 改走逐列。v-for 控制項(tk.controls) → v-for AO(該控制項 objectives) → v-for 已選方法(tk.methods) → 一格 Textarea。
GET …/control-objectives,只顯示 tk.controls 內控制項;無 AO 控制項退成單層(控制項 → 方法)。tk.stepGuidance: Record<"<control_id>|<ao_id>|<method>", string>(三元組 key)。_steps_from_payload / _steps_to_payload 改 (control_id, ao_id, method, description) ↔︎ OSCAL step(含 reviewed-controls)。list_control_objectives(ap_uid)(AO 清單)。add_task / update_task / delete_task(per-task upsert,需穩定 task uid;repo 單列 add/update/delete 能力 pre-flight 驗套件)。_TaskStepItem 加 control_id/ao_id。評估後定案不做(2026-06-21),維持 Phase 7 per-method。本節作為決策軌跡保留(含被排除原因 + 未來反悔條件)。 implementation-plan「Phase 8」同步標記暫不做。
原三頁籤(①受評控制項 ②受評對象 ③行程與方法)有重複輸入:先在 ①②勾母體,再在 ③從母體取子集。 洞察:行程裡每條 task 已各自鉤稽控制項/受評對象(FR-040 落 ap_assessment_activities.related_controls / ap_task_subjects),所有 task 的聯集即等於受評控制項/對象母體 → ①②可由 ③推導,不必手動先選。
定案(user 2026-06-21):
set_tasks 存檔時,把所有 task 控制項聯集回填 ap_reviewed_controls、受評對象聯集回填 ap_assessment_subjects。ap_reviewed_controls 是 AR 驅動源(assessment_result_app_service._inscope_control_ids → AR 矩陣初始 / close-round 覆核 / 判定頁)。 → set_tasks 必須回填,否則 AR 拿不到 scope。回填後 downstream 全部零改動。GRC_AP_REVIEWED_CONTROLS_REQUIRED)」改由 set_tasks 回填滿足; set_tasks 內部回填走 domain self._ap.set_reviewed_controls(不套空值擋,允許 WIP 存無控制項的行程), stage-advance 既有 gate 仍在真正進階時擋空 scope。assessment_plan_app_service.set_tasks):迴圈收集 task 的 controls/subjects 聯集(保序去重)→ self._ap.set_reviewed_controls(ap_uid, build_control_selections(union), …) + self._ap.set_assessment_subjects(ap_uid, [ApAssessmentSubjectsEntity(...)], …)。同 @transaction。RoundApAuthoringView.vue):
groups,去除 selected 過濾);受評對象下拉母體 = 完整 SSP 對象(subjectRegistry 全集,去除 subjectSel 過濾)。tasks[] 即時推導的控制項/對象聯集(移除勾選 UI + save 按鈕)。實作完成並上線(2026-06-21)。commit:BE f39019ed、FE b7b89b6(頁籤改序 + 後兩頁唯讀範圍摘要 + set_tasks 回填 reviewed-controls/subjects)。
延伸(2026-06-21 收尾):AP 進入唯讀後,「行程與方法」原為整片 :disabled 灰掉表單、回頭 review 體感差 → 改成乾淨標籤化唯讀呈現(純文字 + Tag,只顯示有值欄位),編輯階段維持原表單。commit:FE e4e8dc3;changelog docs/changelog/2026-06-21-tweak-ap-readonly-display.md。