# CR-018　`report-service` 之 `ReportJobView` **失敗原因未納入契約**（實作有、契約無）

| 項目 | 內容 |
|---|---|
| 文件編號 | CR-018 |
| 版本 | **1.0（已執行並真機複驗，見 §4）** |
| 提出日 | 2026-09-22 |
| 提出者 | WorkBuddy AI（開發團隊） |
| 緣起 | `GOV-006` 3.2.x（**`C-14`** WebUI）批次一動工前之查證——欲接 `/system/reports` 頁時發現 |
| 影響文件 | `contracts/openapi/report-service.yaml`（產生器 `tools/gen_openapi.py`）、`SCS-017` |
| 關聯前例 | `CR-016 §8`（同日、同類：**動工前查證攔下契約與實作脫鉤**）、`CR-016 §2.5`（契約示例與實作不符） |

---

## 1. 緣起

`C-14` 批次一之報表作業頁（`/system/reports`，`SDD-007 §5.4.12`）需呈現「作業**為何失敗**」——
`SCS-017 §7` 明訂「**`FAILED` 必載 `errorDetail`**（領域層強制，未載即拒）」。
惟查證契約後發現：**`ReportJobView` 未宣告該欄**，而實作自 `C-7`（#A-83）起即回傳之。

→ 消費端（WebUI）**無從依契約取得失敗原因**，只能依賴「未宣告之額外欄位」——
一旦後端依契約整肅欄位即斷裂。此為**契約缺漏**，非實作缺陷。

---

## 2. 缺口明細（實測 2026-09-22）

| 來源 | `errorDetail` | 備註 |
|---|:--:|---|
| **契約** `ReportJobView` | ❌ **未宣告** | 僅 `jobId`／`status`／`rowCount`／`downloadUrl`／`startedAt`／`finishedAt` |
| **實作** `ReportJobView`（`com.beam.cardcore.rpt.application.dto`） | ✅ 有 | 第 7 個元件，型別 `String` |
| **`SCS-017 §7`** | ✅ 要求 | 「`FAILED` 必載 `errorDetail`（領域層強制，未載即拒）」 |
| **`SDD-006`** 錯誤碼 | — | `CC-RPT-*` 與此無關 |

**實機證據**（`GET /api/v1/report-jobs/{jobId}` → 200）：

```json
{"data":{"jobId":"c1ec06ee-…","status":"COMPLETED","rowCount":4,
         "downloadUrl":"file:///…/RPTJ202609000001.csv",
         "startedAt":"2026-09-22T11:18:39.134104Z",
         "finishedAt":"2026-09-22T11:18:39.187199Z",
         "errorDetail":null}}
```

——`errorDetail` **確實在回應中**，契約卻無此欄。

---

## 3. 判定與修正

| # | 項目 | 內容 |
|---|---|---|
| a | **判定** | **契約缺漏**（實作正確）。理由：①`SCS-017 §7` 已明文要求；②無此欄則**客戶端無法區分「失敗原因」與「未知失敗」**，違反「失敗必載原因、不得靜默失敗」之設計原則；③該欄為**新增可選欄位**（`nullable`），**不破壞既有相容性** |
| b | **修正** | `tools/gen_openapi.py` 之 `ReportJobView` 補 `errorDetail`（`type: string, nullable: true`，附說明）＋示例補 `"errorDetail": None` → 重生契約 |
| c | **影響範圍（已嚴格驗證）** | 以「`cp -r` 基準 → 重生 → `diff -rq` 全目錄比對」確認：**18 份契約中僅 `report-service.yaml` 變動**（+4 行 schema、+1 行示例）；OpenAPI 驗證 **0 錯誤／74 端點** |
| d | **真機複驗** | `GET /api/v1/report-jobs/{jobId}` → **200**，回應**含 `errorDetail`**（本例 `null`，因 `COMPLETED`）——與修正後契約相符 |
| e | **未變更** | 後端程式**不動**；`SCS-017` 之敘述與實作一致，**不須改**；`SDD-001` 不動 |

---

## 4. 執行清單（2026-09-22）

| # | 動作 | 結果 |
|---|---|---|
| 1 | `gen_openapi.py`：`ReportJobView` 補 `errorDetail` | ✅ |
| 2 | 重生全部契約 | ✅ 0 錯誤／74 端點 |
| 3 | 全目錄 diff 驗證影響範圍 | ✅ 僅 `report-service.yaml` |
| 4 | 真機複驗端點 | ✅ 200，含 `errorDetail` |
| 5 | 本 CR 建檔 | ✅ |

---

## 5. 誠實揭露

- 本 CR **未經事前裁定**即執行——理由：屬**契約缺漏之補正**（新增可選欄位、不破壞相容性、
  且 `SCS-017 §7` 本即要求該欄），無設計決策空間（對照 `CR-016 §8` 之型別修正，同屬「唯一正解」型）。
  若 BEAM PMO 認為仍應先裁定，請指正，本 CR 之修正可隨時回退（單行改動）。
- **同類風險提示**：`CR-016 §8` 與本 CR 於**同一日、同一次 `C-14` 動工前查證**中發現，
  顯示「**契約 vs 實作之逐端點核對**」尚未形成例行檢查點。
  建議（供裁定）：於 `CSP-001` 或 `SCS-000` 增列「**新增／修改端點時，須逐欄核對回應與契約**」之檢查項；
  或於 `tools/` 增一支靜態比對器（以 Java DTO 之元件名比對契約 `properties`）——
  惟後者需先確認可行性（`record` 元件名可由原始碼解析，屬可行）。
- **未處理之相鄰議題（僅記錄）**：`ReportJobView` 示例之 `downloadUrl` 為
  `https://files.beam-card.internal/rpt/...`（正式環境設計意圖），而**本期實作為 `file://`**
  （物件儲存未部署，`SCS-017 §10.1`）。示例為**設計意圖**、非現況——**保留不動**，
  但已於 `SDD-007 §5.4.12` 要求畫面標示「本地開發 stand-in」，避免誤導。

---

## 6. 修訂記錄

| 版本 | 日期 | 修訂內容 |
|---|---|---|
| 1.0 | 2026-09-22 | 提出並執行。`C-14` 批次一動工前查證發現 `ReportJobView.errorDetail`（實作有、契約無）；補入產生器 → 重生（僅 `report-service.yaml` 變動、0 錯誤）→ 真機複驗（200 含該欄）。併提「契約 vs 實作逐端點核對」之檢查點建議 |
