# 指示書BD — エラー一覧の「エラー内容」選択リストの API（2026-10-27）

担当: **Claude Code**（サブエージェントで実装し、監督がレビューする）。範囲は **API まで**（画面は次の指示書）。
決定の記録は D-184（案は本書 §9。`docs/decisions.md` には別セッションの未コミット差分があるため、その commit 後に追記する）。

---

## 0. 背景とユーザーの決定

- ユーザー要望（2026-10-27）: エラー情報を統計的に取るため、エラー一覧（`/errors`）でエラー内容をセレクトリストから選べるようにする。
  選択肢はスプレッドシート「PUDUエラー一覧_20261016」の「PUDUエラー 決定稿」シート（131行。A列 = ID、C列 = エラー分類(main ja)、G列 = エラー詳細(日)）。
  「その他」を選んだときは備考欄に手入力。セレクトリストはエラー分類で絞れる。
- テーブル定義書（清掃ロボット監視テーブル定義書、2026-10-27 時点）の変更:
  - 新テーブル `error_lists`（管理側UIなし）: `id` bigint unsigned pk AI / `error_category_main_en` varchar(100) / `error_category_main_ja` varchar(200) /
    `error_category_sub_en` varchar(100) / `error_category_sub_ja` varchar(200) / `error_detail_en` text / `error_detail_ja` text /
    `inactive` tinyint unsigned 既定0 / `created_at` / `updated_at` / `deleted_at`（すべて timestamp）。★Pudu の `error_type` の列は無い★
  - `robot_error_reports.error_list_id` bigint unsigned（定義書は必須○・既定値なし・FK なし・索引なし）
- ユーザー回答（2026-10-27）:
  - ① `error_lists` は先方で作成済み。★こちら（DFA）が ID で選んで登録し、名前は ID で DB から引く★（先方が自動で入れる列ではない）
  - ② 更新用アカウントには既に更新権限があるはず → ★接続できるようになったら `SHOW GRANTS` で確認する★（未確認）
  - ③ 未選択は★空欄（NULL）★。こちらが選んで初めて入る
  - ④ ID は基本固定。追加・変更はありうる
  - ⑤ ステージング・本番に導入済み。★ただしステージングに接続できない（1045）ので実DDLは未確認★
  - 決定: ★ID で保存する★／★清掃開始失敗（`NOT_STARTED`）の行にも出す★（制御盤 `D-S_CONTROL_FAILURE`・セキュリティ `SECURITY_ON` の行には出さない — 監督の提案「ロボットのエラーの行だけ」に清掃開始失敗を足した決定と読む）／★その他は備考必須★
  - 「とりあえず API を実装して」

## 1. 共通ルール（G1〜G4。指示書AZ §2 と同じ）

- G1: API を変えたら `d-watch-api.yaml` の★関係する全経路★（`/errors`・`/errors/{error_id}/status`・新設・スキーマ）を直す
- G2: 触った箇所の古いコメント（「4列」「4カラム」等）を直す
- G3: 素通りするテストを書かない（期待値を固定し、壊したら落ちることを確かめる）
- G4: 完了時に `npx tsc --noEmit`・`npm run lint`・`npx vitest run` の件数を報告（基準: 139 files / 3284 tests）

## 2. 禁止・罠

- ★`prisma db push` / `migrate` / `db pull` を実行しない★（`.env.local` はステージングを指している。スキーマ変更は TeamWorks の領域。CLAUDE.md §7）。`npx prisma generate` は DB に触らないので可
- ★`git add -A` 禁止★。★`docs/decisions.md`・`docs/handoff/指示書AY_*`・`docs/handoff/設計_制御盤ログの取り出し_*` は別セッションの未コミット差分 — 触らない★
- 認証情報を出力・コードに書かない
- エラーの文字列（分類名・詳細）は `t()` に通さない・変換しない（D-077 と同じ。日英は DB の列を出し分ける）
- `/prechecks/{id}/status` と D-Sec（同じ PATCH を使う `lib/ds-alerts/edit.ts`）から `error_list_id` を書ける経路を作らない

## 3. API 契約（`d-watch-api.yaml` を先に直す）

### 3-1. 新設 `GET /error-lists`（tags: masters）
- 選択肢。★`inactive = 0` かつ `deleted_at IS NULL` の行だけ★。並びは ★id の昇順★
- ページングしない。`meta` は `/groups` と同じ書き方（`total` = 件数、`page` = 1、`per_page` = 件数）。クエリパラメータなし
- 応答 `{ items: ErrorListItem[], meta }`
- `ErrorListItem`: `id`(integer) / `error_category_main_en` / `error_category_main_ja` / `error_category_sub_en` / `error_category_sub_ja` /
  `error_detail_en` / `error_detail_ja`（すべて string。DB の値のまま）/ `note_required`(boolean。この選択肢を選ぶと備考が必須。`lib/config` の `ERROR_LIST_NOTE_REQUIRED_IDS`)

### 3-2. `ErrorReportItem` に3項目を追加（required に入れる）
- `error_list_id`: integer, nullable。DB の値のまま。null = 未選択
- `error_list`: `ErrorListItem`, nullable。`error_list_id` の名前解決。★無効化・削除済みの行でも解決する★（施設名と同じく名前解決は絞らない）。
  `error_list_id` が null、または `error_lists` に無い id なら null（画面は前者を「未選択」、後者を「不明(id)」と出す）
- `error_list_selectable`: boolean。この行でエラー内容を選べるか。`error_type` が `ERROR_LIST_UNSELECTABLE_ERROR_TYPES`（`D-S_CONTROL_FAILURE`・`SECURITY_ON`。完全一致）なら false、それ以外は true。★画面は error_type を見ずにこれだけを見る★（CLAUDE.md §9.6）

### 3-3. `PATCH /errors/{error_id}/status` の本文に `error_list_id` を追加
- 省略 → 変えない。整数（1以上）→ その id を書く。★null・0・負数・小数・文字列は 422★（解除は今回作らない。実DDLの NULL 可否が未確認のため）
- 次の順で検証する（422 の `details` は既存の書式。フィールドは `error_list_id` / `note`）:
  1. 現在値と同じ id → 検証せず、その列は書かない（無効化済みの id が入っている行で状態だけ保存しても 422 にしない）
  2. 変わる場合: 行の `error_list_selectable` が false → 422
  3. 変わる場合: id が `error_lists` に無い、または `inactive ≠ 0`、または `deleted_at` が非 NULL → 422
  4. ★要求がエラー内容か備考を変えるときだけ★、★適用後★の `error_list_id` が `ERROR_LIST_NOTE_REQUIRED_IDS` に入り、★適用後★の備考が空（null または空白だけ）なら 422（その他を選んで備考を書かない／既にその他の行で備考を消す、の両方）。
     ★状態だけを変える要求には掛けない★・★選べない行（`error_list_selectable` が false）には掛けない★
     （レビュー指摘で監督が判断 2026-10-27: `/prechecks` は `error_list_id` を見ずに備考を消せるので、「その他・備考なし」の行はアプリ自身が作れる。
     そこで状態だけの保存まで 422 にすると、触っていない備考が理由で止まり、アラート画面と食い違う）
- 何も変わらない要求の判定（`isEmptyPatch`）に `error_list_id` を含める
- 応答は従来どおり更新後の `ErrorReportItem`（新3項目を含む）
- ★書き込む列は `robot_error_reports` の5列（status / comment / responded_at / resolved_at / error_list_id）★。`error_list_id` は★この経路だけ★。
  `control_error_histories` は従来どおり（今回触らない）

## 4. 設定（`lib/config/index.ts`）
- `ERROR_LIST_NOTE_REQUIRED_IDS: readonly number[] = [131]`（決定稿の「OTHER ERROR / その他(該当なし)」。ID は基本固定・追加変更ありうる — ユーザー回答④）
- `ERROR_LIST_UNSELECTABLE_ERROR_TYPES: readonly string[] = ["D-S_CONTROL_FAILURE", "SECURITY_ON"]`（ユーザー判断 2026-10-27。`NOT_STARTED` は選べる）

## 5. データ層

### 5-1. `prisma/schema.prisma`（★db pull できないための手書き★）
- `ErrorList` モデル（`@@map("error_lists")`）を定義書どおりに足す。`id BigInt @id @default(autoincrement()) @db.UnsignedBigInt`、
  `inactive Int @default(0) @db.UnsignedTinyInt`、`created_at DateTime @db.Timestamp(0)`、`updated_at` / `deleted_at` は `DateTime?`。リレーションは張らない（FK が無い）
- `RobotErrorReport` に `error_list_id BigInt? @db.UnsignedBigInt`（定義書は必須だがユーザー回答③「初期は空欄」で NULL 可にする）
- 両方に「★定義書ベースの手書き（2026-10-27）。ステージングに接続できず db pull できないため。接続回復後に db pull で置き換え、差分を確認する（D-056 の例外。D-184）★」とコメント
- `npx prisma generate` を実行
- ★`@updatedAt` を付けない★（既存の注意どおり）

### 5-2. fixtures
- `lib/fixtures/error-lists.ts`: 決定稿 131行（元データ: 監督が抽出した JSON。パスは依頼文に書く）。すべて `inactive: 0`・`deleted_at: null`。★`error_type` 列はテーブルに無いので入れない★（コメントに出典だけ書く）
- `lib/fixtures/error-reports.ts`: 既存行のいくつかに `error_list_id` を入れる — 有効な id／その他(131)＋備考あり／`error_lists` に無い id（例 9999）／null（大半）。
  `NOT_STARTED` の行と `D-S_CONTROL_FAILURE`・`SECURITY_ON` の行が fixtures にあることを確かめる（無ければ足す）
- `docs/fixtures.md` に追記

### 5-3. 読み取り
- `GET /error-lists` 用: `db.errorLists()`（fixtures / Prisma）。Prisma は `where: { inactive: 0, deleted_at: null }, orderBy: { id: "asc" }`
- `robot-error-select.ts` の選択列に `error_list_id` を足す
- 名前解決: 一覧・1件とも、行の `error_list_id`（非 null）を集めて `errorList.findMany({ where: { id: { in } } })` を★絞らずに★1回引き、写像で解決（N+1 にしない）
- `error-report-mapping.ts` で3項目を組み立てる。`error_list_selectable` の判定は純粋関数1つ（`lib/config` の定数を読む）

### 5-4. 書き込み
- `/errors` 専用のパーサ・計画（`lib/errors/status-update.ts` を新設。`parsePrecheckStatusUpdateBody` / `planPrecheckStatusUpdate` を内部で使い、`error_list_id` を足す）。
  ★`lib/prechecks/status-update.ts` の挙動は変えない★（`/prechecks` は `error_list_id` を捨てるまま）
- 型 `ErrorStatusPatch = PrecheckStatusPatch & { error_list_id?: number }`、`ErrorStatusUpdateRequest`（`lib/types`）
- `robot-error-write.ts`: 既存の `robotErrorColumnsFromPatch`（4列）は★そのまま★（事前チェックが使う）。`/errors` 用に `robotErrorColumnsFromErrorPatch` を足し、
  `error_list_id` だけを追加で許す（BigInt に変換）。「4列以外を含めば例外」の歯止めは5列版にも入れる
- `lib/db/index.ts` の `updateErrorReportColumns`（interface と fixtures 版・Prisma 版の3か所）を `ErrorStatusPatch` に
- `fixture-error-report-overrides.ts` は `error_list_id` も写す（★写し忘れると fixtures で 200 なのに反映されない★）
- 監査ログ（`lib/logger/events.ts` の `DbWriteColumn`・`auditBase`）に `error_list_id` の old/new
- テスト: `lib/db/__tests__/robot-error-write-sql.test.ts` を5列に。★事前チェック・D-Sec の経路では `error_list_id` を書けない★ことを固定するテストを足す

## 6. 段階と担当（サブエージェント）

| 段階 | 内容 | 担当ファイル |
|---|---|---|
| 1 契約 | §3 の YAML、§4 の設定、`lib/types` の型、§5-1 の schema と generate、§5-2 の `error-lists.ts` | `d-watch-api.yaml` / `lib/config/index.ts` / `lib/types/index.ts` / `prisma/schema.prisma` / `lib/generated/` / `lib/fixtures/error-lists.ts` |
| 2a 読み取り | §5-2 の error-reports fixtures、§5-3、`app/api/error-lists/route.ts`＋テスト、`GET /errors` のテスト更新 | `lib/fixtures/error-reports.ts` / `lib/db/robot-error-select.ts` / `lib/db/error-report-mapping.ts` / 新 `lib/db/error-list-source.ts` / `lib/db/index.ts`（★読み取り関数だけ★）/ `app/api/error-lists/` / `app/api/errors/__tests__/` / `docs/fixtures.md` |
| 2b 書き込み | §5-4 | 新 `lib/errors/` / `app/api/errors/[error_id]/status/` / `lib/db/robot-error-write.ts` / `lib/db/fixture-error-report-overrides.ts` / `lib/logger/events.ts` / `lib/db/index.ts`（★`updateErrorReportColumns` の3か所だけ★）/ 関連テスト |
| 3 レビュー | 監督が差分を読み、別のエージェントに独立レビューさせる | — |
| 4 資料 | `docs/code-map.md`、CLAUDE.md §3・§7（書ける列をテーブルごとに書き分ける）、本書の完了記録 | 監督 |

2a と 2b は並行。`lib/db/index.ts` は担当の関数以外を触らない（Edit が「変更された」で失敗したら読み直して再適用）。

## 7. 完了条件
- tsc 0 / lint 0 / vitest 全件通過（件数を記録）/ `npm run build` 成功
- fixtures ビルドで `GET /error-lists`（131件）・`GET /errors`（新3項目）・PATCH（有効 id／その他＋備考なし→422／選べない行→422／無効 id→422／同じ id で状態だけ→200）を実際に叩いて確かめる

## 8. 接続回復後にやること（今回はできない）
- `SHOW GRANTS` で `error_list_id` の UPDATE 権限を確認（ユーザー回答②）
- `information_schema` で `error_lists` と `error_list_id`（型・NULL 可否）を確認 → db pull → 手書きとの差分確認
- `error_lists` の行数・中身が決定稿 131行と一致するか（特に 131 = その他）
- ★ローカルDB（`dwatch_local`）には `error_lists` も `error_list_id` も無かった★。実DBモードでは `GET /errors`・`PATCH /errors`・D-Sec の保存・`GET /error-lists` が 500 になり、
  シード（`select` の無い `robotErrorReport.create`）も落ちる。→ ★2026-10-27 にローカルにだけ表と列を足す手順を `scripts/local-db/` に用意した★（ユーザー指示「ローカル環境でテストするのでダミーのDB作成」）

## 9. D-184（案）
- エラー一覧の各行に「エラー内容」（`error_lists` の id）を登録できるようにする。保存先は `robot_error_reports.error_list_id`（★アプリが書く5列目。この表のこの経路だけ★）
- 選べる行は `D-S_CONTROL_FAILURE`・`SECURITY_ON` 以外（清掃開始失敗は選べる）。その他（id 131）は備考必須。未選択は NULL。解除は作らない
- 名前解決は絞らない／選択肢は有効な行だけ
- `prisma/schema.prisma` の `ErrorList` と `error_list_id` は定義書ベースの手書き（D-056 の例外。接続回復後に db pull で置き換え）
- 却下案: 備考（comment）に G列の文字列を書く — 権限変更は要らないが、手入力の備考と混ざり統計が文字列一致頼みになる
