# FR-040 行程鉤稽控制項/受評對象/參與人員 — 實作計畫

> Date: 2026-06-20 · 設計見同夾 `design.md` · 決策見 `docs/analysis/2026-06-20-ap-task-control-subject-linkage.md`
> 原則：先文件後 code；開工前先跑「Phase 0 pre-flight」驗證假設（plan→build 常漂移）。

## Phase 0 — Pre-flight 驗證（開工第一步，逐項確認再動）

1. `jedi_oscal_v2` `ApTasksEntity` 確有 `associated_activities / subjects / responsible_roles`（JSONB）—— 已查屬實。
2. `ap_assessment_activities` 表/entity 確有 `related_controls / props / steps` —— 已查屬實。
3. **確認 `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）。
4. **確認 `AssessmentPlanAppService.set_tasks` / `self._ap.set_tasks` 的套件 signature**
   （現在只寫 ap_tasks；要擴成同 transaction 內先寫 activities 再寫 tasks）。
5. 確認 control-id 形狀：`related_controls.control-selections[].include-controls[].control-id` 用的是
   OSCAL control-id（如 `ac-6.1`），跟 Tab1 reviewed-controls 存的 control_ids 同源（**verify 一致，否則對不上**）。
6. 確認 Tab2 受評對象（`ap_assessment_subjects`）回前端的 subject 結構（subject_uuid / subject_type / title），
   作為前端 options + payload 來源。
7. 確認專案參與者查詢（auditor/manager party uuid / user uuid）既有 service（FE 受評對象/參與者面板已有類似）。

## Phase 1 — 套件 schema：加 2 張 link 表（jedi_oscal_v2）

> user 2026-06-21 允許改外部套件。dev 走 poetry path-dependency，feature 完成才發 Nexus。

- 新增 model/entity/mapper/repo：
  - `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)。
- repo 提供 by-task 全量覆寫（mirror set_tasks 策略）。
- **mapper（OSCAL 序列化）**：
  - 匯出：`ap_task_subjects` → `task.subjects[]`（依 type 群組 include-subjects）；
    `ap_task_participants` → `task.responsible-roles[]`（依 role_id 群組 party-uuids）。
  - 匯入：反向 parse。
- `ap_tasks.subjects` / `responsible_roles` 兩 JSONB 欄改為**不寫**（SoT 移到新表）；是否 drop 欄由套件決定（建議保留欄位但停用，降風險）。
- SQL：`scripts/sql/2026-06-2x-fr040-ap-task-link-tables.sql`（建表 + GRANT cm_app/cmmgr + index + schema_migrations），cmmgr 套 DEV（feature 階段先 DEV）。

## Phase 2 — BE 寫入（`AssessmentPlanAppService.set_tasks` 擴充）

> 檔：`app/grc/service/assessment_plan_app_service.py`

- `set_tasks(ap_uid, tasks, ...)` 每條行程：
  1. 組 `ApAssessmentActivitiesEntity`：`related_controls` = controls → control-selections 結構；
     `props` = methods → `[{name:"method",value:m}]`（沿用既有 `_methods_to_props`）；`uuid` = 新 uuid4。
  2. 組 `ApTasksEntity`：`timing`（既有）、`associated_activities` = `[{activity-uuid: <上面 activity uuid>}]`。
  3. 寫 **link 表**：`ap_task_subjects`（subjects）、`ap_task_participants`（participants），by task_id。
  4. 全量覆寫：同 `@transaction` 內依序重建 activities → tasks → link 表。
- 新增 helper：`_controls_to_related_controls` / 反向（controls ↔ related_controls JSONB）。
- **methods 仍走 props**（不動既有 `_methods_to_props`），只是搬到 activity 而非 task。
- subjects/participants **不再組 OSCAL JSONB**（直接落表，OSCAL 形狀交給 Phase 1 mapper）。

## Phase 3 — BE 讀取（AP detail 還原）

> 同檔 `_to_ap_detail`（約 line 127-144）

- tasks 改成 **join ap_tasks + ap_assessment_activities + ap_task_subjects + ap_task_participants**。
- 每條還原：`methods`（activity.props）、`controls`（activity.related_controls）、
  `subjects`（ap_task_subjects）、`participants`（ap_task_participants）、`timing`。
- audit 欄位規範：participants/subjects 的 party/subject uuid → enrich nickname/name（比照既有 enrich pattern）。

## Phase 4 — FE（`RoundApAuthoringView.vue` Tab3）

- 載入：除既有 fetchAp，補抓 Tab1 控制項集、Tab2 受評對象集、專案參與者集（多數已在頁面其他 tab 抓過，能重用就重用）。
- 每條行程 row 加 3 個 MultiSelect：控制項 / 受評對象（依 type 分組）/ 參與人員。
  - options 分別 = Tab1 勾選集 / Tab2 全集 / 參與者（auditor·manager）。
- `saveTasks` payload 擴充 controls / subjects / participants（見 design §6）。
- readonly 規則沿用既有。
- i18n：補欄位標籤 key（zh-tw + en），沿用 `lang.ap_authoring.*`。

## Phase 5 — 測試

- 套件 pytest（jedi_oscal_v2）：新 model/repo CRUD + mapper round-trip（表 ↔ OSCAL task.subjects/responsible-roles）。
- BE pytest：`set_tasks` round-trip（存→讀形狀一致）、methods 仍落 props、controls 落 activity、
  subjects/participants 落 link 表；全量覆寫不殘留舊 activity/link。logger patch autouse fixture（DBLogHandler 雷）。
- FE/E2E：在 `compliance-manager-test` 專案補（行程加控制項/對象/人員 → 存 → 重整還原）。
- 測試計畫由 `feature-test-planner` agent 產出 `test-plan.md`（Phase 4 SOP）。

## DB / 套件異動

- **新增 2 張表**（jedi_oscal_v2）：`ap_task_subjects` / `ap_task_participants`（見 Phase 1）。
- migration `scripts/sql/2026-06-2x-fr040-ap-task-link-tables.sql`（建表+GRANT+index+schema_migrations），dev 先套。
- 控制項/方法/timing **零 schema 變更**（用現有欄位）。
- 套件異動走 dev poetry path-dep，feature 完成 + smoke 過才發 Nexus（依套件發版規範）。

## Phase 6 — 稽核人員 = AP metadata.parties + 就地新增（design §10，B 方案，待 user approve）

> 確認結果：**零新表** —— `oscal.assessment_plans.metadata_id`(1:1) 已存在、`oscal.parties`/`oscal.roles`
> 以 `metadata_id` scope，重用即可。per-task 沿用既有 `ap_task_participants`。

- **Phase 6.0 pre-flight**：確認 (a) `party_repo_impl` / SSP party CRUD 能以任意 metadata_id 操作（非寫死 SSP）；
  (b) AP draft 生成時 metadata row 是否已建、是否已有 roles；(c) 權限（誰能在 AP 加 party）。
- **BE**：新增 `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。
- **set_tasks/_ap_detail**：participants 的 party_uuid 改指 AP party uuid（資料形狀不變，來源語意換）。
- **FE**：`taskParticipantOptions` 改抓 `GET /ap/{apUid}/parties`（取代 SSP `people`）；
  下拉加「＋新增」inline quick-add（姓名 / 類型 / role=assessor）→ 呼 POST parties → 選入。
- **OSCAL 匯出（後續）**：AP parties→metadata.parties、assessor→metadata.roles、ap_task_participants→task.responsible-roles。
- **零新表 / 零 migration**（重用 oscal.parties/roles/metadata）。
- 取代過渡版（commit `1e8c231` 稽核人員=SSP parties）。

## DB / 套件異動

- **新增 2 張表**（jedi_oscal_v2）：`ap_task_subjects` / `ap_task_participants`（見 Phase 1）。
- migration `scripts/sql/2026-06-21-fr040-ap-task-link-tables.sql`（建表+GRANT+index+schema_migrations），dev 已套。
- 控制項/方法/timing **零 schema 變更**（用現有欄位）。
- **Phase 6（稽核人員 AP parties）零新表**：重用 `oscal.parties`/`oscal.roles`（metadata-scoped）。
- 套件異動走 dev poetry path-dep / symlink，feature 完成 + smoke 過才發 Nexus（依套件發版規範）。

## Phase 7 — 說明改「每方法查核指引」(activity.steps)（✅ 實作完成 2026-06-21，待手測驗收）

> 完整設計見 design.md §11。**零套件改動、零 migration**（`ap_assessment_activities.steps` 欄位早已存在、
> entity/mapper 已雙向接好）。BE 12 測試綠 / FE build 過。changelog
> `docs/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_role` i18n。

- **BE**（`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}]`。
  - helpers `_steps_from_payload` / `_steps_to_payload`。
  - route `_TaskItem` 增 `steps` 欄（List of {method, description}，load_default None）。
- **FE**（`RoundApAuthoringView.vue`）：
  - TaskRow 增 `stepGuidance: Record<methodValue,string>`；載入由 `tk.steps` 還原。
  - 卡身「查核方法」下加「查核指引」區：`v-for over tk.methods` 動態列每方法一個 Textarea。
  - saveTasks payload `steps = methods.filter(有指引).map(m => ({method:m, description:guidance[m]}))`。
  - i18n：區塊標題 `task_step_guidance` 等。
- **測試**：set_tasks round-trip 含 steps；steps ⊆ methods；method 取消其 step 不存。
- `description`（整體備註）保留不動。

## Phase 8 — 查核指引下沉 AO 層 + 行程逐列儲存（⏸ 評估後定案暫不做 2026-06-21）

> **定案維持 Phase 7 per-method（每方法一條 textarea）為最終形態**，AO 下沉 + 逐列儲存暫不做。
> 理由與未來反悔條件見 design §12 banner。下方 8.0~8.5 保留為「若未來要 per-AO 報表自動拆分」時的現成 plan。

### Phase 8.0 — Pre-flight（開工第一步，逐項驗，漂移就停）
1. **AO 顯示 label 來源**：`catalog_control_parts` 的 AO part，[a][b][c] 標籤從哪來 —
   驗 `part.props`(name=label?) vs `part_id` 尾碼 vs `prose` 首段；FE grid 顯示要的 label/prose 取得方式定案。
2. **AO 端點落點 + DI**：AP app service 現無 `_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）。
3. **逐列儲存 repo 能力**：驗 jedi_oscal_v2 `ApTasksRepo`/`ApAssessmentActivitiesRepo` 有無單列
   update/delete by uid/id（現 set_tasks 走全量 delete-all+`_ap.set_tasks` 全覆寫）；task 是否有穩定 `uid`（`_ap_detail` 已回 t.uid → 有）。
4. **OSCAL step shape**：確認 `select-control-by-id.statement-ids` 是放 AO `_obj.N` 的正確欄（vs `control-objective-selections`）；
   挑與 AR/匯出未來相容者。
5. control_id ↔ catalog 比對鍵一致性（沿用 Phase 7：related-controls 的 control-id == reviewed-controls control_id == catalog control_id）。

### Phase 8.1 — BE：AO 清單端點
- `assessment_plan_app_service.list_control_objectives(ap_uid)` → `{control_id: [{ao_id, label, prose}]}`，只取 AP reviewed-controls。
  - 重用 AR 既有 AO 推導鏈（cc_map ← AP→SSP→profile→catalog；`derive_ao_pairs`），補抓 part title/prose/label。
- route `GET /ap/{apUid}/control-objectives`（auditor/manager read；沿用既有 round/AP 解析守門）。
- DI wiring 依 8.0.2 決定。

### Phase 8.2 — BE：steps 改 (control_id, ao_id, method) + helpers
- `_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，歸控制層）。
- serializer `_TaskStepItem` 加 `control_id`(allow_none) / `ao_id`(allow_none)。

### Phase 8.3 — BE：逐列儲存端點（Part B）
- `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）。
- routes：`POST /ap/{apUid}/tasks`、`PUT/DELETE /ap/{apUid}/tasks/{taskUid}`；守門沿用。
- 既有全量 `set_tasks` 保留（相容/批次）；helper 抽共用（單列與全量共用組裝邏輯）。
- repo 單列能力不足則於套件補（走 symlink/path-dep，發版等 user）。

### Phase 8.4 — FE：控制項→AO→方法 指引 grid + 逐列儲存
- 載入 `GET …/control-objectives`；`tk.stepGuidance` key 改三元組 `"<control_id>|<ao_id>|<method>"`。
- 卡身指引區 grid：`v-for 控制項 → v-for AO → v-for 已選方法`；無 AO 控制項退單層。
- 每卡「儲存此行程」呼單列端點、「刪除」呼 delete；新增呼 add。未存變更離開提示。
- i18n：AO/grid 標題、儲存/刪除單列按鈕。

### Phase 8.5 — 測試
- BE：`_steps_from_payload`/`_steps_to_payload` 含 AO round-trip + 向後相容（舊 method-only step）；
  `list_control_objectives` AO 推導（含控制層 fallback）；add/update/delete_task 單列正確 + 不誤動他列。
- FE/E2E（compliance-manager-test）：行程加多控制項多 AO 指引 → 逐列存 → 重整還原。

### DB / 套件異動（Phase 8）
- **零新表 / 零 migration**（沿用 `ap_assessment_activities.steps`）。
- 若 repo 單列 add/update/delete 能力不足 → jedi_oscal_v2 補 repo 方法（dev symlink/path-dep，發版等 user 明示）。

## 收尾

- changelog（type=feat）：套件加表+mapper / BE set_tasks 擴充 / FE 三連動 / Phase 6 AP parties / Phase 7 steps / Phase 8 AO 指引+逐列儲存，各別記。
- design.md §9 範圍外項目逐一保留為 follow-up。
