# FR-038 OSCAL GRC 重設計 — API 契約（planning contract）

> 範圍：**規劃層 API 契約**。把 design.md §5 路由骨架展開成「逐端點 request / response / 權限 / error code」，
> 讓 **FE（Wave 3）可與 BE 遷移（Wave 2）平行開工** —— FE 對著本契約刻畫面，不用等 BE 寫完。
>
> **本檔不是各模組正式 api-spec.md**。各模組 `docs/api/<模組>/api-spec.md` 的權威更新在 Wave 2 各 sub-project 執行時做；
> 本契約若與 Wave 2 實作出現偏差，以實作 + 該模組 api-spec.md 為準（屆時回頭同步本檔的偏差註記）。
>
> 上游基準：[design.md](design.md)（§3 engagement 模型 / §4 套件介面 / §5 路由骨架 / §6 error code）、
> [requirement-analysis.md](requirement-analysis.md)（§3.3 輪次 7 態 / §4.5-4.7 AP/AR/POA&M / §5 SSP→OSCAL / §6 FE 影響）、
> [oscal-v2-deltas.sql](oscal-v2-deltas.sql)（欄位 / enum 值域）。
> 撰寫日：2026-06-14

---

## 0. 全域慣例（每個端點預設遵守，不再逐條重述）

| 面向 | 規則 |
|------|------|
| **Envelope（成功）** | `{ "code": 1, "data": ... }`（BE Swagger 寫 `code`；jedi-common 對 FE 統一成 `status:true`）|
| **Envelope（失敗）** | `{ "code": 0, "msg": "...", "data": {...} }`，HTTP status code 對齊 error class |
| **單筆 GET / 單筆寫入** | `data` 直接放結果物件，**不要再包一層** |
| **分頁列表** | **POST + body**：`{ pager:{page,page_size,with_total}, sort:[{field,order}], filters:{...} }`；response `data` 是陣列，**`meta` 在頂層**（`{page,page_size,total,total_pages,has_next,has_prev,paging}`）|
| **URL 命名** | kebab-case；單筆操作單數、清單/批次/collection 複數 |
| **審計欄位** | response 帶 `created_user`/`updated_user`（login_name）時，**必同時帶** `created_user_name`/`updated_user_name`（nickname），app service 層 batch enrich |
| **assignee/負責人欄位** | 帶 `assignee_user_id` 必 enrich `assignee_user_name`(nickname)+`assignee_login_name`；帶 `assignee_org_unit_id` 必 enrich `assignee_org_unit_name`（不曝光內部 id 以外資訊用名稱）|
| **權限檢查** | 在 app service 層透過 domain service 查 participant.role；route 不查 DB |
| **locale** | 有翻譯欄位的 entity update 必傳 locale（HTTP：`str(get_locale())`）|
| **時間欄位** | ISO 8601 字串（`created_at` / `updated_at` / `start_at` / `target_date` 等）|

**標記圖例**：**[沿用]** = 現有 contract 不動（只列關鍵端點）；**[新]** = 全新端點；**[改]** = 既有端點 shape / 行為變更。

**路由 prefix 假設**：OSCAL 物件類 `/oscal/...`；產品業務（輪次 / 專案）類沿用現有 `/projects/...`、`/oscal-project/...`。各 sub-project 落地時對齊既有 blueprint，本契約以相對語意路徑表達。

---

## 1. 合規框架 Framework（**不大改** — 底層 schema rename，API 對齊）

> sub-project BE-B1。FE 100% 現成。本期變動：`oscal_frameworks→frameworks`、`oscal_framework_versions→framework_versions`、catalog 綁 `framework_version_id`、發布狀態 `publish_status`。
> 套件對應：`FrameworkService` / `CatalogService`（design §4.2）。

| 端點 | 標記 | 權限 | 說明 |
|------|------|------|------|
| `POST /oscal/frameworks/list` | [沿用] | 全參與者 | 分頁列表，filters 支援 `code`/`publish_status`/`search` |
| `GET /oscal/framework/{uid}` | [沿用] | 全參與者 | 框架詳情（含 versions 摘要）|
| `POST /oscal/frameworks` | [沿用] | manager | 建框架 |
| `PUT /oscal/framework/{uid}` | [沿用] | manager | 改框架 |
| `POST /oscal/framework-versions/import` | [改] | manager | 匯入 PDF/Excel → **標準 catalog/group/control/part(AO) 結構**（落 v2 schema）|
| `POST /oscal/framework/{uid}/publish` | [新] | manager | 發布版本（`publish_status: draft→published`）；資源庫只能選 published 版本 |

**`POST /oscal/frameworks` request**（[沿用]，欄位對齊新 schema）
```jsonc
{ "code": "CMMC_L2", "name": "CMMC 2.0 Level 2", "authority": "DoD", "description": "..." }
```
**response `data`**：`{ uid, code, name, authority, publish_status, main_version_id, created_at, created_user, created_user_name }`

**`POST /oscal/framework-versions/import` request**（[改]，multipart）
```
file: <PDF/Excel>
framework_uid: <str>      // 掛在哪個框架
version: "2.0"            // 版本字串
parser_code: "CMMC_L2"    // 對應 parser adapter（design §4.3）
```
**response `data`**：`{ framework_version_uid, version, catalog_uid, publish_status:"draft", control_count, group_count, ao_count }`
（→ 套件 `CatalogService.import_catalog_from_pdf` + `FrameworkService.add_framework_version`，匯入後回填 `framework_versions.catalog_id`）

**`POST /oscal/framework/{uid}/publish` request**：`{ "framework_version_uid": "<str>" }`
**response `data`**：`{ framework_version_uid, publish_status:"published", published_at }`（→ `FrameworkService.publish_version`）

**key error**：`GRC_FRAMEWORK_VERSION_NOT_FOUND`(GRC_404032)、`GRC_IMPORT_INVALID_FILE`(GRC_400001)、`GRC_IMPORT_FILE_TOO_LARGE`(GRC_400002)、`GRC_NOT_MANAGER`(GRC_403002)。

---

## 2. 合規資源庫 Resource Library（**不大改** — 語意調整成三件組）

> sub-project BE-B1。FE 100% 現成。新定位：**catalog 副本 + profile + SSP 範本 三件組**，由稽核顧問維護。
> 套件對應：`OscalSnapshotService.clone_resource_library`（邊界②/①）、`OscalIoService.import_ssp_docx/excel`、`ProfileService.resolve_profile`。

| 端點 | 標記 | 權限 | 說明 |
|------|------|------|------|
| `POST /oscal/resource-libraries/list` | [沿用] | 全參與者 | 分頁列表 |
| `GET /oscal/resource-library/{uid}` | [沿用] | 全參與者 | 三件組詳情（catalog/profile/ssp uid + 控制數摘要）|
| `POST /oscal/resource-libraries` | [改] | manager | 建資源庫：選 `framework_version` → snapshot catalog 副本（邊界①）+ 依 profile resolve 納入控制集 |
| `POST /oscal/resource-library/{uid}/import-ssp` | [沿用] | manager | 匯入 SSP docx/excel（落 v2 schema；UI/流程沿用 `SspImportDialog`/`ssp-docx-import-v2`）|
| `POST /oscal/resource-library/{uid}/publish` | [新] | manager | 發布資源庫（專案成立只能選 published）|

**`POST /oscal/resource-libraries` request**（[改]）
```jsonc
{
  "name": "CMMC L2 標準稽核範本",
  "framework_version_uid": "<str>",   // 選哪個 published framework_version → snapshot catalog
  "description": "...",
  "profile": {                        // baseline 定義：納入哪些 control / AO
    "include_all": false,
    "include_controls": ["AC.L2-3.1.1", "AC.L2-3.1.2", "..."]  // 或依 profile import 規則
  }
}
```
**response `data`**：`{ uid, name, catalog_uid, profile_uid, ssp_template_uid, framework_version_uid, resolved_control_count, publish_status:"draft", created_user, created_user_name }`
（→ `ProfileService.resolve_profile`（邊界①）→ `OscalSnapshotService.clone_resource_library`（邊界②建三件組副本））

**key error**：`GRC_FRAMEWORK_VERSION_NOT_FOUND`(GRC_404032)、`GRC_EXCEL_INVALID_FILE`(GRC_400065)、`GRC_NOT_MANAGER`(GRC_403002)。

---

## 3. 專案 Project（**不大改列表/建立；專案成立流程大改**）

> sub-project BE-B2。FE 列表/建立/總覽 100% 現成。

### 3.1 列表 / 總覽（[沿用]）

| 端點 | 標記 | 權限 | 說明 |
|------|------|------|------|
| `POST /projects/list` | [沿用] | 全參與者 | 專案分頁列表（加上 OSCAL AP 統計視角）|
| `GET /project/{uid}` | [沿用] | 參與者 | 專案總覽（基本資訊 + 參與者 + 輪次摘要）|
| `POST /projects` 或既有建立路由 | [沿用] | manager | 建專案主檔基本資訊（不含 clone；clone 由 §3.2 start 觸發）|

### 3.2 專案成立 `POST /oscal-project/start`（**[改] — 重點全寫**）

> 對齊現有 `OscalProjectStartRoute`。**核心變更（requirement §4.3 / Q1）**：
> - 起點從「MF defaults + template SSP 兩套」改為「**clone 資源庫三件組脫鉤**（邊界②）」。
> - **AP/AR 延後**：不在 start 當場建（改由輪次 `launch-audit` 才建）。
> - **為 SSP 控制項建 job per Q1**：workflow/job 綁定點從「AP task」改為「**專案 SSP 控制項**」，且在 start 建好。

**權限**：manager / 專案 owner。

**request**
```jsonc
{
  "name": "精誠機械 CMMC L2 導入",
  "resource_library_uid": "<str>",   // 選哪個 published 資源庫 → clone 三件組
  "basic_info": {                    // 沿用 ProjectBasicInfoForm 欄位
    "client_name": "...", "framework_code": "CMMC_L2", "description": "...",
    "owner_user_id": 123, "start_date": "2026-06-20", "target_date": "2026-12-31"
  },
  "participants": [                  // 角色：manager / auditor / participant
    { "user_id": 123, "role": "manager" },
    { "user_id": 456, "role": "auditor" },
    { "user_id": 789, "role": "participant" }
  ]
}
```

**response `data`**
```jsonc
{
  "project_uid": "<str>",
  "catalog_uid": "<str>",            // 專案 catalog 副本（★單一真相來源）
  "profile_uid": "<str>",
  "ssp_uid": "<str>",                // living SSP（clone 自資源庫範本）
  "control_count": 110,
  "job_count": 110,                  // 每個 SSP 控制項一個 job（Q1）
  "ap_created": false,               // AP/AR 延後到 launch-audit
  "ar_created": false,
  "participants": [ { "user_id":123,"role":"manager","user_name":"王經理","login_name":"wang" }, ... ],
  "created_user": "wang", "created_user_name": "王經理"
}
```
（→ `OscalSnapshotService.clone_resource_library`（邊界②）；job 建立用主專案 jedi-flow-engine 綁 SSP 控制項。）

**contract 判斷（ambiguity → 見 §11-A）**：路徑用 `/oscal-project/start`（對齊現有 route），未改成 `/projects/{uid}/start`，避免破壞既有 FE 呼叫點。

**key error**：`GRC_PROJECT_NAME_DUPLICATED`(GRC_409005)、resource-library not found（沿用資源庫 not-found 碼）、`GRC_NOT_MANAGER`(GRC_403002)。

---

## 4. 稽核輪次 Project Audit Rounds（**[新] — 全新，重點全寫**）

> sub-project BE-B3。對應 `compliance.project_audit_rounds`（oscal-v2-deltas D-2）。
> **狀態機留主專案**（design §4.4）；snapshot 呼叫套件 `OscalSnapshotService.snapshot_ssp`（邊界③）。
> FE：`RoundSwitcherBar` 既有，改接本組端點（requirement §6「輪次切換」）。

### 4.0 值域（DB CHECK 強制，FE 直接用）

| 欄 | 允許值 |
|----|--------|
| `round_type` | `initial` / `close-out` / `surveillance` |
| `status`（7 態）| `not_started` / `planning` / `audit_planning` / `auditing` / `remediation` / `pending_reverify` / `closed` |
| 不變式 | `parent_round_id` NOT NULL **⟺** `round_type='close-out'` |

**狀態機流轉（FE 用來決定按鈕可用性）**
```
not_started ─[建立輪次]──────────────────▶ planning            (受評方編 living SSP)
planning    ─[launch-audit, snapshot SSP]─▶ audit_planning      (稽核員寫 AP)
audit_planning ─[開始稽核]───────────────▶ auditing            (填 AO 判定/finding/risk)
auditing    ─[AR 定版]─┬ 無 not_met ─────▶ closed              (直接結案)
                       └ 有 not_met ─────▶ remediation         (系統自動生 POA&M)
remediation ─[整改完+更新 SSP]───────────▶ pending_reverify
pending_reverify ─[launch-reverify]──────▶ (開新 close-out round, status=audit_planning, parent=本輪)
```

### 4.1 端點清單

| 端點 | 標記 | 權限 | 說明 |
|------|------|------|------|
| `POST /projects/{project_uid}/audit-rounds` | [新] | manager | 建輪次（initial / surveillance）|
| `POST /projects/{project_uid}/audit-rounds/list` | [新] | 參與者 | 輪次分頁/清單（含血緣）|
| `GET /audit-round/{round_uid}` | [新] | 參與者 | 輪次詳情 |
| `POST /audit-round/{round_uid}/launch-audit` | [新] | manager | 啟動稽核：snapshot living SSP（邊界③）→ 回填 `ssp_id`、建/沿用 AP（engagement）、轉 `audit_planning` |
| `POST /audit-round/{round_uid}/launch-reverify` | [新] | auditor | 發起覆核：開新 `close-out` round（`parent_round_id`=本輪），重新 snapshot 當下 SSP |
| `POST /audit-round/{round_uid}/start-auditing` | [新] | auditor | `audit_planning → auditing`（AP 草稿補完後開始填判定）|
| `POST /audit-round/{round_uid}/close` | [新] | manager | 結案（→ §8 close-round；所有 POA&M closed 才可）|

> **判斷（§11-B）**：狀態轉換用獨立動詞端點（`launch-audit`/`launch-reverify`/`start-auditing`/`close`）而非單一 `PUT status`，因每個轉換有不同前置條件 + 副作用（snapshot / 建 AP / 開新 round），語意明確且權限分流。

### 4.2 建輪次 `POST /projects/{project_uid}/audit-rounds`

**request**
```jsonc
{
  "name": "首次稽核",
  "round_type": "initial",       // initial / surveillance（close-out 不由此端點建，走 launch-reverify）
  "start_at": "2026-07-01"       // 選填
}
```
**response `data`**
```jsonc
{
  "uid": "<str>", "round_no": 1, "name": "首次稽核",
  "round_type": "initial", "status": "planning",
  "parent_round_id": null, "parent_round_uid": null,
  "ssp_id": null, "ssp_uid": null,
  "assessment_plan_id": null, "ap_uid": null,
  "ar_result_id": null,
  "start_at": "2026-07-01", "end_at": null,
  "created_user": "wang", "created_user_name": "王經理"
}
```

> **判斷（§11-C）**：response 同時帶內部 `*_id`（FK，FE debug 用）與對外 `*_uid`。FE 慣例以 uid 操作；id 為唯讀傳遞。各 sub-project 可只回 uid，本契約兩者皆列以免歧義。

**key error**：`GRC_ROUND_INVALID_ROUND_TYPE`（建 close-out 走錯端點 → 412）、`GRC_NOT_MANAGER`(GRC_403002)。

### 4.3 啟動稽核 `POST /audit-round/{round_uid}/launch-audit`

**前置**：round.status = `planning`。
**副作用**：snapshot 當前 living SSP（邊界③）→ 回填 `ssp_id`（凍結不可改）；engagement 為 initial/surveillance → 新建 AP（`import_ssp_id`=快照）+ AR；轉 `audit_planning`。
**request**：`{}`（或 `{ "name": "稽核計畫-第1輪" }` 帶給自動生成的 AP 標題）
**response `data`**
```jsonc
{
  "uid": "<str>", "status": "audit_planning",
  "ssp_uid": "<frozen-snapshot-uid>",   // 凍結快照
  "ap_uid": "<str>",                     // engagement AP（自動生草稿，見 §5）
  "ar_uid": "<str>",
  "ar_result_id": null                   // 開始填判定時才建 result
}
```
（→ `OscalSnapshotService.snapshot_ssp` + `AssessmentPlanService.create_ap` + `generate_draft_from_ssp` + `AssessmentResultService.add_ar`）

**key error**：`GRC_ROUND_INVALID_STATUS_TRANSITION`(GRC_412031)、`GRC_ROUND_SSP_NOT_FROZEN`(GRC_412033，理論上 launch 才凍結，反向誤呼叫防呆)、`GRC_NOT_MANAGER`(GRC_403002)。

### 4.4 發起覆核 `POST /audit-round/{round_uid}/launch-reverify`

**前置**：母輪 status = `pending_reverify`。
**副作用**：開新 `close-out` round（`parent_round_id`=母輪、`round_type='close-out'`、沿用母輪 AP/AR）；重新 snapshot 當下 living SSP（PM 教材：沿用舊快照是 bug）；新 round status = `audit_planning`；narrowed reviewed-controls 從母輪 not_met findings 推導。
**request**：`{ "name": "複驗-第2輪" }`（選填）
**response `data`**：新 round 物件（同 §4.2 shape，`round_type:"close-out"`、`parent_round_uid` 指母輪、`ssp_uid` 為新快照、`ap_uid` 沿用母輪 AP）。
（→ 新增一筆 `ar_results`（自帶 narrowed scope）；`AssessmentResultService.add_result`）

**key error**：`GRC_REVERIFY_PARENT_NOT_PENDING`(GRC_412032，母輪不在 pending_reverify)、`GRC_NOT_AUDITOR`(GRC_403001)。

---

## 5. AP 稽核計畫 Assessment Plan（**[新] — 全新畫面，重點全寫**）

> sub-project BE-B4 / FE-C1。requirement §4.5。對應 `assessment_plans` + `ap_reviewed_controls` + `ap_assessment_subjects`(D-3) + `ap_tasks`。
> 套件對應：`AssessmentPlanService`。AP 回答四件事，只①必填（②③④稽核員臨場補）。

| 端點 | 標記 | 權限 | 說明 |
|------|------|------|------|
| `GET /audit-round/{round_uid}/ap` | [新] | 參與者 | 取本輪 engagement 的 AP（草稿/詳情）|
| `POST /audit-round/{round_uid}/ap/generate-draft` | [新] | auditor | 重新從 SSP 快照生草稿（launch-audit 已自動生一次；此為手動重生）|
| `PUT /ap/{ap_uid}/reviewed-controls` | [新] | auditor | ①設定查哪些控制（必填）|
| `GET /ap/{ap_uid}/assessment-subjects` | [新] | 參與者 | 抽查名單（含 SSP 快照可選資產）|
| `PUT /ap/{ap_uid}/assessment-subjects` | [新] | auditor | ②設定抽查名單（subject_type/subject_uuid/include）|
| `PUT /ap/{ap_uid}/tasks` | [新] | auditor | ③④行程 + 方法 |

### 5.1 AP 詳情 `GET /audit-round/{round_uid}/ap`

**response `data`**
```jsonc
{
  "uid": "<str>", "title": "稽核計畫-第1輪",
  "import_ssp_uid": "<frozen-snapshot-uid>",   // 對著哪份凍結快照（AP 鐵則）
  "reviewed_controls": [                         // ① 自動草稿來自 SoA 適用集
    { "control_id": "AC.L2-3.1.1", "title": "...", "included": true }
  ],
  "assessment_subjects": [                       // ② 見 §5.3
    { "uid":"...", "subject_type":"inventory-item", "subject_uuid":"...", "title":"DC-01 網域控制器", "include": true }
  ],
  "tasks": [                                     // ③④ 行程 + 方法
    { "uid":"...", "title":"文件審查", "timing":"2026-07-10", "methods":["EXAMINE","INTERVIEW","TEST"] }
  ],
  "created_user": "auditor1", "created_user_name": "陳稽核"
}
```

### 5.2 設定 reviewed-controls `PUT /ap/{ap_uid}/reviewed-controls`
**request**：`{ "control_ids": ["AC.L2-3.1.1", "AC.L2-3.1.2", "..."] }`
**response `data`**：`{ "ap_uid":"...", "reviewed_control_count": 95 }`（→ `set_reviewed_controls`）
**key error**：`GRC_AP_REVIEWED_CONTROLS_REQUIRED`(GRC_412034，空清單)、`GRC_NOT_AUDITOR`(GRC_403001)。

### 5.3 設定抽查名單 `PUT /ap/{ap_uid}/assessment-subjects`
> 全量覆寫（FE 送整份名單）。`subject_type` ∈ `component / inventory-item / location / party / user`（design §7）。
**request**
```jsonc
{
  "subjects": [
    { "subject_type":"inventory-item", "subject_uuid":"<ssp-snapshot-obj-uuid>", "include": true,  "title":"DC-01" },
    { "subject_type":"party",          "subject_uuid":"<party-uuid>",            "include": true,  "title":"資安長" },
    { "subject_type":"user",           "subject_uuid":"<...>",                   "include": false }
  ]
}
```
**response `data`**：`{ "ap_uid":"...", "subjects":[ {uid, subject_type, subject_uuid, include, title, sort_order}, ... ] }`（→ `set_assessment_subjects`）

### 5.4 設定 tasks（行程 + 方法）`PUT /ap/{ap_uid}/tasks`
**request**
```jsonc
{ "tasks": [ { "title":"現場訪談", "timing":"2026-07-12", "methods":["INTERVIEW","EXAMINE"], "description":"..." } ] }
```
**response `data`**：`{ "ap_uid":"...", "tasks":[ {uid, title, timing, methods, sort_order}, ... ] }`（→ `set_tasks`）

---

## 6. AR 稽核結果 Assessment Result（**[改] — 中改：AO 全量矩陣 + 風險總結，重點全寫**）

> sub-project BE-B5 / FE-C2。requirement §4.6。判定下沉到 **AO 層**（Q3）：每個 in-scope AO 一筆 finding（`met`/`not_met`/`pending`）。
> 套件對應：`AssessmentResultService`（finding 全量矩陣）+ `AssessmentRiskService`（風險總結，Q2 多對多）。

### 6.0 值域

| 欄 | 允許值 |
|----|--------|
| finding `state`（target_status_state） | `met`(satisfied) / `not_met`(not-satisfied) / `pending`(未判定) |
| risk `severity`（等級，risk 層判定） | `low` / `medium` / `high` / `critical` |
| risk `status`（CMMC 允許集，design §7）| `open` / `investigating` / `remediating` / `closed`（ISO 的 deviation/risk_accepted 本期不 wire）|

### 6.1 端點清單

| 端點 | 標記 | 權限 | 說明 |
|------|------|------|------|
| `GET /audit-round/{round_uid}/ar/findings` | [改] | 參與者 | **AO 全量判定矩陣**（以控制項為導覽單位、展開到 AO）+ stats 分母 |
| `PUT /ar-finding/{finding_uid}` | [改] | auditor | 單 AO upsert 判定（met/not_met/pending）|
| `POST /ar-result/{ar_result_uid}/observations` | [新] | auditor | 建 observation（看到什麼，好壞都記；引用證據不複製）|
| `POST /audit-round/{round_uid}/ar/risks` | [新] | auditor | **建系統風險**（風險等級在 risk 層）|
| `GET /audit-round/{round_uid}/ar/risks` | [新] | 參與者 | 風險總結列表（含關聯 finding 數）|
| `PUT /ar-risk/{risk_uid}/findings` | [新] | auditor | **組風險**：勾選哪幾條 finding（多對多）|
| `POST /audit-round/{round_uid}/ar/finalize` | [改] | auditor | **AR 定版**：判定完整性檢查 → 追加一筆 `ar_results` → 無 not_met 結案 / 有 not_met 轉 remediation |

### 6.2 AO 全量矩陣 `GET /audit-round/{round_uid}/ar/findings`

**response `data`**（以控制項為導覽單位，每控制項展開其 AO findings）
```jsonc
{
  "ar_result_uid": "<str>",
  "stats": {                                  // 分母 = in-scope AO 總數
    "total_ao": 320, "met": 280, "not_met": 25, "pending": 15,
    "controls_total": 95, "controls_with_not_met": 18
  },
  "controls": [
    {
      "control_id": "AC.L2-3.1.1", "title": "...",
      "ao_findings": [
        {
          "finding_uid": "<str>",
          "ao_id": "AC.L2-3.1.1[a]",          // catalog_control_parts (AO) 的識別
          "ao_statement": "determine if ...",
          "state": "met",                      // met / not_met / pending
          "observation_uid": null,
          "updated_user": "auditor1", "updated_user_name": "陳稽核"
        }
      ]
    }
  ]
}
```
（→ `AssessmentResultService.list_findings`；finding 掛 `catalog_control_parts`(AO)）

### 6.3 單 AO 判定 `PUT /ar-finding/{finding_uid}`
**request**：`{ "state": "not_met", "description": "未發現密碼複雜度政策", "observation_uid": "<opt>" }`
**response `data`**：`{ finding_uid, ao_id, state, description, observation_uid, updated_user, updated_user_name }`（→ `upsert_finding`）
**key error**：`GRC_AR_RESULT_NOT_LATEST`(GRC_412008 既有 `GRC_AR_NOT_LATEST_ROUND`)、`GRC_NOT_AUDITOR`(GRC_403001)。

### 6.4 建風險 `POST /audit-round/{round_uid}/ar/risks`
**request**
```jsonc
{ "title":"存取控制缺口", "description":"...", "severity":"high", "status":"open" }
```
**response `data`**：`{ uid, title, description, severity, status, linked_finding_count:0, created_user, created_user_name }`（→ `AssessmentRiskService.add_risk`）

### 6.5 組風險（多對多）`PUT /ar-risk/{risk_uid}/findings`
> 全量覆寫該 risk 的 finding 關聯（`oscal.assessment_finding_risks`）。
**request**：`{ "finding_uids": ["<not_met-finding-1>", "<not_met-finding-2>"] }`
**response `data`**：`{ risk_uid, linked_findings: [ {finding_uid, ao_id, state}, ... ] }`（→ `AssessmentRiskService.link_findings`）
**key error**：`GRC_RISK_NO_FINDING_LINKED`(GRC_412037，定版前 risk 必須至少關聯一條 finding)、`GRC_NOT_AUDITOR`(GRC_403001)。

### 6.6 AR 定版 `POST /audit-round/{round_uid}/ar/finalize`
**前置**：所有 in-scope AO 有判定（無 pending）；每個 not_met finding 有 finding 紀錄；每個 risk 至少關聯一條 finding。
**副作用**：追加 `ar_results`（不改舊的）；無 not_met → round 轉 `closed`；有 not_met → round 轉 `remediation` + 自動生 POA&M（§7）。
**request**：`{}`
**response `data`**
```jsonc
{
  "ar_result_uid": "<str>", "round_status": "remediation",  // 或 "closed"
  "not_met_count": 25, "poam_generated": true, "poam_item_count": 18
}
```
（→ `AssessmentResultService.add_result` + `PoamService.generate_from_findings`）

**key error**：`GRC_VERDICT_INCOMPLETE`(GRC_412003，有 pending)、`GRC_FINDING_REQUIRED_FOR_FAILED`(GRC_412011)、`GRC_RISK_NO_FINDING_LINKED`(GRC_412037)、`GRC_AR_RESULT_NOT_LATEST`(GRC_412008)、`GRC_NOT_AUDITOR`(GRC_403001)。

---

## 7. POA&M 改善計劃（**[改] — 小改：milestone+assignee / 180 天 / 三層整改**）

> sub-project BE-B5 / FE-C3。requirement §4.7。FE 100% 現成（小改）。
> 三層忠實落地：`risk → assessment_remediations(response) → poam_milestones(task)`（D-4b）。POA&M item 用 FK 指回 AR，不複製（PM1）。
> 套件對應：`PoamService`。

| 端點 | 標記 | 權限 | 說明 |
|------|------|------|------|
| `POST /audit-round/{round_uid}/poam-items/list` | [改] | 參與者 | POA&M item 分頁列表（含 180 天告警欄）|
| `GET /poam-item/{item_uid}` | [改] | 參與者 | POA&M item 詳情（封面 + 連著的 risk/remediation/milestone）|
| `POST /poam-item/{item_uid}/remediations` | [新] | manager | 建整改計畫（response；CMMC 可自動帶預設一筆）|
| `POST /remediation/{remediation_uid}/milestones` | [新] | manager | 加里程碑（per-milestone assignee）|
| `PUT /poam-milestone/{milestone_uid}` | [新] | manager | 改里程碑（狀態 / assignee / target_date）|
| `POST /audit-round/{round_uid}/close-round` | [新] | manager | 結案（→ §8）|

### 7.1 POA&M item 列表 `POST /audit-round/{round_uid}/poam-items/list`
**response `data`**（陣列）
```jsonc
[
  {
    "uid": "<str>", "title": "存取控制缺口", "status": "open",
    "risk_uid": "<str>", "severity": "high",
    "target_date": "2026-09-15",
    "days_remaining": 93,          // 180 天追蹤
    "deadline_warning": false,     // CMMC 180 天逼近告警（PM4）
    "milestone_count": 3, "milestone_done": 1
  }
]
```
**`deadline_warning` 判斷（§11-D）**：`days_remaining <= 30`（或最早 target_date 逾期）為 true；確切門檻 Wave 2 定，FE 只讀此布林。

### 7.2 整改計畫 `POST /poam-item/{item_uid}/remediations`
**request**：`{ "title":"整改計畫", "description":"...", "lifecycle":"planned" }`
**response `data`**：`{ uid, risk_uid, title, description, lifecycle, milestone_count:0 }`（→ `upsert_remediation`）

### 7.3 加里程碑 `POST /remediation/{remediation_uid}/milestones`
**request**
```jsonc
{
  "title": "M1 部署密碼政策",
  "description": "...",
  "assignee_user_id": 456,        // per-milestone 負責人（跨部門可拆）
  "assignee_org_unit_id": 12,
  "target_date": "2026-08-20",
  "task_type": "milestone"
}
```
**response `data`**
```jsonc
{
  "uid": "<str>", "title": "M1 部署密碼政策", "status": "open",
  "assignee_user_id": 456, "assignee_user_name": "李工程師", "assignee_login_name": "lee",
  "assignee_org_unit_id": 12, "assignee_org_unit_name": "資訊部",
  "target_date": "2026-08-20", "task_type": "milestone"
}
```
（→ `add_milestone` → `oscal.poam_milestones`）

### 7.4 改里程碑 `PUT /poam-milestone/{milestone_uid}`
**request**：`{ "status":"done", "assignee_user_id":789, "target_date":"2026-08-25" }`（部分欄位）
**response `data`**：同 §7.3 shape（含 `completed_at` 當 status=done）。
**值域**：milestone `status` ∈ `open` / `in_progress` / `done`。
**key error**：`GRC_POAM_NOT_FOUND`(GRC_404019)、`GRC_POAM_INVALID_STATUS_TRANSITION`(GRC_412006)、`GRC_NOT_MANAGER`(GRC_403002)。

---

## 8. 結案 / 覆核連動 Close Round（**[新]**）

> sub-project BE-B5。requirement §4.8。

`POST /audit-round/{round_uid}/close-round`
**前置**：本輪所有 POA&M item closed（`GRC_POAM_NOT_ALL_CLOSED`(GRC_412005)）；本輪 status = `remediation` 或 `pending_reverify`。
**行為差異**：
- **母輪（initial/surveillance）**：整改完成 → 轉 `pending_reverify`（不能自宣告通過，等覆核）。
- **close-out 輪定版**：確認母輪 not_met AO 過了 → 沿 `parent_round_id` 回頭關閉**母輪** POA&M → **母輪轉 `closed`**（跨 round 連動，design §3）。

**request**：`{}`
**response `data`**
```jsonc
{
  "round_uid": "<str>", "status": "pending_reverify",   // 或 "closed"（close-out 連動）
  "parent_round_closed": false,                          // close-out 連動關閉母輪時 true
  "parent_round_uid": null
}
```

**key error**：`GRC_POAM_NOT_ALL_CLOSED`(GRC_412005)、`GRC_ROUND_INVALID_STATUS_TRANSITION`(GRC_412031)、`GRC_NOT_MANAGER`(GRC_403002)。

---

## 9. SSP 維護（**不大改** — OSCAL 落點對齊 + 新增「啟動稽核」動作）

> sub-project BE-B3。FE 100% 現成（7 tabs）。requirement §4.4 + §5。
> SSP 維護端點 **[沿用] 現有 contract**（system-characteristics / parties / components / inventory / implemented-requirements / by-components / SoA props），僅落地資料對齊 v2 schema + UI→OSCAL 對應（§5 表）。
> 套件對應：`SspService` 子物件 CRUD。

| 端點群 | 標記 | 說明 |
|--------|------|------|
| `GET/PUT /ssp/{uid}/system-characteristics` | [沿用] | 受評標的 → `system-characteristics` |
| `GET/PUT /ssp/{uid}/parties` | [沿用] | 人員/單位 → `parties`+`roles` |
| `GET/POST/PUT/DELETE /ssp/{uid}/components` | [沿用] | 元件 |
| `GET/POST/PUT/DELETE /ssp/{uid}/inventory-items` | [沿用] | 資產清冊 |
| `GET/PUT /ssp/{uid}/implemented-requirements/{control_id}` | [沿用] | 控制實作 + SoA（props.applicability / inclusion-justification / implementation-status；statements[].by-components[]）|
| SSP docx/excel 匯入 | [沿用] | `ssp-docx-import-v2` 流程 |

> **「啟動稽核 → snapshot」動作**不在 SSP 模組，而在輪次 §4.3 `launch-audit`（snapshot 後 living SSP 仍可改）。FE 在 SSP 頁 header 放「啟動稽核」按鈕，呼叫 §4.3。

---

## 10. 任務執行 My Jobs（**不大改** — 綁定點後端改，FE 不動）

> requirement §4.3b / §6（Q1）。FE 100% 現成、**畫面不動**。
> 變更：workflow/job 綁定點後端從「AP task」改為「**專案 SSP 控制項**」，且在專案成立（§3.2）就建。稽核員 Phase 3 走 AR 判定（§6），**不**走 job 引擎。

| 端點 | 標記 | 說明 |
|------|------|------|
| `POST /my-jobs/list`（或既有 SP `get_user_task_queue`）| [沿用] | 我的任務佇列 |
| `GET /job-execution/{uid}` + 留言 / 證據子端點 | [沿用] | 任務執行詳情 |

---

## 11. OSCAL 匯出 Export（**[新]**）

> sub-project BE-B5。套件對應：`OscalIoService.export_oscal(doc_type, uid, fmt)`。

`GET /oscal/export/{doc_type}/{uid}?fmt=json`
**path**：`doc_type` ∈ `catalog` / `ssp` / `assessment-results` / `poam`；`uid` = 對應 OSCAL 物件 uid。
**query**：`fmt` ∈ `json`（本期主力）/ `xml` / `yaml`。
**權限**：參與者（manager/auditor 皆可匯出本專案文件）。
**response**：標準 OSCAL v1.2.2 文件（`Content-Type: application/json` 直接回 OSCAL JSON body，**不包 envelope** —— 因為要餵外部工具）。
> **判斷（§11-E）**：匯出端點回**裸 OSCAL JSON 不包 `{code,data}`**，與其他端點不同；FE 下載時直接存檔。各 sub-project 落地時於 api-spec.md 標明此例外。
**key error**：對應 doc not found（沿用各模組 not-found 碼，包在 envelope 回 404）。

---

## 12. 新增 Error Code 規劃（待 Wave 2 開工前複查最大序號續編）

> 命名 `GRC_<HTTP><3位序號>`。**現有最大值**（2026-06-14 grep）：404→`404036`、412→`412030`、403→`403053`、409→`409033`。
> 下表為**保留意圖**；Wave 2 實作前再 grep `common/code/grc_error_code.py` 確認無衝突後定序號（§11-F：本契約引用的序號為建議值，可能在 Wave 2 微調）。

| Error Code（建議）| HTTP | 訊息 | 對應 |
|------|------|------|------|
| `GRC_ROUND_NOT_FOUND` = GRC_404037 | 404 | 稽核輪次不存在 | §4 |
| `GRC_AP_SUBJECT_NOT_FOUND` = GRC_404038 | 404 | 抽查名單項目不存在 | §5.3 |
| `GRC_AR_RISK_NOT_FOUND` = GRC_404039 | 404 | 系統風險不存在 | §6.4 |
| `GRC_REMEDIATION_NOT_FOUND` = GRC_404040 | 404 | 整改計畫不存在 | §7.2 |
| `GRC_POAM_MILESTONE_NOT_FOUND` = GRC_404041 | 404 | 整改里程碑不存在 | §7.3 |
| `GRC_ROUND_INVALID_STATUS_TRANSITION` = GRC_412031 | 412 | 輪次狀態轉換不合法 | §4.3 / §8 |
| `GRC_REVERIFY_PARENT_NOT_PENDING` = GRC_412032 | 412 | 母輪不在待複驗狀態，無法發起覆核 | §4.4 |
| `GRC_ROUND_SSP_NOT_FROZEN` = GRC_412033 | 412 | SSP 快照尚未凍結 | §4.3 |
| `GRC_AP_REVIEWED_CONTROLS_REQUIRED` = GRC_412034 | 412 | AP 必須至少設定一個查核控制項 | §5.2 |
| `GRC_ROUND_INVALID_ROUND_TYPE` = GRC_412035 | 412 | 輪次類型不合法（close-out 須走 launch-reverify）| §4.2 |
| `GRC_AR_FINDING_TARGET_REQUIRED` = GRC_412036 | 412 | finding 缺少目標 AO | §6.3 |
| `GRC_RISK_NO_FINDING_LINKED` = GRC_412037 | 412 | 風險至少需關聯一條 finding | §6.5 / §6.6 |

> 沿用既有碼：`GRC_VERDICT_INCOMPLETE`(412003)、`GRC_FINDING_REQUIRED_FOR_FAILED`(412011)、`GRC_AR_NOT_LATEST_ROUND`(412008)、`GRC_POAM_NOT_ALL_CLOSED`(412005)、`GRC_POAM_INVALID_STATUS_TRANSITION`(412006)、`GRC_NOT_MANAGER`(403002)、`GRC_NOT_AUDITOR`(403001)、`GRC_POAM_NOT_FOUND`(404019)、`GRC_FRAMEWORK_VERSION_NOT_FOUND`(404032)。

---

## 附錄 A：契約判斷（ambiguity 註記 — 供 user verify）

| # | 端點 / 議題 | 我做的判斷 | 反悔成本 |
|---|-----------|-----------|---------|
| §11-A | 專案成立路徑 | 用 `/oscal-project/start`（對齊現有 route），不改 `/projects/{uid}/start`，避免破壞既有 FE 呼叫點 | 低（改路徑常數）|
| §11-B | 輪次狀態轉換 | 用獨立動詞端點（launch-audit / launch-reverify / start-auditing / close-round），非單一 `PUT status`；每轉換有不同前置 + 副作用 | 中 |
| §11-C | response id vs uid | 同時列 `*_id`（FK）與 `*_uid`（對外）；FE 以 uid 操作。各 sub-project 可只回 uid | 低 |
| §11-D | POA&M 180 天告警 | response 給布林 `deadline_warning` + `days_remaining`，門檻（建議 ≤30 天）Wave 2 定，FE 只讀布林 | 低 |
| §11-E | OSCAL 匯出 envelope | 匯出端點回**裸 OSCAL JSON 不包 `{code,data}`**（餵外部工具），與全站慣例不同 | 中（FE 下載處理）|
| §11-F | error code 序號 | 表列序號為**建議續編值**；Wave 2 開工前須 re-grep 確認無衝突再定 | 低 |
| §11-G | reviewed-controls 粒度 | AP `reviewed-controls` 以 **control 層**設定（§5.2），AO 全量判定（§6.2）由 in-scope control 自動展開其 AO；非在 AP 逐 AO 勾選 | 中 |
| §11-H | assessment-subjects 寫入 | 用**全量覆寫**（PUT 整份名單）而非逐筆 POST/DELETE，對齊 FE「勾選名單一次送出」UX | 低 |
| §11-I | finding/subject 多對多寫入 | `PUT /ar-risk/{uid}/findings` 與 subjects 同採全量覆寫語意 | 低 |

## 附錄 B：套件 service method ↔ 端點 traceability

| 套件 method（design §4.2）| 端點 |
|------|------|
| `FrameworkService.add_framework_version` / `publish_version` | §1 import / publish |
| `CatalogService.import_catalog_from_pdf` | §1 framework-versions/import |
| `ProfileService.resolve_profile`（邊界①）| §2 resource-libraries（建立）|
| `OscalSnapshotService.clone_resource_library`（邊界②）| §2 建立資源庫 / §3.2 專案成立 |
| `OscalSnapshotService.snapshot_ssp`（邊界③）| §4.3 launch-audit / §4.4 launch-reverify |
| `AssessmentPlanService.create_ap` / `generate_draft_from_ssp` | §4.3 launch-audit / §5.1 / §5 generate-draft |
| `AssessmentPlanService.set_reviewed_controls` / `set_assessment_subjects` / `set_tasks` | §5.2 / §5.3 / §5.4 |
| `AssessmentResultService.add_ar` / `add_result` / `upsert_finding` / `list_findings` | §4.3 / §6.6 / §6.3 / §6.2 |
| `AssessmentRiskService.add_risk` / `link_findings` / `list_risks` | §6.4 / §6.5 / §6.5(list) |
| `PoamService.generate_from_findings` / `upsert_remediation` / `add_milestone` / `list_*` | §6.6 / §7.2 / §7.3 / §7.1 |
| `OscalIoService.export_oscal` | §11 |
| `OscalIoService.import_ssp_docx/excel` | §2 import-ssp / §9 SSP 匯入 |
