# FR-040 稽核計畫填寫（ap-authoring）優化 — 設計（SD）

> Date: 2026-06-20 · Branch: `FR-040-AP-improvement`
> 決策依據與被排除選項見 [`docs/analysis/2026-06-20-ap-task-control-subject-linkage.md`](../../analysis/2026-06-20-ap-task-control-subject-linkage.md)
> OSCAL 依據：v1.2.2 schema + 官方範例 `oscal-content/examples/ap/json/ifa_assessment-plan.json`

## 1. 範圍

稽核計畫填寫頁（`RoundApAuthoringView.vue`，route `project-ap-authoring-round`）兩件事：

1. **(已完成,先行)** 查核方法選項改 `system_menu`（group `AUDIT_METHOD`）驅動，後台可擴充。
   見 commit `8de251b6`(BE seed) / `f1b2052`(FE)。
2. **(本設計)** 「行程與方法」一條行程（task）能多對多鉤稽 **控制項 / 受評對象（設備·系統·人員）/ 參與人員**。

## 2. 使用情境

受評單位看一條行程即知：**幾點 → 稽核員用什麼方法 → 要查哪些控制項 → 要檢查哪些設備/系統 →
哪些人(受評方)要到場 → 稽核方誰會來**。據此準備人與物。

## 3. 一條「行程」的資料模型

| 欄位 | 意義 | 多選 | 選項母體（同步來源）|
|---|---|---|---|
| title / type | 既有 | — | — |
| timing | 時間（幾點）| — | 既有 |
| method | 查核方法 | ✅ | `system_menu` AUDIT_METHOD |
| controls | 要查哪些控制項 | ✅ | **Tab1 受評控制項** 勾選集 |
| subjects | 受評對象（設備/系統/人員）| ✅ 跨類型 | **Tab2 受評對象** 全部項目 |
| participants | 稽核人員（稽核方）| ✅ | **SSP parties**（OSCAL party 池，與受評對象同源；外部稽核員加為 SSP party）|

## 4. OSCAL 對應 + 儲存落點

> 控制項/方法用現有欄位；**受評對象/參與人員各開一張 link 表**（2026-06-21 決策：路線圖有
> 行程通知/行事曆/儀表板，按人/對象反查行程，join 遠勝 JSONB containment）。建表理由與表結構見
> [analysis 受評對象/參與人員段](../../analysis/2026-06-20-ap-task-control-subject-linkage.md)。

```
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 進表 → 不雙寫、相容性不變。

## 5. 選項同步規則（前端）

- 控制項下拉可選集 = **Tab1 已勾選受評控制項**（取消勾選的控制項，已選進行程者需提示/移除）。
- 受評對象下拉可選集 = **Tab2 已設定受評對象**（component/inventory/party 分組顯示）。
- 稽核人員下拉可選集 = **SSP parties**（OSCAL responsible-role 引用 party；與受評對象同一 party 池，
  選稽核方那些）。**非**專案參與者（那是操作系統的內部使用者）。外部稽核員須先在 SSP「人員/單位」加為 party。
  > 2026-06-21 修正：原設計誤用專案參與者；user 指出稽核員（含外部）應源自 SSP party。
- 精神對齊 OSCAL：reviewed-controls / assessment-subjects 是母體，task 只引用其子集。

## 6. API 合約（沿用既有端點，擴充 payload）

`PUT /ap/{apUid}/tasks` — body `{ tasks: [...] }`，每條行程：

```jsonc
{
  "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）。

## 7. UI（RoundApAuthoringView.vue Tab3）

每條行程 row 既有（標題/類型/時間/方法）+ 新增兩個 MultiSelect：
- **控制項**（options=Tab1 勾選集，顯示 control-id + 標題）
- **受評對象**（options=Tab2，依 type 分組：設備/系統/人員）
- **稽核人員**（options=SSP parties，與受評對象同一 party 池）

readonly 規則沿用既有（僅 `audit_planning` 且 auditor/manager 可編）。

## 8. OSCAL 相容性

完全符合。匯出 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。

## 9. 不在本期範圍

- AO/statement 層精準（`statement-ids` / control-objective-selections）—— 先做控制項層。
- activity.steps 的 UI。
- 行程↔活動 1:N（多方法各對不同控制項）。
- 受評對象/參與人員升級 link 表（B2）。

## 10. 稽核人員 = AP metadata.parties + 就地新增（2026-06-21 增補，取代「稽核人員=SSP parties」）

### 為什麼（OSCAL 依據，官方範例證實）
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。

### 落地（零新表，重用既有 metadata-scoped party 基礎設施）
- `oscal.assessment_plans.metadata_id`（1:1）已存在；`oscal.parties` / `oscal.roles` 以 `metadata_id` scope。
- **AP parties = `oscal.parties WHERE metadata_id = AP.metadata_id`**（= OSCAL AP.metadata.parties）。
- **受評對象**仍來自 import 的 SSP parties（受評方）；**稽核人員**來自 AP parties（稽核方）→ 兩池分離、不再混淆。
- per-task 選擇：沿用 `ap_task_participants(party_uuid, role_id="assessor")`（party_uuid 改指 AP party uuid）。
- 角色：確保 AP metadata 的 `oscal.roles` 有 `assessor`（建 AP draft 時 seed，或首次新增 party 時補）。

### 行為
- 稽核人員下拉 options = AP parties（list by ap.metadata_id）。
- **就地新增（inline quick-add）**：下拉內「＋新增」開小表單（姓名 / person·organization / 預設 role=assessor）
  → 建一筆 `oscal.parties`（metadata_id=AP.metadata_id）→ 回 uuid → 自動選入。**建真 party 拿 uuid，不存自由文字**。
- 重用 `ssp_party_app_service` 的 party CRUD 形態，只是 metadata_id 來源改 AP（mirror，不另造輪子）。

### OSCAL 匯出
`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 落地後切換。

## 11. 行程說明 → 每方法查核指引（Phase 7，activity.steps）

### 11.1 目標 / WHY
受評單位常需要的不是一句籠統說明，而是「**每個查核方法各自要看什麼、怎麼看**」，例：
> 文件檢查：檢視帳號權限總表、FORM4042640 帳號權限審查紀錄表，確認「一人一帳號」。
> 電腦設定比對：比對電腦使用者帳號設定，確認每項程序與帳號皆具唯一性。

把現在的單一多行「說明」(自由文字 Textarea，過渡 commit `0103997`) 升級成**結構化、依方法分段的查核指引**：受評方一看就知每個方法要準備/檢視什麼，且 OSCAL-native、可被匯出/報表/通知引用。

### 11.2 OSCAL 依據
`activity.steps[]`（required `uuid`；含 `title` / `description` / `props`…）。官方範例每 step = 一個查核程序步驟
（`{uuid, title, description}`）。本期把 **step ↔ 查核方法 1:1 綁定**：`step.props` 帶 `{name:"method", value:<method value>}`。

### 11.3 資料模型 / 儲存（零新表）
`ap_assessment_activities.steps` JSONB（欄位已存在）：
```jsonc
[
  { "uuid": "...", "title": "<method value/label>",
    "description": "<該方法的查核指引>",
    "props": [{ "name": "method", "value": "EXAMINE" }] }
]
```
- **method 清單**（`activity.props` name=method）仍是「這條行程用哪些方法」的權威來源（chip / 統計）。
- **steps** 是「每個方法的指引」。規則：**steps ⊆ methods**（只存「已選方法且有填指引」者；取消勾選的方法不存其 step）。
- `ap_tasks.description`（多行 Textarea）**保留**為「行程整體備註」（選填），與 per-method steps 並存、語意不同。

### 11.4 UI（RoundApAuthoringView.vue Tab3 行程卡身）
在「查核方法」下方新增一區 **「查核指引」**：依 `tk.methods` **動態列出每個已選方法一列**（方法 label + 該方法的指引 Textarea）。
- 未選方法 → 不顯示；勾選新方法 → 自動長出該列；取消 → 該列隱藏。
- FE 狀態：`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]`。
  - 取消勾選的方法其文字**留在記憶體 map**（重新勾回還在），存檔時只輸出 methods 內的。
- readonly 沿用既有。

### 11.5 API payload（PUT /ap/{apUid}/tasks 每筆 task 增欄）
```jsonc
"steps": [ { "method": "EXAMINE", "description": "檢視帳號權限總表…" }, … ]   // 只帶有指引的方法
```
- 讀回（GET …/ap 的 tasks）同形狀：`steps: [{method, description}]`（method 由 step.props 還原；title 由 FE i18n 出 label）。

### 11.6 BE 改動
- `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}]`。
- helpers：`_steps_from_payload(steps)` / `_steps_to_payload(activity_steps)`。
- route schema `_TaskItem` 增 `steps`（`fields.List(Nested(method, description))`，load_default None）。
- **零新表 / 零 migration**（用既有 `ap_assessment_activities.steps`）。

### 11.7 OSCAL 匯出
`activity.steps` 即 OSCAL steps（title/description/props）；匯出零轉換。

### 11.8 邊界 / 不做
- 一方法一指引（1:1）；不做多 step / 巢狀 step。
- step 的 `reviewed-controls` / `responsible-roles`（OSCAL 容許）本期不用。
- 不刪 `description`（整體備註保留）。

### 11.9 狀態
**實作完成（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`。
- 附帶同頁修補：查核指引方法標籤改 soft pill；受評對象 party 角色改走 `lang.oscal_role.ssp_party_role` i18n（原顯示 raw role-id）。

## 12. 查核指引下沉到 AO 層（評估後不做，定案維持 Phase 7 per-method）

> **定案（2026-06-21）：維持 Phase 7「每方法一條 textarea」為最終形態，不下沉到 AO 層、不加控制項維度。**
> 理由：per-method 已是 OSCAL 標準做法（`activity.steps` + `props` method，官方範例即如此），對齊報表
> 「檢查方式/檢查方法」結構；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 ③）。

### 12.1 WHY / 問題（已評估，定案不做，見上方 banner）
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）。

### 12.2 OSCAL 依據（AO scope 放 step）
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）。

### 12.3 資料模型（零新表，沿用 `ap_assessment_activities.steps` JSONB，每 step 加 AO scope）
```jsonc
// 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"] } ] } ] }
}
```
- method 權威來源仍是 `activity.props`（行程用哪些方法、chip/統計）；step 是「每 (AO,方法) 的指引」。
- 規則 **step ⊆ (行程選的控制項 × 各控制項 AO × 行程選的方法)**，且只存「有填指引」者。
- 控制層 fallback：無 AO 的控制項，step 的 `statement-ids` 省略（scope 退到 control-id），等同 Phase 7 行為。
- **向後相容**：Phase 7 已存的 method-only step（無 reviewed-controls）→ 載入時歸到「控制層」格，不壞；dev 重填即升級到 AO 層。

### 12.4 API
**AO 清單（新增，FE 畫 grid 用）**
`GET /ap/{apUid}/control-objectives` → `{ "<control_id>": [ { "ao_id":"ia-1_obj.a", "label":"[a]", "prose":"..." }, … ] }`
- 只回該 AP **reviewed-controls** 的控制項（grid 母體 = 行程能選的控制項）。
- BE 重用 AR 既有 AO 推導鏈：AP→凍結 SSP→專案 profile→catalog→`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 首段，擇一）。
- DI 待定：AP app service 目前無 `_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）
- 單條 payload 沿用 Phase 7 `_TaskItem` + `steps` 改成 `[{control_id, ao_id|null, method, description}]`。
- 既有全量 `PUT /ap/{apUid}/tasks`（set_tasks）保留為相容/批次入口；FE 改走逐列。

### 12.5 UI（RoundApAuthoringView.vue Tab3）
- 行程卡「查核指引」區改 grid：`v-for 控制項(tk.controls)` → `v-for AO(該控制項 objectives)` → `v-for 已選方法(tk.methods)` → 一格 Textarea。
  - AO 清單來自 `GET …/control-objectives`，只顯示 `tk.controls` 內控制項；無 AO 控制項退成單層（控制項 → 方法）。
  - 狀態：`tk.stepGuidance: Record<"<control_id>|<ao_id>|<method>", string>`（三元組 key）。
  - 取消勾選的控制項/方法其文字留記憶體，存檔只輸出在 (controls×AO×methods) 內者。
- 每張卡一顆「儲存此行程」（呼單列端點）+「刪除」；頂部保留「新增行程」。離開有未存變更提示（防誤丟）。
- readonly 沿用既有。

### 12.6 BE 改動
- `_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 驗套件**）。
- route + serializer：3 個單列端點 + `_TaskStepItem` 加 `control_id`/`ao_id`。

### 12.7 不在本期
- ③ AR per-AO matrix + 那份 per-AO Excel 報表（輸出端，另開 phase；本期讓資料變 AO 形狀備好源頭）。
- step 多巢狀、AO 多選方法各對不同子項。

### 12.8 狀態
**評估後定案不做（2026-06-21）**，維持 Phase 7 per-method。本節作為決策軌跡保留（含被排除原因 + 未來反悔條件）。
implementation-plan「Phase 8」同步標記暫不做。

## 13. AP 規劃頁改「行程驅動」單頁 + 範圍由行程回填（Phase 9，2026-06-21 定案）

### 13.1 WHY / 決策
原三頁籤（①受評控制項 ②受評對象 ③行程與方法）有重複輸入：先在 ①②勾母體，再在 ③從母體取子集。
洞察：**行程裡每條 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`。
- 採方案 1（唯讀保留），非完全移除。

### 13.2 必守 / 相依
- **`ap_reviewed_controls` 是 AR 驅動源**（`assessment_result_app_service._inscope_control_ids` → AR 矩陣初始 / close-round 覆核 / 判定頁）。
  → `set_tasks` **必須**回填，否則 AR 拿不到 scope。回填後 downstream 全部零改動。
- **前置條件位移**：原「AP 必須先設 reviewed-controls（空擋 `GRC_AP_REVIEWED_CONTROLS_REQUIRED`）」改由 set_tasks 回填滿足；
  set_tasks 內部回填走 domain `self._ap.set_reviewed_controls`（**不套空值擋**，允許 WIP 存無控制項的行程），
  stage-advance 既有 gate 仍在真正進階時擋空 scope。
- **語意改變**：在 scope = 一定在某條行程裡（無「在範圍但未排程」狀態）。符合「範圍＝排定要查的」流程。

### 13.3 落地
- **BE**（`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`。
- **FE**（`RoundApAuthoringView.vue`）：
  - TabPanel 改序（行程→控制項→對象）。
  - 任務控制項下拉母體 = **完整控制樹**（`groups`，去除 `selected` 過濾）；受評對象下拉母體 = **完整 SSP 對象**（`subjectRegistry` 全集，去除 `subjectSel` 過濾）。
  - ②③改唯讀：顯示由 `tasks[]` 即時推導的控制項/對象聯集（移除勾選 UI + save 按鈕）。
- 零新表、零 migration（重用既有 reviewed-controls/subjects 表，只是改成由行程回填）。

### 13.4 狀態
**實作完成並上線（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`。
