Date: 2026-06-20 · 設計見同夾
design.md· 決策見docs/analysis/2026-06-20-ap-task-control-subject-linkage.md原則:先文件後 code;開工前先跑「Phase 0 pre-flight」驗證假設(plan→build 常漂移)。
jedi_oscal_v2 ApTasksEntity 確有 associated_activities / subjects / responsible_roles(JSONB)—— 已查屬實。ap_assessment_activities 表/entity 確有 related_controls / props / steps —— 已查屬實。ap_assessment_activities 的 repo + set/upsert 方法: jedi_oscal_v2/infra/repository/ap/ap_assessment_activities_repo_impl.py 的 CRUD signature (能否 by ap_id 全量覆寫,mirror ap_tasks 的 set_tasks)。AssessmentPlanAppService.set_tasks / self._ap.set_tasks 的套件 signature (現在只寫 ap_tasks;要擴成同 transaction 內先寫 activities 再寫 tasks)。related_controls.control-selections[].include-controls[].control-id 用的是 OSCAL control-id(如 ac-6.1),跟 Tab1 reviewed-controls 存的 control_ids 同源(verify 一致,否則對不上)。ap_assessment_subjects)回前端的 subject 結構(subject_uuid / subject_type / title), 作為前端 options + payload 來源。user 2026-06-21 允許改外部套件。dev 走 poetry path-dependency,feature 完成才發 Nexus。
ap_task_subjects(task_id FK CASCADE, subject_uuid, subject_type, include, sort_order, title, audit 欄)ap_task_participants(task_id FK CASCADE, party_uuid, role_id, sort_order, audit 欄)ap_assessment_subjects 既有風格;index on (task_id) + (subject_uuid)/(party_uuid)。ap_task_subjects → task.subjects[](依 type 群組 include-subjects); ap_task_participants → task.responsible-roles[](依 role_id 群組 party-uuids)。ap_tasks.subjects / responsible_roles 兩 JSONB 欄改為不寫(SoT 移到新表);是否 drop 欄由套件決定(建議保留欄位但停用,降風險)。scripts/sql/2026-06-2x-fr040-ap-task-link-tables.sql(建表 + GRANT cm_app/cmmgr + index + schema_migrations),cmmgr 套 DEV(feature 階段先 DEV)。AssessmentPlanAppService.set_tasks 擴充)檔:
app/grc/service/assessment_plan_app_service.py
set_tasks(ap_uid, tasks, ...) 每條行程:
ApAssessmentActivitiesEntity:related_controls = controls → control-selections 結構; props = methods → [{name:"method",value:m}](沿用既有 _methods_to_props);uuid = 新 uuid4。ApTasksEntity:timing(既有)、associated_activities = [{activity-uuid: <上面 activity uuid>}]。ap_task_subjects(subjects)、ap_task_participants(participants),by task_id。@transaction 內依序重建 activities → tasks → link 表。_controls_to_related_controls / 反向(controls ↔︎ related_controls JSONB)。_methods_to_props),只是搬到 activity 而非 task。同檔
_to_ap_detail(約 line 127-144)
methods(activity.props)、controls(activity.related_controls)、 subjects(ap_task_subjects)、participants(ap_task_participants)、timing。RoundApAuthoringView.vue Tab3)saveTasks payload 擴充 controls / subjects / participants(見 design §6)。lang.ap_authoring.*。set_tasks round-trip(存→讀形狀一致)、methods 仍落 props、controls 落 activity、 subjects/participants 落 link 表;全量覆寫不殘留舊 activity/link。logger patch autouse fixture(DBLogHandler 雷)。compliance-manager-test 專案補(行程加控制項/對象/人員 → 存 → 重整還原)。feature-test-planner agent 產出 test-plan.md(Phase 4 SOP)。ap_task_subjects / ap_task_participants(見 Phase 1)。scripts/sql/2026-06-2x-fr040-ap-task-link-tables.sql(建表+GRANT+index+schema_migrations),dev 先套。確認結果:零新表 ——
oscal.assessment_plans.metadata_id(1:1) 已存在、oscal.parties/oscal.roles以metadata_idscope,重用即可。per-task 沿用既有ap_task_participants。
party_repo_impl / SSP party CRUD 能以任意 metadata_id 操作(非寫死 SSP); (b) AP draft 生成時 metadata row 是否已建、是否已有 roles;(c) 權限(誰能在 AP 加 party)。ap_party_app_service(list/add/update/delete parties,scope = ap_uid → assessment_plans.metadata_id), mirror ssp_party_app_service;首次操作確保 oscal.roles 有 assessor(AP metadata scope)。 route:GET/POST /ap/{apUid}/parties、PUT/DELETE …/parties/{uid}。DI wiring。taskParticipantOptions 改抓 GET /ap/{apUid}/parties(取代 SSP people); 下拉加「+新增」inline quick-add(姓名 / 類型 / role=assessor)→ 呼 POST parties → 選入。1e8c231 稽核人員=SSP parties)。ap_task_subjects / ap_task_participants(見 Phase 1)。scripts/sql/2026-06-21-fr040-ap-task-link-tables.sql(建表+GRANT+index+schema_migrations),dev 已套。oscal.parties/oscal.roles(metadata-scoped)。完整設計見 design.md §11。零套件改動、零 migration(
ap_assessment_activities.steps欄位早已存在、 entity/mapper 已雙向接好)。BE 12 測試綠 / FE build 過。changelogdocs/changelog/2026-06-21-feat-fr040-phase7-ap-task-step-guidance.md。 實作細節對齊下方;差異:i18n 區塊標題實際用task_guidance/task_guidance_ph(非task_step_guidance)。 附帶同頁修補:查核指引方法標籤改 soft pill;受評對象 party 角色改走lang.oscal_role.ssp_party_rolei18n。
assessment_plan_app_service.py):
set_tasks:payload steps:[{method, description}] → activity.steps,每筆補 uuid/props=[{name:"method",value:method}]/title=method/description。規則 steps ⊆ methods。_ap_detail:讀 activity.steps → steps:[{method(from props), description}]。_steps_from_payload / _steps_to_payload。_TaskItem 增 steps 欄(List of {method, description},load_default None)。RoundApAuthoringView.vue):
stepGuidance: Record<methodValue,string>;載入由 tk.steps 還原。v-for over tk.methods 動態列每方法一個 Textarea。steps = methods.filter(有指引).map(m => ({method:m, description:guidance[m]}))。task_step_guidance 等。description(整體備註)保留不動。定案維持 Phase 7 per-method(每方法一條 textarea)為最終形態,AO 下沉 + 逐列儲存暫不做。 理由與未來反悔條件見 design §12 banner。下方 8.0~8.5 保留為「若未來要 per-AO 報表自動拆分」時的現成 plan。
catalog_control_parts 的 AO part,[a][b][c] 標籤從哪來 — 驗 part.props(name=label?) vs part_id 尾碼 vs prose 首段;FE grid 顯示要的 label/prose 取得方式定案。_part_repo/_catalog_control_repo/_profile_import_repo; 決定 (i) AP service 補 wiring(傾向,read 歸屬 AP)還是 (ii) route 借 assessment_result_app_service 既有 _project_catalog_controls+_derive_ao_map。驗 derive_ao_pairs 能否順帶回 part title/prose(目前只回 id)。ApTasksRepo/ApAssessmentActivitiesRepo 有無單列 update/delete by uid/id(現 set_tasks 走全量 delete-all+_ap.set_tasks 全覆寫);task 是否有穩定 uid(_ap_detail 已回 t.uid → 有)。select-control-by-id.statement-ids 是放 AO _obj.N 的正確欄(vs control-objective-selections); 挑與 AR/匯出未來相容者。assessment_plan_app_service.list_control_objectives(ap_uid) → {control_id: [{ao_id, label, prose}]},只取 AP reviewed-controls。
derive_ao_pairs),補抓 part title/prose/label。GET /ap/{apUid}/control-objectives(auditor/manager read;沿用既有 round/AP 解析守門)。_steps_from_payload([{control_id, ao_id, method, description}]) → OSCAL step(props method + reviewed-controls.control-selections.include-controls[{control-id, statement-ids:[ao_id]}],ao_id 為 None 則省 statement-ids)。只存有指引者。_steps_to_payload(activity.steps) → [{control_id, ao_id, method, description}](向後相容:無 reviewed-controls 的舊 step → control_id/ao_id=None,歸控制層)。_TaskStepItem 加 control_id(allow_none) / ao_id(allow_none)。add_task(ap_uid, task) → 建 1 task + 1 activity + link,回 uid。update_task(ap_uid, task_uid, task) → 更新該 task 的 activity(related_controls/props/steps) + 重建其 link 表,by task_uid。delete_task(ap_uid, task_uid) → 刪 task(cascade activity + link)。POST /ap/{apUid}/tasks、PUT/DELETE /ap/{apUid}/tasks/{taskUid};守門沿用。set_tasks 保留(相容/批次);helper 抽共用(單列與全量共用組裝邏輯)。GET …/control-objectives;tk.stepGuidance key 改三元組 "<control_id>|<ao_id>|<method>"。v-for 控制項 → v-for AO → v-for 已選方法;無 AO 控制項退單層。_steps_from_payload/_steps_to_payload 含 AO round-trip + 向後相容(舊 method-only step); list_control_objectives AO 推導(含控制層 fallback);add/update/delete_task 單列正確 + 不誤動他列。ap_assessment_activities.steps)。