# 指示書AW — エラー一覧ページ（`/errors`）を作る（`robot_error_reports` の未対応エラーを一覧し、対応状況と備考を更新する）

★**前提: 指示書AV（段階5b を含む）は実装済み（HEAD `79485ce` 時点）。**★
（AV の実装コミットは `1501173`〜`939a2c5`、段階5b（清掃詳細ログの既定を全期間に）は `137a94b`。本書はその上に足す）

**日付**: 2026-10-06 / **担当**: Cursor（★実装は Claude Code（2026-10-06。ユーザー指示「ここで実装する」）。§13★）/ **基点**: develop `79485ce`
**前提**: ★D-160（本書の根拠。`docs/decisions.md:12612-12622`）★ / D-152（ステージングへの4カラム書き込みを解禁。`:12325-12356`）/
D-156 項3・4（AT の `@updatedAt` 例外・書き込みの規則。`:12475-12489`）/ D-158 P-8〜P-10（エラーの3区分・`robot_error_reports` を正とする。`:12523-12581`）/
D-105（`PrecheckItem.id` は string・`error_id` を `Number()` に通さない。`:5442` 以降）/
指示書AT（4カラム UPDATE）/ 指示書AU 段階0-4（`robot_error_reports` は `select` で読む）・段階3b（予定は2表から）/
`CLAUDE.md` §3・§4.8・§6・§7・§9.3・§9.4・§9.6

★**期待値表（§3 の表1）は監督が確定した正。変えないこと**★（`CLAUDE.md` §0-2・§6-2・§9.4）。
★**表2・表3 は本書で表1 と既存のコードから導いたもの**★。★監督が確認し正とした（2026-10-06。§11 Q14）★。
★**§11 の質問に回答が来て本書の暫定が変わったら、§11 の各行に書いた「直す場所」を本文と同時に直すこと**★
（本文と末尾の決定が食い違ったまま渡さない）。

★**行番号はすべて 2026-10-06 の develop `79485ce`（AV 実装後）で確かめたもの**★
（`f424689` 以降のコミットは資料だけで、`app/` `lib/` `components/` `d-watch-api.yaml` `prisma/` に差分が無いことを `git diff` で確かめた）。
★**着手時に develop が進んでいたら、関数名・文で探し直すこと**★（特に `lib/db/index.ts`・`lib/types/index.ts`・`d-watch-api.yaml`・`lib/i18n.ts`）。

---

## 0. 何をするか

| # | やること | 段階 |
|---|---|---|
| 1 | 新しいページ ★`/errors`「エラー一覧」★（EN 暫定 "Errors"）をメニューの「モニタリング」の ★「アラート」の直後★ に足す | 段階5 |
| 2 | ★`robot_error_reports` の全行のうち、状態が「未解消」「対応中」のもの★を一覧する。★「解消済みも表示」★ で解消も出す | 段階2〜5 |
| 3 | 一覧から ★対応状況（未解消 → 対応中 → 解消 → 未解消）と備考★ を更新できる。★書くのは4カラムだけ★（指示書AT の書き込みを、事前チェックの行だけから ★全行★ に広げる） | 段階3〜5 |
| 4 | ★エラーの種類（`error_type`）・レベル（`error_level`）・シリアルは生値のまま★出す（区分を作らない。D-077 / D-160 項3） | 段階2・5 |

★**変えないもの**★（D-160 末尾）: ★アラート画面（`/alerts`。事前チェック＋出発失敗）の挙動★／★`PATCH /prechecks/{id}/status` の挙動（事前チェックの行だけ・他は404）★／
★D-Sec アラート画面とその取得元（`control_error_histories`。次フェーズで切り替え）★／★AT の `@updatedAt` 例外とガードテスト★。

---

## 1. 背景

- ステージングに各店舗のエラー16件（ロボット・制御盤・セキュリティ・清掃開始失敗）が入ったが、★アラート画面は「事前チェックの異常」と「出発失敗」だけを出す作り★なので、
  ★本社（事前チェックの予定がある唯一の店舗）しか出なかった★（D-160。2026-10-06 実測・先方投入: 16件すべて本清掃の予定に紐づく）。
  - `/alerts` が拾うのは ★事前チェック行（`precheck ≠ 0`）に紐づくエラー行だけ★（`lib/db/index.ts:1366` の `clean_timetable_id: { in: 事前チェック行の id }`、`:1393-1405`）。
    ★本清掃行に紐づくエラー・予定 ID が NULL のエラーは出ない★（`:1387-1388` / `:1402-1405`。D-104）
- `robot_error_reports` は ★エラー全般の表★ になった（第11回 P-8〜P-10。D-158）:
  制御盤エラー＝`robot_id` NULL・`serial_number` が制御盤の SN／★セキュリティエラーと「清掃を開始できなかった」＝`robot_id` NULL・`serial_number` = `'0'`★（`error_type` で見分ける）。
  ★先方の DDL では `robot_id` が NULL 可、`prisma/schema.prisma:318` は NOT NULL のまま★（db pull 待ち）→ ★`robot_id` / `robot` を読むと落ちる★（指示書AU 段階0-4。`lib/db/robot-error-select.ts:4-12`）
- ユーザー回答（2026-10-06。D-160）: 未解消・対応中の全行を一覧／解消は切り替えで表示／このページから対応状況と備考を更新／種類は生値のまま。

---

## 2. 決定（Cursor は従うこと）

| # | 決定 | 理由 |
|---|---|---|
| D1 | ★対象は `robot_error_reports` の全行★（`error_type` を問わない。事前チェック・本清掃・予定 ID が NULL・`robot_id` が NULL の行すべて）。★既定は `status` が 未解消(0)・対応中(1)★、`include_resolved=true` で 解消(2) を足す | D-160 項1 |
| D2 | ★状態コード ↔ enum は既存の `lib/db/precheck-mapping.ts` の2関数だけ★（`resolutionStatusFromRobotErrorStatus()` `:33` / `robotErrorStatusFromResolutionStatus()` `:53`）。★0/1/2 を直書きしない★ — 絞り込みのコードも `["unresolved", "in_progress"].map(robotErrorStatusFromResolutionStatus)` で作る | 変換を1箇所に（AT §1 #7） |
| D3 | ★検知時刻 ＝ `errored_at ?? created_at`★。★既存の `detectedAtFromError()`（`lib/db/precheck-mapping.ts:171`）を使い、`jstIsoFromDb()` に通す★ | `/alerts` と同じ規則（D-105。指示書S 3-4） |
| D4 | ★紐づく予定の4区分★: `clean_timetable_id` が NULL →「予定なし」（`none`）／★本表にも履歴にも無い id★ →「予定不明」（`unknown`）／`precheckMinutesFromCode(precheck)` が null →「本清掃」（`main`）／それ以外 →「事前チェック（N分前）」（`precheck`。N は ★生の分数★） | D-160 / 監督の既定。分数は D-105 項1 と同じく生値（設定の 60/30 に無い値でも出す） |
| D5 | ★予定は2表から引く★: 一覧は `findTimetablesByChunks()`（`lib/db/timetable-source.ts:255`）、1件は `findTimetableById()`（`:285`）。★同じ id は本表が勝つ★ | 指示書AU 段階3b。表1 E6 |
| D6 | ★`robot_error_reports` の読み取りは `select` で列を絞る。`robot_id` / `robot` を読まない★。新しい `select` は `lib/db/robot-error-select.ts` に定数で置き、`lib/db/__tests__/robot-error-select.test.ts:18-23` の一覧に足す | 指示書AU 段階0-4（NULL の行で落ちる） |
| D7 | ★並びは検知時刻の降順、同時刻は `error_id` の昇順（文字列の辞書順）★ | 監督の既定。`sortPrechecks()` と同じ（D-105 項2 #4。`error_id` が数値である保証が無い） |
| D8 | ★SQL に降ろすのは `status IN`・`facility_id IN`・検知時刻の期間★。★並べ替えとページ分けは「キーだけの一覧」をアプリで行う★（2段階で読む。下の「D8 の理由」） | ★`errored_at ?? created_at` の順は Prisma のクエリビルダで書けない★（`COALESCE` の `orderBy` が無い。生SQL は禁止）。★§11 Q1★ |
| D9 | ★種類（`error_type`）の絞り込みは部分一致・大小文字を区別しない★（`includesFoldPlain()`。`lib/format/search-text.ts:190`）。★SQL の `LIKE` にしない（アプリ側で行う）★ | 機種・シリアルと同じ照合（指示書AG D-2）。`LIKE` は `%` `_` のエスケープと DB の照合順序で fixtures と結果が割れる。★§11 Q4★ |
| D10 | ★施設名の絞り込みは既存の `resolveFacilityIdsByName()`（`lib/facilities/resolve-by-name.ts:78`）★、分類は `resolveFacilityIdsByMasters()`（`lib/facilities/resolve-by-master.ts:88`）、★2本は `intersectFacilityIds()`（`lib/facilities/intersect-ids.ts:45`）で AND★ | `/clean-logs` と同じ（`app/api/clean-logs/route.ts` の取得部分）。★`null` = 絞らない / `[]` = 0件★（罠1） |
| D11 | ★更新は新しい `PATCH /errors/{error_id}/status`★。★`robot_error_reports` に行があれば書ける（無ければ404）★。★規則は AT のまま★: パッチは `planPrecheckStatusUpdate()`（`lib/prechecks/status-update.ts:50`）、本文の検証は `parsePrecheckStatusUpdateBody()`（`:100`）、書き込みは `writeRobotErrorColumns()`（`lib/db/robot-error-write.ts:121`）を ★そのまま使う★。★改名しない★（§11 Q7） | D-160 項2。★同じ列を2つの規則で書かない★ |
| D12 | ★`PATCH /prechecks/{id}/status` は変えない★（事前チェックの行だけ・他は404）。★ルートの共通化はしない★（新しいルートを並べて置く） | D-160「変えないもの」。AT のルートテストを巻き込まない |
| D13 | ★fixtures は「DB の行の形」で持つ★（日時は `toDbJst()` を通した壁時計値の `Date`、id は `bigint`）。★fixtures と Prisma は同じ純粋関数（行 → API の値・絞り込み・並べ替え）を通す★ | `CLAUDE.md` §9.5（判定ロジックは切替の外側）。`USE_FIXTURES` で件数が変わらない（指示書AC 禁止事項6） |
| D14 | ★fixtures の書き込みは新しい上書き表★（`lib/db/fixture-error-report-overrides.ts`）。★パッチは `robotErrorColumnsFromPatch()`（`lib/db/robot-error-write.ts:70`）で DB の列の値に写してから上書きする★ | Prisma 版と同じ歯止め・同じ値（`toDbJst()` 済みの日時）。★AT の `fixture-precheck-overrides.ts` は触らない★（§11 Q8） |
| D15 | ★メニューは常に出す★（`/alerts` と同じ）。★`AppShell.tsx` には契約（`facilities.contract`）でメニューを出し分ける仕組みが無い★（`components/layout/AppShell.tsx:8-34`・`:126-158`。全メニューを無条件に描く）。★本書でも作らない★ | 監督の既定「`/alerts` と同じ規則」。実測した規則は「無条件」。★§11 Q2★ |
| D16 | ★列の見出しは「施設名」★（監督の依頼文は「店舗」） | `CLAUDE.md` §8「施設（旧称店舗）」。★既存キー `店舗` は EN が `"Stores"`（複数形。`lib/i18n.ts:390` のレポートの粒度タブ）で列見出しに合わない★。★§11 Q5★ |
| D17 | ★対応状況の巡回順（未解消 → 対応中 → 解消 → 未解消）は純粋関数1本★: `AlertsScreen.tsx:80-89` の `nextResolutionStatus()` を `lib/prechecks/status-cycle.ts`（新規）へ移し、★`/alerts` と `/errors` の両方がそれを import する★ | ★2画面で巡回順が食い違わないため★。`/alerts` の挙動は変わらない（関数を移すだけ）。★§11 Q10★ |
| D18 | ★レスポンスに `clean_timetable_id` を載せる（画面には出さない）★ | 「予定不明」の行を調べるときに要る。数値（`Number()`）で返す — `clean_timetables.id` は `BigInt`（`PrecheckItem` の `facility_id` と同じ作法）。★§11 Q9★ |

### D8 の理由（「キーだけの一覧」をアプリで並べる）

- 並びの基準は ★`errored_at ?? created_at`★（D3）。Prisma の `orderBy` は ★列ごとの並べ替えしか書けず `COALESCE` を書けない★。
  `[{ errored_at: { sort: "desc", nulls: "last" } }, { created_at: "desc" }]` にすると ★`errored_at` が NULL の行（表1 E7）は検知時刻に関係なく最後に回る★ — 表1 の並びと食い違う（例外もテスト失敗も出ない）。
- ★生SQL（`$queryRaw`）は禁止★（`CLAUDE.md` §3。ESLint でも禁止。D-122）。
- そこで ★2段階で読む★:
  1. ★キーだけ★（`error_id` / `clean_timetable_id` / `facility_id` / `error_type` / `errored_at` / `created_at` / `status`）を、★SQL で `status IN`・`facility_id IN`・期間を絞って★ 全件読む
  2. アプリで ★絞り込みの「正」（純粋関数）→ 並べ替え → ページ分け★ をして、★そのページの `error_id` だけ★ 列を足して読み直す（最大 `PAGINATION.maxPerPage` = 200件。`lib/config/index.ts:421-424`）
- ★既存の一覧も `take` / `skip` を SQL に降ろしていない★（`lib/types/index.ts:588` 付近「`page` / `per_page` … ★`take` / `skip` は SQL に降ろさない★（指示書AC D-3）」）。本書の形はそれより軽い（1段目はキーだけ）。
- ★性能の見積もり★: 既定（未解消＋対応中）は対応待ちの件数しか読まない。★「解消済みも表示」は保持期間ぶん全部のキーを読む★（ステージングは 188行。D-158 2026-10-06 実測・先方投入）。★本番の行数は不明★ → §10 の範囲外・リリース前に見直す。

---

## 3. ★期待値表（正）★

### 表1（★監督確定・正・変えない★）

| # | 行 | 既定（解消を含めない）で出るか | 「解消済みも表示」で出るか | 紐づく予定の表示 | 検知時刻 |
|---|---|---|---|---|---|
| E1 | status 0・本清掃の予定に紐づく | 出る | 出る | 本清掃 | errored_at |
| E2 | status 1・事前チェック（precheck=30）に紐づく | 出る | 出る | 事前チェック（30分前） | errored_at |
| E3 | status 2・本清掃 | 出ない | 出る | 本清掃 | errored_at |
| E4 | status 0・clean_timetable_id NULL | 出る | 出る | 予定なし | errored_at |
| E5 | status 0・clean_timetable_id が両方の予定表に無い id | 出る | 出る | 予定不明 | errored_at |
| E6 | status 0・予定が clean_timetable_histories にだけある | 出る | 出る | 本清掃（履歴の予定も引く） | errored_at |
| E7 | status 0・errored_at NULL | 出る | 出る | （予定どおり） | created_at |
| E8 | status 0・robot_id NULL・serial_number '0'・error_type SECURITY_ON | 出る | 出る | （予定どおり） | errored_at。シリアル欄は「0」、種類は「SECURITY_ON」のまま |
| E9 | status 0・precheck=10 の予定に紐づく | 出る | 出る | 事前チェック（10分前） | errored_at（設定の 60/30 に無い値でも生の分数で出す） |

並び順: 検知時刻の降順、同時刻は error_id の昇順。

PATCH: AT §3-4 の状態遷移と日時の規則（12行の表）をそのまま適用する。★事前チェック以外の行（E1・E4・E5・E8）にも書ける★こと、★存在しない error_id は404★、★書く列は4列以内（ガードテスト）★。

★テストの書き方の注意（表1 の値を変えずに、テストを確かにするための指定）★:
- ★E7 の `created_at` は9行の中で一番新しく、E7 の `errored_at` 以外の値より後★にする（例: 他は 10/5 19:00〜22:00、E7 は `created_at` = 10/6 08:00）。
  → ★E7 が先頭に来ること★を確かめる。`nulls: "last"` の並べ替え（D8 の理由）だと最後に来るので、取り違えを検出できる
- ★2行を同じ検知時刻にして `error_id` の昇順★を確かめる（例: E4 = `"E-10"`・E5 = `"E-9"` を同時刻にし、★`"E-10"` が先★ — 辞書順。数値順なら逆になる）
- E2 の 30 は ★`PRECHECK_MINUTES_BEFORE[1]`（`lib/config/index.ts:244`）から読む★。★E9 の 10 は設定に無い値であることが本題なので直書きする★（`lib/fixtures/prechecks.ts` の `20` と同じ扱い。コメントで理由を書く）
- E6 は ★履歴の表（`cleanTimetableHistory`）にだけ★ 予定を置く。★本表の `findMany` が E6 の id を返さない★ことをスタブで確かめる
- E8 の行は ★`robot_id: null`★ を持たせ、`guardNullRobotId()`（`lib/db/__tests__/robot-error-null-guard.ts`）をスタブに入れる（`robot_id` を読むと落ちる）
- E7・E8 の「（予定どおり）」は ★テストを書く人が予定を決めてよい★（例: 両方とも本清掃）
- 日時は ★`+09:00` 付きの ISO 文字列から `toDbJst(new Date(...))`★ で DB の壁時計値を作る（スタブが返す `Date` は DB と同じ形にする）

### 表2（一覧の絞り込み・ページ。★本書で導いたもの。監督確認済み・正★）

入力は表1 の9行（`facility_id` は E1〜E5 = 1、E6〜E9 = 2 とする）。

| # | 入力 | 期待 | 確かめること |
|---|---|---|---|
| F1 | 既定（パラメータなし） | E3 以外の8行・`meta.total` = 8 | D1 |
| F2 | `include_resolved=true` | 9行 | D1 |
| F3 | `include_resolved=yes`（`true` / `false` 以外） | 422 | 入力の検証（黙って false に倒さない） |
| F4 | `error_type=secur`（大小文字違い・部分） | E8 だけ | D9 |
| F5 | `error_type=`（空）／空白だけ | F1 と同じ | 空は「絞らない」 |
| F6 | `date_from` = `date_to` = E7 の `created_at` の日付 | E7 が入る（`errored_at` が NULL でも `created_at` の日付で当たる） | D3。★SQL 側も `errored_at` が NULL の行を `created_at` で絞る★ |
| F7 | `date_from` > `date_to` | 422 | `parseCleanLogQuery()` と同じ文言（`lib/clean-logs/query.ts` の「date_fromはdate_to以前の日付を指定してください」） |
| F8 | 分類で絞って施設が0件（`facility_ids = []`） | 0件（★全件にならない★） | 罠1 |
| F9 | 施設名が当たり分類も当たるが積集合が空 | 0件 | D10（AND） |
| F10 | `per_page=3&page=2`（既定の8行） | 並び順の4〜6行目・`meta.total` = 8 | D8 |
| F11 | `page=99` | `items: []`・`meta.total` = 8 | 404 にしない（一覧の作法） |
| F12 | `status = 3` の行が混ざる | ★既定でも「解消済みも表示」でも出ない・例外にならない★ | SQL の `status IN` で外れる。純粋関数も生の数値で比べるので `resolutionStatusFromRobotErrorStatus()` の例外は起きない。★§11 Q3★ |

### 表3（ラベル `errorScheduleLabel()`。★本書で導いたもの。監督確認済み・正★）

| # | `schedule_link` / `precheck_minutes` | JP | EN（暫定） |
|---|---|---|---|
| S1 | `main` / null | 本清掃 | Main run |
| S2 | `precheck` / 30 | 事前チェック（30分前） | Pre-check (30 min before) |
| S3 | `precheck` / 10 | 事前チェック（10分前） | Pre-check (10 min before) |
| S4 | `none` / null | 予定なし | No schedule |
| S5 | `unknown` / null | 予定不明 | Unknown schedule |
| S6 | `precheck` / ★null★（API 契約では起きない） | 事前チェック | Pre-check |

S6 は `precheckSeverityLabel()`（`lib/prechecks/label.ts`）と同じ考え方 — ★黙って「null分前」を出さない★。

---

## 4. ★落とし穴（全段階で読むこと）★

| # | 罠 | どう避けるか |
|---|---|---|
| 4-1 | ★`robot_error_reports` を `select` なし・`include` で読む／`robot_id`・`robot` を選ぶ★ | ★`robot_id` が NULL の行（E8・NOT_STARTED・制御盤）で読み取りが落ちる★（指示書AU 段階0-4）。★`robot-error-select.ts` の定数だけを使う★。`findUnique` も同じ |
| 4-2 | ★`facility_ids` の `null` と `[]` を混同する★ | `!== null` で分岐する（罠1。`lib/db/index.ts:1320-1328` のコメントと同じ）。★`?.length` で書かない★ |
| 4-3 | ★期間の条件を `errored_at` だけにかける★ | ★`errored_at` が NULL の行は `created_at` で絞る★（`OR: [{ errored_at: 範囲 }, { errored_at: null, created_at: 範囲 }]`）。片方だけだと E7 が期間指定で消える（表2 F6） |
| 4-4 | ★期間の `Date` を自分で作る★ | ★`dbDateRange()`（`lib/db/date-range.ts:50`）の1箇所★。DB の値は JST の壁時計値（`CLAUDE.md` §3）。★9時間を足さない・引かない★ |
| 4-5 | ★`errored_at ?? created_at` を SQL の `orderBy` で近似する★ | ★しない★（D8 の理由）。並べ替えは純粋関数1本 |
| 4-6 | ★`take` / `skip` を `findTimetables*()` に渡す★ | 型で禁止されている（`lib/db/timetable-source.ts:57-70`）。予定は id で引くだけ |
| 4-7 | ★`error_id` を `Number()` に通す★ | ★文字列のまま★（varchar(50)。D-105 項2）。React の key も文字列のまま |
| 4-8 | ★`findUnique({ error_id })` の結果をそのまま採る★ | ★DB の照合順序は大小文字を区別しない（ci）★。`rer-1` で `RER-1` が当たる → ★`report.error_id !== id` なら null（404）★（`precheckItemById()` `lib/db/index.ts:1517-1521` と同じ。D-4） |
| 4-9 | ★`clean_timetables.id` の照合を `bigint` の `===` で書く★ | `bigint` は `===` で比べられるが Map のキーにはならない → ★`.toString()` をキーにする★（`lib/db/index.ts:1331` の `key()` と同じ） |
| 4-10 | ★予定を本表だけから引く★ | ★E6 が「予定不明」になる★。`findTimetablesByChunks()` / `findTimetableById()` を使う（D5） |
| 4-11 | ★状態コードを直書きする（`status: { in: [0, 1] }`）★ | D2。`robotErrorStatusFromResolutionStatus()` から作る |
| 4-12 | ★`planPrecheckStatusUpdate()` を通さずにパッチを作る／本文を `data` に渡す★ | ★パッチを作るのは `planPrecheckStatusUpdate()` だけ★（AT §3-6）。本文の `updated_at`・`error_type` が DB に届く経路を作らない |
| 4-13 | ★`update`（1件更新）を使う★ | ★`writeRobotErrorColumns()`（`updateMany` の1文）だけ★（AT §3-2）。★書き込み用の接続（`getWritePrisma()`）で書く★ |
| 4-14 | ★`prisma/schema.prisma` の `RobotErrorReport.updated_at` に `@updatedAt` を付ける★ | ★付けない★（AT §3-1・D-156 項3）。`lib/db/__tests__/robot-error-write-sql.test.ts` が見張っている |
| 4-15 | ★1段目と2段目の間に行が消える★（ステージングのデータ入れ替え。第10回 N-6） | 2段目で見つからない `error_id` は ★飛ばす★（★例外にしない★ — 一覧全体が500になる）。`meta.total` は1段目の件数のまま（1件ずれうる。承知のうえ）。★コメントで理由を書く★ |
| 4-16 | ★`/alerts` の挙動を変える★ | `AlertsScreen.tsx` で変えてよいのは ★`nextResolutionStatus()` を import に置き換えることだけ★（D17）。★`prechecks()` / `precheckItemById()` / `/prechecks` のルートは触らない★ |
| 4-17 | ★JP/EN の切替で列幅や位置が動く★ | ★`table-layout: fixed` ＋ `<colgroup>`★（`components/features/detail-log/CleanLogTable.module.css:45`・`CleanLogTable.tsx:80`）。★状態ボタンは固定幅★（「対応中」と "In progress" で幅が違う）。ツールバーの入力欄も固定幅 |
| 4-18 | ★画面の文字列を `t()` に通さない／キーの全角括弧がずれる★ | 新しい文言はすべて `t()`（`lib/i18n.ts`）。★`{n}` は replace★（`"{n}分前"` の前例）。★既存キーの EN と衝突しないか確かめる★（`"{n}件"` は `"{n} facilities"`（`lib/i18n.ts:274`）なので件数表示に使えない。`店舗` は `"Stores"`） |
| 4-19 | ★生SQL（`$queryRaw` / `$executeRaw`）・`eslint-disable` を足す★ | ★禁止★（`CLAUDE.md` §3） |
| 4-20 | ★`git add -A`★ | ★禁止★（`CLAUDE.md` §7）。ファイルパスを明示する。★作業ツリーに自分の作業でないファイルがあっても含めない★ |

---

## 段階0 — 着手前

1. ★`develop` で作業する★（`.claude/worktrees/` の中ではない）。`git pull` で最新にし、★AV の実装（段階5b を含む）が入っていること★を `git log` で確かめる（`79485ce` 以降・`lib/dashboard/latest-run.ts` と `lib/clean-logs/detail-log-date-range.ts` があること）。
2. ★作業ツリーに自分の作業でない変更があっても触らない・コミットに含めない★（`git status` で控える）。
3. `npx prisma generate` → `npx tsc --noEmit -p .` → `npm test` が通ることを確かめ、★テストの件数を記録する★（以後の増分の基準）。通らなければ着手前に報告。
4. ★Next.js は版が新しい★。ルートを書く前に `node_modules/next/dist/docs/` の Route Handler の項を読む（`CLAUDE.md` 末尾）。★動的セグメントの `params` は Promise★（`app/api/prechecks/[id]/status/route.ts:88-95` に合わせる）。
5. 次の grep で ★`robot_error_reports` の読み取りがすべて定数の `select` を使っている★ことを確かめる（本書で足す読み取りも同じにする）:
   ```
   grep -rn "robotErrorReport\." lib app --include=*.ts | grep -v __tests__
   ```

---

## 段階1 — 期待値表をテストにする（実装より先）

★この時点ではテストが落ちる（関数・型が無い）のが正しい★。コミットはテストだけで1つ（「テストだけ。この時点では落ちる」と明記）。

1. ★`lib/db/__tests__/error-report-mapping.test.ts`（新規）★:
   - 冒頭コメントに表1・表3 を写し、「★表1 は監督確定（D-160 / 指示書AW §3）。変えないこと★」「表2・表3 は指示書AW で導いたもの」と書く
   - `errorReportItemFromRow()`: E1〜E9 の各行 → `schedule_link` / `precheck_minutes` / `detected_at` / `serial_number` / `error_type` / `error_level` が表1 どおり（`it.each`）
   - `errorScheduleLink()`: NULL → `none`／予定が `undefined` → `unknown`／本清掃 → `main`／`PRECHECK_MINUTES_BEFORE[1]` → `precheck`・その分数／10 → `precheck`・10／★予定の id と `clean_timetable_id` が違う → 例外★（呼び出しの誤り）
   - `errorReportKeyFromRow()`: `detected_at` が E7 で `created_at`、それ以外で `errored_at`（`+09:00` の ISO。★壁時計値のまま★ — `toDbJst(new Date("2026-10-05T19:05:00+09:00"))` → `"2026-10-05T19:05:00+09:00"`）
   - ★`status` に 3 が入った行でも `errorReportKeyFromRow()` は例外にならない★（キーは生の数値を持つ。表2 F12）
2. ★`lib/error-reports/__tests__/query.test.ts`（新規）★:
   - `filterSortErrorReportKeys()`: 表2 F1・F2・F4・F5・F6・F8・F12 と、★表1 の並び順★（E7 が先頭・同時刻は `"E-10"` が `"E-9"` より先）
   - `errorReportStatusCodes(false)` = 未解消・対応中のコード、`(true)` = それに解消を足したもの（★期待値も `robotErrorStatusFromResolutionStatus()` から作る★。0/1/2 を直書きしない）
   - `parseErrorReportQuery()`: F3（422）・F7（422）・日付の形式（422）・`group_id=0`（422。`parseMasterIdFilters()` に任せる）・`page` / `per_page` の丸め（`parsePrecheckQuery()` と同じ）・`error_type` / `facility_name` は前後の空白を除く
3. ★`lib/error-reports/__tests__/label.test.ts`（新規）★: 表3 S1〜S6（`t` は恒等関数と EN 辞書の2通り）
4. ★`lib/prechecks/__tests__/status-cycle.test.ts`（新規）★: 未解消 → 対応中 → 解消 → 未解消（3件）
5. `lib/db/__tests__/robot-error-select.test.ts`: `SELECTS`（`:18-23`）に段階2-3 の2つの定数を足す（★`robot_id` / `robot` を含まない・`clean_timetable_id` を含む★がそのまま効く）

---

## 段階2 — 型・純粋関数

### 2-1. `lib/types/index.ts`

`PrecheckDbQuery`（`:592` 付近）の後に足す。★YAML のスキーマ名と揃える★（`CLAUDE.md` §9.2）:

```ts
/** エラー一覧の「紐づく予定」（指示書AW D4）。`d-watch-api.yaml` の `ErrorReportItem.schedule_link` */
export type ErrorScheduleLink = "main" | "precheck" | "none" | "unknown";

/** `GET /errors` の1行（`robot_error_reports` の1行。指示書AW）。`d-watch-api.yaml` の `ErrorReportItem` */
export interface ErrorReportItem {
  /** `robot_error_reports.error_id`（★string のまま。`Number()` を通さない。D-105 項2★） */
  id: string;
  /** `errored_at ?? created_at`（`detectedAtFromError()`）。ISO8601（+09:00） */
  detected_at: string;
  facility_id: number;
  facility_name: string;
  /** ★生値★（`'0'` もそのまま。D-077） */
  serial_number: string;
  /** ★生値★（区分を作らない。D-077 / D-160 項3） */
  error_type: string;
  /** ★生値★ */
  error_level: string;
  /** ★画面には出さない★（「予定不明」の調査用。D18） */
  clean_timetable_id: number | null;
  schedule_link: ErrorScheduleLink;
  /** `schedule_link === "precheck"` のときだけ数値（★生の分数★）。それ以外は null */
  precheck_minutes: number | null;
  /** ★`PrecheckStatus` を使い回す★（名前は歴史的なもの。§11 Q7） */
  resolution_status: PrecheckStatus;
  responded_at: string | null;
  resolved_at: string | null;
  note: string | null;
}

/** `DataSource.errorReports()` に渡す取得条件（指示書AW）。★引数は必須★ */
export interface ErrorReportDbQuery {
  /** false = 未解消・対応中だけ / true = 解消も含める */
  include_resolved: boolean;
  /** ★`null` = 施設で絞らない / `[]` = 0件★（罠1） */
  facility_ids: number[] | null;
  /** JST の `YYYY-MM-DD`（検知時刻の日付）。★下限なしは `null`★ */
  date_from: string | null;
  /** JST の `YYYY-MM-DD`（★その日を含む★）。★上限なしは `null`★ */
  date_to: string | null;
  /** 種類の部分一致（大小文字を区別しない）。★`""` = 絞らない★ */
  error_type: string;
  page: number;
  per_page: number;
}
```

★`PrecheckStatusUpdateRequest` / `PrecheckStatusPatch`（`:542` / `:556`）は ★そのまま使う★（新しい型を作らない。D11）。

### 2-2. `lib/prechecks/status-cycle.ts`（新規）

`components/features/alerts/AlertsScreen.tsx:76-89` の `nextResolutionStatus()` を ★docstring ごと★ 移し、`export` する。★中身は1文字も変えない★。

### 2-3. `lib/db/robot-error-select.ts` — 定数を2つ足す（`:51-64` の後）

```ts
/**
 * エラー一覧の1段目（キーだけ。指示書AW D8）。絞り込み・並べ替えに要る列だけ。
 * ★`clean_timetable_id` は並べ替えには使わないが、select の共通テスト（紐づけのキーを含む）のために読む★
 */
export const ROBOT_ERROR_LIST_KEY_SELECT = {
  error_id: true,
  clean_timetable_id: true,
  facility_id: true,
  error_type: true,
  errored_at: true,
  created_at: true,
  status: true,
} as const satisfies Prisma.RobotErrorReportSelect;

/**
 * エラー一覧の2段目・PATCH の前後の読み取り（`errorReportItemFromRow()`）に要る列。
 * ★`errorReports()` と `errorReportById()` が同じ定数を使う★（行の作り方を1本にする。AT 段階4-1 と同じ理由）
 */
export const ROBOT_ERROR_LIST_ROW_SELECT = {
  error_id: true,
  clean_timetable_id: true,
  serial_number: true,
  facility_id: true,
  facility: { select: { facility_name: true } },
  error_type: true,
  error_level: true,
  errored_at: true,
  created_at: true,
  status: true,
  responded_at: true,
  resolved_at: true,
  comment: true,
} as const satisfies Prisma.RobotErrorReportSelect;
```

★`robot_id` / `robot` / `section` を足さない★（4-1）。

### 2-4. `lib/db/error-report-mapping.ts`（新規）— ★純粋関数のみ★（`lib/db/precheck-mapping.ts` と同じ作法）

```ts
/** 1段目の行（`ROBOT_ERROR_LIST_KEY_SELECT` の結果がそのまま入る形） */
export interface ErrorReportKeySource extends PrecheckDetectedAtSource {
  error_id: string;
  facility_id: bigint;
  error_type: string;
  status: number;
}
/** 絞り込み・並べ替えに使うキー（★`status` は生の数値★。表2 F12） */
export interface ErrorReportKey {
  error_id: string;
  facility_id: number;
  error_type: string;
  /** `jstIsoFromDb(detectedAtFromError(row))` */
  detected_at: string;
  status: number;
}
/** 2段目の行（`ROBOT_ERROR_LIST_ROW_SELECT` の結果がそのまま入る形） */
export interface ErrorReportRowSource extends PrecheckDetectedAtSource {
  error_id: string;
  clean_timetable_id: bigint | null;
  serial_number: string;
  facility_id: bigint;
  facility: { facility_name: string };
  error_type: string;
  error_level: string;
  status: number;
  responded_at: Date | null;
  resolved_at: Date | null;
  comment: string | null;
}
/** 予定（本表・履歴のどちらか。`findTimetables*()` の `select: { id: true, precheck: true }`） */
export interface ErrorTimetableRef { id: bigint; precheck: number }

export function errorReportKeyFromRow(row: ErrorReportKeySource): ErrorReportKey
export function errorScheduleLink(
  cleanTimetableId: bigint | null,
  timetable: ErrorTimetableRef | undefined
): { schedule_link: ErrorScheduleLink; precheck_minutes: number | null }
export function errorReportItemFromRow(
  row: ErrorReportRowSource,
  timetable: ErrorTimetableRef | undefined
): ErrorReportItem
export function errorReportListWhere(query: ErrorReportDbQuery): Prisma.RobotErrorReportWhereInput
```

- `errorScheduleLink()`: ①`cleanTimetableId === null` → `none` ②`timetable === undefined` → `unknown` ③★`timetable.id !== cleanTimetableId` → 例外★（呼び出しの誤り）④`precheckMinutesFromCode(timetable.precheck)`（`lib/db/precheck-mapping.ts:97`）が null → `main` ⑤それ以外 → `precheck`・その値。
  ★`CLEAN_TIMETABLE_PRECHECK.main` を自分で比べない★（`precheckMinutesFromCode()` の1本）
- `errorReportItemFromRow()`:
  - `id: row.error_id`（★string のまま★）／`detected_at: jstIsoFromDb(detectedAtFromError(row))`
  - `facility_id: Number(row.facility_id)`／`clean_timetable_id: row.clean_timetable_id === null ? null : Number(row.clean_timetable_id)`（★BigInt を残すと `NextResponse.json` が例外★）
  - `serial_number` / `error_type` / `error_level` は ★そのまま★
  - `resolution_status: resolutionStatusFromRobotErrorStatus(row.status)`（★3以上は例外★ — 1段目で外れているので一覧では来ない。PATCH の1件読みで来たら500。fail-loud）
  - `responded_at` / `resolved_at` は `jstIsoFromDb()`（null はそのまま）／`note: row.comment`
  - ★`precheckItemFromErrorRow()`（`:202-238`）と同じ書き方で揃える★（日時・BigInt の扱い）
- `errorReportListWhere()`（★SQL 側の写し。正は 2-5 の純粋関数★）:
  ```ts
  const range = dbDateRange(query.date_from, query.date_to);
  return {
    status: { in: errorReportStatusCodes(query.include_resolved) },
    ...(query.facility_ids !== null
      ? { facility_id: { in: query.facility_ids.map((id) => BigInt(id)) } }
      : {}),
    ...(range.gte || range.lt
      ? { OR: [{ errored_at: range }, { errored_at: null, created_at: range }] }
      : {}),
  };
  ```
  ★`error_type` は SQL に降ろさない★（D9）。★`page` / `per_page` も降ろさない★（D8）

### 2-5. `lib/error-reports/query.ts`（新規）

```ts
export interface ErrorReportQuery {
  dateFrom: string | null;   // ★空文字は null に正規化★（`parsePrecheckQuery()` の注記と同じ理由）
  dateTo: string | null;
  includeResolved: boolean;
  facilityName: string;      // trim 済み。"" = 絞らない
  errorType: string;         // trim 済み。"" = 絞らない
  groupId: number | null;
  customerId: number | null;
  page: number;
  perPage: number;
}
export function parseErrorReportQuery(searchParams: URLSearchParams): QueryParseResult<ErrorReportQuery>
export function errorReportStatusCodes(includeResolved: boolean): number[]
export function compareErrorReportKeys(a: ErrorReportKey, b: ErrorReportKey): number
export function filterSortErrorReportKeys(keys: readonly ErrorReportKey[], query: ErrorReportDbQuery): ErrorReportKey[]
```

- `parseErrorReportQuery()`: 日付の形式は `parseDate()`（`lib/clean-logs/query.ts`）、★`date_from > date_to` は 422（表2 F7）★、★期間の日数の上限は設けない★（`/prechecks` と同じ。§11 Q13）／
  `include_resolved` は ★無い・空・`"false"` → false、`"true"` → true、それ以外 → 422★（表2 F3）／分類は `parseMasterIdFilters()`（`lib/api/master-id.ts:93`）／
  `page` / `per_page` は `parsePrecheckQuery()`（`lib/prechecks/query.ts`）と同じ丸め（`PAGINATION`）。★エラーに入力値をエコーしない★
- `errorReportStatusCodes()`: `(includeResolved ? ["unresolved", "in_progress", "resolved"] : ["unresolved", "in_progress"]).map(robotErrorStatusFromResolutionStatus)`（D2）
- `compareErrorReportKeys()`: ★`Date.parse(b.detected_at) - Date.parse(a.detected_at)`★、0 なら ★`a.error_id < b.error_id ? -1 : a.error_id > b.error_id ? 1 : 0`★（`localeCompare` を使わない — 環境で順が変わる）
- `filterSortErrorReportKeys()` — ★絞り込みの「正」★（SQL はその写し。指示書AC C-5 と同じ考え方）:
  1. `status` が `errorReportStatusCodes(query.include_resolved)` に入る（★生の数値で比べる★）
  2. `query.facility_ids !== null` なら `facility_id` が入る（罠1）
  3. ★`detected_at.slice(0, 10)`★ が `date_from` 以上・`date_to` 以下（★文字列で比べる★。`filterFixturePrechecks()` `lib/db/fixture-prechecks.ts:33-38` と同じ）
  4. `query.error_type !== ""` なら `includesFoldPlain(key.error_type, query.error_type)`
  5. `compareErrorReportKeys()` で並べる（★入力の配列を書き換えない★。コピーしてから `sort`）

### 2-6. `lib/error-reports/label.ts`（新規）

`errorScheduleLabel(t: Translate, item: Pick<ErrorReportItem, "schedule_link" | "precheck_minutes">): string` — 表3。
`Translate` は `lib/prechecks/label.ts` の型を import する。`{n}` は replace（`precheckSeverityLabel()` と同じ方式A）。

段階1 のテストが通ること。コミット。

---

## 段階3 — データ層

### 3-1. `DataSource`（`lib/db/index.ts:205`。`updatePrecheckErrorColumns()` の宣言 `:300-308` の直後）

```ts
/**
 * エラー一覧（`GET /errors`。指示書AW）。★`robot_error_reports` の全行★が対象（事前チェック・本清掃・予定なし・`robot_id` NULL を問わない）。
 * ★2段階で読む★（D8）: キーだけを SQL で `status IN`・`facility_id IN`・期間で絞って読み、
 *   `filterSortErrorReportKeys()` で絞り込み・並べ替え → ページの `error_id` だけ列を足して読む。
 * ★`total` は絞り込み後・ページ分け前の件数★（`meta.total`）。
 */
errorReports(query: ErrorReportDbQuery): Promise<{ items: ErrorReportItem[]; total: number }>;
/**
 * `PATCH /errors/{error_id}/status` の前後の読み取り。★`error_id === id` の行があれば返す（事前チェックかどうかを問わない）★。
 * ★大小文字は JS の `===` で決める★（D-4。`precheckItemById()` と同じ）。★期間・施設・状態では絞らない★
 */
errorReportById(id: string): Promise<ErrorReportItem | null>;
/**
 * 4カラムを書く（`writeRobotErrorColumns()` の1本。★書き込み用の接続★）。
 * ★`matched` で存在を判定しないこと★ — 存在確認は書く前に `errorReportById()` で行う
 */
updateErrorReportColumns(errorId: string, patch: PrecheckStatusPatch): Promise<{ matched: number }>;
```

### 3-2. fixtures のデータ — `lib/fixtures/error-reports.ts`（新規）

- ★DB の行の形★（D13）で持つ: `FixtureErrorReportRow`（＝ `ErrorReportRowSource` ＋ ★`robot_id: bigint | null`（読まない。実データの形を示すためだけ）★）。
  日時は ★`toDbJst(new Date(at(n, "HH:MM")))`★（`at()` は `lib/fixtures/_helpers.ts`）。施設名は `facilities`（`lib/fixtures/facilities.ts`）から id で引いて入れる（★施設名を直書きしない★）
- 予定は `errorReportTimetables: ErrorTimetableRef[]`（★fixtures では本表・履歴を分けない★。E6 相当の行にはコメントで「実DBでは履歴にだけある想定」と書く）
- ★入れる行（監督の指定＋表1）★。id は `"FX-ERR-01"` 〜（★`lib/fixtures/prechecks.ts` の id と重ならない★）。値は ★ダミー★（実在の店名・住所を入れない。`CLAUDE.md` §7）:

  | id | 内容 | `status` | 予定 | 備考 |
  |---|---|---|---|---|
  | FX-ERR-01 | ロボットのエラー（例 `EmergencyStop` / `ERROR`）・本清掃に紐づく | 0 | 本清掃 | E1 |
  | FX-ERR-02 | ロボットのエラー・事前チェック（`PRECHECK_MINUTES_BEFORE[1]`）に紐づく | 1（`responded_at` あり） | 事前チェック | E2 |
  | FX-ERR-03 | 解消済み・本清掃（`responded_at` / `resolved_at` / 備考あり） | 2 | 本清掃 | E3 |
  | FX-ERR-04 | `clean_timetable_id` NULL | 0 | なし | E4 |
  | FX-ERR-05 | `clean_timetable_id` が予定に無い id | 0 | 不明 | E5 |
  | FX-ERR-06 | 予定が履歴にだけある想定・本清掃 | 0 | 本清掃 | E6 |
  | FX-ERR-07 | `errored_at` NULL（`created_at` だけ） | 0 | 本清掃 | E7 |
  | FX-ERR-08 | ★`robot_id` NULL・`serial_number` `'0'`・`error_type` `SECURITY_ON`★ | 0 | 本清掃 | E8 |
  | FX-ERR-09 | 事前チェック（★`10` を直書き。設定に無い値★） | 0 | 事前チェック | E9 |
  | FX-ERR-10 | ★`robot_id` NULL・`serial_number` = 制御盤の SN（`lib/fixtures/ds-devices.ts` の `DS-001`）・`error_type` `D-S_CONTROL_FAILURE`★ | 0 | 本清掃 | P-8 a |
  | FX-ERR-11 | ★`robot_id` NULL・`serial_number` `'0'`・`error_type` `NOT_STARTED`・`error_level` `CRITICAL`★ | 0 | 本清掃 | ステージング実測の形（D-158 2026-10-06 実測・先方投入） |

  ★`error_level` の値（`CRITICAL` 以外）と `SECURITY_ON` / `D-S_CONTROL_FAILURE` のレベルは実データ未受領のダミー★ — ファイル冒頭のコメントに書く。
- `lib/fixtures/index.ts` に `export * from "./error-reports";` を1行足す

### 3-3. fixtures の上書き表 — `lib/db/fixture-error-report-overrides.ts`（新規）

`lib/db/fixture-precheck-overrides.ts` と同じ形で、★値は DB の列の形★（D14）:

```ts
const overrides = new Map<string, RobotErrorColumnsData>();
export function resetFixtureErrorReportOverrides(): void
/** 上書きを反映した行（★元の配列の要素は書き換えない★）。`comment` は `status` と同じく上書きのキーがあるときだけ差し替える */
export function withFixtureErrorReportOverride<T extends FixtureErrorReportRow>(row: T): T
/** `robotErrorColumnsFromPatch(patch)` で DB の列に写し、既存の上書きに重ねる。★`error_id === id`★ が無ければ `matched: 0` */
export function applyFixtureErrorReportPatch(rows: readonly FixtureErrorReportRow[], id: string, patch: PrecheckStatusPatch): { matched: number }
```

- ★`robotErrorColumnsFromPatch()` を通す★ので、4つ以外のキー・空パッチは Prisma 版と同じく例外になり、日時は ★`toDbJst()` 済み★（読み出しは `jstIsoFromDb()` で戻る）
- ★プロセスを再起動すると消える★（AT と同じ。D-045）
- ★AT の `fixture-precheck-overrides.ts` とは状態を共有しない★（id が重ならないので食い違いは起きない。§11 Q8）

### 3-4. fixtures 版（`fixtureSource`。`lib/db/index.ts:548-563` の事前チェックの3メソッドの後）

```ts
async errorReports(query: ErrorReportDbQuery) {
  const rows = errorReportRows.map(withFixtureErrorReportOverride);
  const keys = filterSortErrorReportKeys(rows.map(errorReportKeyFromRow), query);
  const start = (query.page - 1) * query.per_page;
  const pageIds = keys.slice(start, start + query.per_page).map((k) => k.error_id);
  // 以下、Prisma 版と同じ: id で行を引き、予定を id で引いて errorReportItemFromRow()
  ...
  return { items, total: keys.length };
},
async errorReportById(id) { /* `===` で探し、上書きを反映して errorReportItemFromRow() */ },
async updateErrorReportColumns(errorId, patch) { return applyFixtureErrorReportPatch(errorReportRows, errorId, patch); },
```

★予定の引き当て（`clean_timetable_id` → `ErrorTimetableRef`）は `.toString()` をキーにした Map★（4-9）。
★ページの切り出し・行の組み立てを Prisma 版と同じ関数にしたければ、非公開の関数（例 `pageOfErrorReports()`）に切り出して2本で共有してよい★（★規則を2箇所に書かない★）。

### 3-5. Prisma 版（`prismaSource`。`updatePrecheckErrorColumns()` `lib/db/index.ts:1556-1562` の後）

```ts
async errorReports(query: ErrorReportDbQuery) {
  const { getPrisma } = await import("./prisma");
  const prisma = await getPrisma();
  // 1段目: キーだけ（★SQL で status / 施設 / 期間を絞る。並べ替え・ページ分けはしない★。D8）
  const keyRows = await prisma.robotErrorReport.findMany({
    where: errorReportListWhere(query),
    select: ROBOT_ERROR_LIST_KEY_SELECT,
  });
  const keys = filterSortErrorReportKeys(keyRows.map(errorReportKeyFromRow), query);
  const start = (query.page - 1) * query.per_page;
  const pageIds = keys.slice(start, start + query.per_page).map((k) => k.error_id);
  if (pageIds.length === 0) return { items: [], total: keys.length };   // ★0件なら2段目を撃たない★
  // 2段目: そのページの行だけ列を足して読む
  const rows = (await Promise.all(
    chunkForInClause(pageIds).map((ids) =>
      prisma.robotErrorReport.findMany({ where: { error_id: { in: ids } }, select: ROBOT_ERROR_LIST_ROW_SELECT })
    )
  )).flat();
  // 予定は2表から id で（D5）。★id が0件ならクエリを撃たない★（findTimetablesByChunks が塊0個で返す）
  const timetableIds = [...new Set(rows.flatMap((r) => (r.clean_timetable_id === null ? [] : [r.clean_timetable_id.toString()])))].map(BigInt);
  const timetables = await findTimetablesByChunks(prisma, chunkForInClause(timetableIds), (ids) => ({
    where: { id: { in: ids } },
    select: { id: true, precheck: true },
  }));
  ...（rowById / timetableById を `.toString()` / `error_id` で引き、★pageIds の順★に並べて errorReportItemFromRow()。見つからない error_id は飛ばす（4-15））
  return { items, total: keys.length };
},
```

- ★`errorReportById()`★: `findUnique({ where: { error_id: id }, select: ROBOT_ERROR_LIST_ROW_SELECT })` → ★`report.error_id !== id` なら null★（4-8）→
  `clean_timetable_id` があれば `findTimetableById(prisma, report.clean_timetable_id, { id: true, precheck: true })`（`lib/db/timetable-source.ts:285`）→ `errorReportItemFromRow()`
- ★`updateErrorReportColumns()`★: `updatePrecheckErrorColumns()`（`:1556-1562`）と ★同じ中身★（`getWritePrisma()` → `writeRobotErrorColumns()`）。
  ★同じ3行を2回書きたくなければ、非公開の関数に切り出して2本から呼んでよい★（`updatePrecheckErrorColumns()` の挙動は変えない）
- import は `lib/db/index.ts:97-127` の並びに足す（`ROBOT_ERROR_LIST_*_SELECT`・`error-report-mapping`・`fixture-error-report-overrides`・`errorReportRows` / `errorReportTimetables`）

### 3-6. テスト

1. ★`lib/db/__tests__/error-report-mapping.test.ts`★ に `errorReportListWhere()` を足す:
   既定 → `status: { in: [未解消, 対応中のコード] }` だけ／`facility_ids: []` → `facility_id: { in: [] }`（★キーが消えない★）／`null` → `facility_id` のキーが無い／
   期間あり → `OR` の2要素で、`gte` / `lt` が ★壁時計値★（`date_from = "2026-10-05"` → `gte` = `2026-10-05T00:00:00.000Z`、`date_to = "2026-10-05"` → `lt` = `2026-10-06T00:00:00.000Z`）／期間なし → `OR` のキーが無い
2. ★`lib/db/__tests__/fixture-error-reports.test.ts`（新規）★: fixtures の11行が ★`errorReportKeyFromRow()` / `errorReportItemFromRow()` を例外なく通る★／`FX-ERR-03` は既定で出ない／
   `applyFixtureErrorReportPatch()` → `errorReports()` に反映される／`resetFixtureErrorReportOverrides()` で戻る／存在しない id・大小文字違いは `matched: 0`／★4つ以外のキーは例外★

コミット。

---

## 段階4 — API（YAML → 型 → ルート）

### 4-1. `d-watch-api.yaml`（★先に直す。`CLAUDE.md` §6-1★）

1. `tags`（`:39-62`）の `prechecks`（`:46-47`）の後に `- name: errors` / `description: エラー一覧(robot_error_reports)` を足す
2. `/prechecks/{id}/status`（`:690-755`）の後・`# ---- reports`（`:757`）の前に ★`/errors`★ と ★`/errors/{error_id}/status`★ を足す:
   - `GET /errors`: `description` に ★対象は robot_error_reports の全行（事前チェック・本清掃・予定なし・制御盤・セキュリティ・清掃開始失敗を問わない）★／既定は未解消・対応中、`include_resolved=true` で解消も／
     ★並び順は detected_at（= errored_at、無ければ created_at）の降順、同値は id（error_id）の昇順（文字列の辞書順）★／★error_type・error_level・serial_number は DB の値のまま（D-077）★／`/alerts` との違い（あちらは事前チェック＋出発失敗だけ）。
     パラメータ: `date_from` / `date_to`（★検知時刻の日付★。`date_from > date_to` は422）/ `$ref GroupId` / `$ref CustomerId` / `facility_name`（部分一致。記号を無視）/ `error_type`（部分一致・大小文字を区別しない）/ `include_resolved`（`type: boolean`、`default: false`。`true` / `false` 以外は422）/ `$ref Page` / `$ref PerPage`。
     200 は `ErrorReportListResponse`、422 は `ValidationError`
   - `PATCH /errors/{error_id}/status`: ★`/prechecks/{id}/status` の `description`（`:694-713`）の日時・備考の規則をそのまま写す★。違いは ★「robot_error_reports に error_id の行があれば書ける（事前チェックかどうかを問わない）。無ければ404」★だけ。
     パスパラメータは `/prechecks/{id}/status` と同じくインラインの `{ type: string, maxLength: 50 }`（★共有の `PathId`（integer）を参照しない★。`:715-717` のコメントと同じ理由）。
     リクエストは `/prechecks/{id}/status` と同じ（`status` 必須・`note` は `maxLength: 400`）。200 は `ErrorReportItem`、404 は `Error`、422 は `ValidationError`
3. `components/schemas` の `PrecheckListResponse`（`:1882` 付近）の後に ★`ErrorReportItem`★（2-1 の型と同じ。`schedule_link` は `enum: [main, precheck, none, unknown]` と4区分の説明・`precheck_minutes` は「schedule_link=precheck のときだけ。生の分数」）と ★`ErrorReportListResponse`★（`items` + `meta: $ref Meta`。★`summary` は無い★）を足す

### 4-2. `GET /errors` — `app/api/errors/route.ts`（新規）

形は `app/api/clean-logs/route.ts`（動く見本）と `app/api/prechecks/route.ts` に揃える:

1. `parseErrorReportQuery()` → 失敗は 422
2. ★`try` の中で★ `Promise.all([resolveFacilityIdsByName(q.facilityName), resolveFacilityIdsByMasters(q)])` → `intersectFacilityIds()`（D10）→ `db.errorReports({ include_resolved, facility_ids, date_from, date_to, error_type, page, per_page })`
3. 例外は 500（`logApiError`。文言「データの取得に失敗しました」）
4. `ListResponse<ErrorReportItem>`（`{ items, meta: { total, page, per_page } }`）を返す。`withRequestId()` で包む

### 4-3. `PATCH /errors/{error_id}/status` — `app/api/errors/[error_id]/status/route.ts`（新規）

★`app/api/prechecks/[id]/status/route.ts`（全173行）と同じ順序・同じ部品★（形式 → 本文 → 実在 → 計画 → 書き込み → 読み直し）。違いだけ書く:

| 箇所 | `/prechecks` | `/errors` |
|---|---|---|
| `ROUTE` | `/api/prechecks/[id]/status` | ★`/api/errors/[error_id]/status`★ |
| `params` | `{ id }` | ★`{ error_id }`★ |
| 実在・読み直し | `db.precheckItemById()` | ★`db.errorReportById()`★ |
| 書き込み | `db.updatePrecheckErrorColumns()` | ★`db.updateErrorReportColumns()`★ |
| 404 の文言 | 対応状況を記録できるアラートが見つかりません（出発失敗には記録先がありません） | ★「エラーが見つかりません」★ |
| 監査ログの `before` の型 | `PrecheckItem` | ★`ErrorReportItem`★（4項目の名前は同じ） |

- 同じものを使う: `parsePathErrorId()`（`lib/api/path-params.ts:89`）／`parsePrecheckStatusUpdateBody()`／`planPrecheckStatusUpdate()`／`isEmptyPatch()`／`columnsForPatch()`（`lib/db/robot-error-write.ts:135`）／`logDbWrite()` / `dbErrorCode()`（`lib/logger/db-write.ts:53` / `:22`）
- ★`const now = new Date()` は1回だけ★。★空パッチなら書かずに 200 で `before`・`db.write` も出さない★。★`matched === 0` は 404・`db.write`（`success: false`, `db_error_code: "no_matching_row"`）★。★書き込みの例外は 500・`db.write`（`success: false`）★。★成功時の `db.write` は書き込み直後に1件★
- ★監査ログに備考の中身を載せない★（`comment_changed: boolean` だけ。`docs/logging-design.md` の禁止キー）
- ★`auditBase()` 等を `/prechecks` のルートと共有するための書き換えはしない★（D12）。同じ形の小さな関数を新しいルートに置く

### 4-4. テスト

1. ★`app/api/errors/__tests__/route.test.ts`（新規。Prisma 経路）★ — `app/api/prechecks/[id]/status/__tests__/route.test.ts:1-110` の作法（`vi.mock("@/lib/db/use-fixtures", () => ({ USE_FIXTURES: false }))`・`@/lib/db/prisma` をスタブ）:
   - スタブの `robotErrorReport.findMany` は ★`guardNullRobotId()` を通し★、★`select` を見て返す列を絞る★（1段目と2段目で返す形が違う）。`where` は ★`status.in` / `facility_id.in` / `OR`（期間）/ `error_id.in` を評価★する
   - `cleanTimetable.findMany` / `cleanTimetableHistory.findMany` を別の配列から返す（E6 は履歴だけ）。`facilityNameIndex` 等、施設名・分類の解決に要るものは既存の `app/api/__tests__/master-filter.test.ts` の作法を見て用意する
   - ★表1 の9行で★: 既定は E3 以外の8行・並びが表1 のとおり（E7 が先頭・同時刻は `"E-10"` が先）・各行の `schedule_link` / `precheck_minutes` / `detected_at` / `serial_number` / `error_type` が表1 どおり。`include_resolved=true` で9行
   - 表2 F3・F4・F6・F7・F8・F10・F11・F12
   - ★1段目の `where` が `errorReportListWhere()` の戻りと `toEqual`★（ルートが別の条件を組み立てていない）
   - ★1段目の `select` が `ROBOT_ERROR_LIST_KEY_SELECT`、2段目が `ROBOT_ERROR_LIST_ROW_SELECT`★／★0件のとき2段目と予定のクエリを撃たない★
2. ★`app/api/errors/[error_id]/status/__tests__/route.test.ts`（新規。Prisma 経路）★ — `/prechecks` の同名テストの describe を写し、行を表1 にする:
   - ★200: E1（本清掃）・E4（予定なし）・E5（予定不明）・E8（`robot_id` NULL）・E2（事前チェック）★ に書ける。`updateMany` の `data` が該当の列だけ
   - AT §3-4 の代表（#1・#2・#4・#8・#9）・備考だけ・備考を空に・★空パッチなら `updateMany` を呼ばない・`db.write` も出さない★
   - ★本文に `updated_at`・`error_type`・`memo` を混ぜても `data` に出ない★
   - ★404: 行が無い／★大小文字違い（`e-1` で `E-1`）★／`matched === 0`★
   - 422: id（空・51文字）・JSON・status・note の型・401文字（`PRECHECK_NOTE_MAX_LENGTH + 1`）
   - ★書き込みの例外 → 500。`db.write`（`success: false`）と `api.error` が1件ずつ・コードだけ★
   - ★`db.write`: `route` が `/api/errors/[error_id]/status`・`record_id` が文字列・`columns` が実際に SET した列だけ・備考の中身を含まない★
   - ★参照用の接続で `updateMany` を呼ばない★（スタブの参照用は `updateMany` で例外。`/prechecks` のテスト `:96-98` と同じ）
3. ★`app/api/errors/__tests__/route.fixtures.test.ts`（新規）★: fixtures で GET（既定で `FX-ERR-03` が出ない・`include_resolved=true` で11行）→ PATCH → GET に反映される／存在しない id は404。`beforeEach` で `resetFixtureErrorReportOverrides()`
4. ★既存のテストが無修正で通ること★: `app/api/prechecks/__tests__/route.test.ts`・`app/api/prechecks/[id]/status/__tests__/*.test.ts`・`lib/db/__tests__/robot-error-write-sql.test.ts`（★`/prechecks` の挙動と書き込みの SQL が変わっていない証拠★）

YAML → ルート → テストの順でコミット（1つでも分けてもよい）。

---

## 段階5 — 画面

### 5-1. メニュー — `components/layout/AppShell.tsx`

`MENU_MONITORING`（`:8-23`）の ★`/alerts` の行（`:10`）の直後★ に足す:

```ts
// ★エラー一覧（D-160 / 指示書AW）★ — robot_error_reports の未対応エラーを全部出す（/alerts は事前チェック＋出発失敗だけ）。
//   ★契約によるメニューの出し分けは無い（このファイルの全メニューと同じ。指示書AW D15）★
{ href: "/errors", id: "errors", label: "🚨 エラー一覧" },
```

★ラベルとアイコンは暫定（§11 Q6）★。モックにこのページは無い（★モックは書き換えない★）。

### 5-2. ページ — `app/(dashboard)/errors/page.tsx`（新規）

`app/(dashboard)/alerts/page.tsx`（5行）と同じ形で `<ErrorsScreen />` を返す。

### 5-3. `components/features/errors/ErrorsScreen.tsx` ＋ `.module.css`（新規）

★見た目の作法は既存の画面から借りる。新しい共通部品は作らない★（AT §3-8 と同じ考え方）。

- **見出し**: `t("エラー一覧")`、副題 `t("未対応のエラー（ロボット・制御盤・セキュリティ・清掃開始失敗）")`。件数は `t("全{n}件").replace("{n}", String(total))`（★`"{n}件"` は使わない★。4-18）
- **ツールバー**（★1操作＝1確定。「検索」ボタンは置かない★。`AlertsScreen.tsx:146-155` の判断と同じ）:
  - 期間2つ（`<input type="date">`。★初期値は空＝期間なし★。空の側は URL に付けない。両方空のあいだは `t("指定なし（全期間）")` を出す — 清掃詳細ログ（AV 段階5b。`137a94b`）と同じ見せ方。`lib/i18n.ts:363` の既存キー）
  - 分類のセレクト（`AlertsScreen.tsx:358-410` をそのまま写す。`useClassificationFilter(t)`・`appendClassificationParams()`（`lib/masters/options.ts:218`）。★option のラベルを `t()` に通さない★）
  - 施設名・種類のテキスト入力（★300ms の debounce★。`DetailLogScreen.tsx:110-117` の作法。placeholder は `t("施設名")` / `t("種類")`）
  - チェックボックス `t("解消済みも表示")`（既定オフ）
  - ★どれを変えても `setPage(1)` をその場で★（`AlertsScreen.tsx:162-170` の注記）
- **表**（★`table-layout: fixed` ＋ `<colgroup>`★。`CleanLogTable.module.css:30-50` の作法で、列幅を CSS 変数にし `min-width` を合計にする。★狭い画面では表を横スクロールの箱に入れる★）。列と幅の目安（★幅は実装者が調整してよい。ただし固定にする★）:

  | 列 | 中身 | 幅の目安 |
  |---|---|---|
  | `t("検知時刻")` | `formatMd` + `formatHm`（例 `10/5 19:05`。`AlertsScreen.tsx:42-44`） | 96px |
  | `t("施設名")` | `facility_name` | 残り（最小 140px） |
  | `t("シリアル")` | `serial_number`（★`0` もそのまま★） | 140px |
  | `t("種類")` | `error_type`（★生値。`t()` に通さない★。長い値は折り返す） | 170px |
  | `t("レベル")` | `error_level`（★生値。`t()` に通さない★） | 90px |
  | `t("紐づく予定")` | `errorScheduleLabel(t, item)` | 170px |
  | `t("対応状況")` | ★ボタン★（下） | 150px |
  | `t("備考")` | ★インライン編集★（下） | 220px |

  ★`error_type` / `error_level` / `serial_number` を `t()` に通さない理由★: 辞書に同じ綴りのキーがあると誤って訳される（`AlertsScreen.tsx:363-364` の「屋号名まで通すと辞書の対訳に誤って当たる」と同じ）
- **対応状況のボタン**: `AlertsScreen.tsx:46-72`（`statusBadgeClass()` / `StatusBadgeText`）と `:503-511` の見た目を写す。クリックで ★`nextResolutionStatus()`（`lib/prechecks/status-cycle.ts`）★ の状態を送る（★備考は送らない★）。★ボタンは固定幅★（4-17）
- **備考**: `AlertsScreen.tsx:97-140`（`PrecheckNote`）の作法を写す — クリックで textarea・★`onBlur` で確定★・★変えていなければ送らない★・★失敗時は編集状態のまま★・`maxLength={PRECHECK_NOTE_MAX_LENGTH}`（`lib/config/index.ts:613`）・★備考だけ変えるときも現在の `status` を送る★
- **更新**: `PATCH /api/errors/${encodeURIComponent(item.id)}/status`。★成功したらレスポンスでその1行だけ差し替える★（一覧を取り直さない）。★「解消済みも表示」がオフでも、解消にした行は次に取り直すまで消さない★（押し間違いをその場で戻せる。`AlertsScreen.tsx:242-251` と同じ）。
  ★保存中はその行のボタン・入力を無効★。保存中・失敗の表示は ★その行の直後に `<tr><td colSpan={8}>` で★ 出す（`t(rowError.message)`。サーバーの文言は辞書に無ければ原文）
- **空・読み込み・失敗**: `t("該当するエラーはありません")` / `t("読み込み中…")` / サーバーの文言（`AlertsScreen.tsx:445-450` の作法。★「読み込み中…」は `t()` に通す★）
- **ページ送り**: `Pagination`（`AlertsScreen.tsx:532-537`）。★`meta.total` を使う★。存在しないページに取り残されたら最終ページへ（`AlertsScreen.tsx:219-223`。★`pages > 0` の判定を落とさない★）

### 5-4. `/alerts` の巡回順を共有する — `components/features/alerts/AlertsScreen.tsx`

★変えてよいのはここだけ★（D17・4-16）: `:76-89` の `nextResolutionStatus()` を削り、`lib/prechecks/status-cycle.ts` から import する。★他は1文字も変えない★。

### 5-5. `lib/i18n.ts`

足すキー（★EN は暫定。ユーザーが後でまとめて見直す★）。★キーは画面の原文と1文字も違わないこと（全角括弧・中点）★。重複は lint が落とす:

```ts
// ---- エラー一覧（D-160 / 指示書AW）----
"🚨 エラー一覧": "🚨 Errors",
エラー一覧: "Errors",
"未対応のエラー（ロボット・制御盤・セキュリティ・清掃開始失敗）": "Open errors (robots, control panels, security, start failures)",
"全{n}件": "{n} total",
検知時刻: "Detected at",
種類: "Type",
レベル: "Level",
紐づく予定: "Schedule",
対応状況: "Status",
備考: "Note",
本清掃: "Main run",
"事前チェック（{n}分前）": "Pre-check ({n} min before)",
事前チェック: "Pre-check",
予定なし: "No schedule",
予定不明: "Unknown schedule",
解消済みも表示: "Show resolved",
該当するエラーはありません: "No matching errors",
エラーが見つかりません: "Error not found",
```

★既にあるキーは足さない★: `施設名`・`シリアル`・`未解消`・`対応中`・`解消`・`クリックして備考を編集`・`備考を入力`・`保存中…`・`保存に失敗しました`・`対応状況の更新に失敗しました`・`読み込み中…`・`指定なし（全期間）`・`ホールディングス`・`顧客`（`lib/i18n.ts` で確かめてから足す。AV の実装でキーが増えていないかも見る）。

### 5-6. 確認

★画面のテストは vitest で走らない★（`vitest.config.ts:14`）ので段階7 の画面確認で見る。★巡回順・ラベルは純粋関数のテスト（段階1）で固定してある★。

---

## 段階6 — 資料（本書で直すのは次の2つだけ）

1. `docs/fixtures.md`: ★§13「エラー一覧（error-reports）」を新設★（§12.3 `:439-` の後）。3-2 の表（11行・表1 との対応）と、
   ★既定で10行（`FX-ERR-03` 以外）・「解消済みも表示」で11行★、★`robot_id` は読まない（`select` で絞る）ので fixtures でも値は使わない★、★`error_level` 等はダミー★ を書く
2. ★`d-watch-api.yaml` / `lib/types/index.ts` / ルートの冒頭コメント★（段階4 で書いたもの）

★`CLAUDE.md` / `docs/decisions.md` / `docs/code-map.md` / `docs/spec.md` は監督が直す★。

---

## 段階7 — 全体の確認

1. `npx tsc --noEmit -p .` — 0件
2. `npm run lint` — 0件（★`eslint-disable` を1つも足していない★・i18n の重複キー・`$queryRaw`・`any`・未使用の import が無い）
3. `npm test` — ★全件通過★（段階0 の件数から増えていること）
4. ★`USE_FIXTURES=true` で `npm run build`★ — 成功（PowerShell なら `$env:USE_FIXTURES='true'; npm run build`）。★ワークツリーではなく本体で行う★（AV §13-4。Turbopack が node_modules のジャンクションを拒否する）
5. ★画面で確かめる（`USE_FIXTURES=true`）★: `.claude/launch.json` の ★`d-watch-fixtures`（ポート 3100）★ で起動する。★確かめ終わったら自分で起動した dev サーバーは必ず止める★
   - ★メニュー★: 「モニタリング」の「🔔 アラート」の直後に「🚨 エラー一覧」。EN で "🚨 Errors"
   - ★一覧★: 既定で10行・並びが検知時刻の降順。`FX-ERR-08` のシリアルが「0」、種類が「SECURITY_ON」。`FX-ERR-09` が「事前チェック（10分前）」、`FX-ERR-04` が「予定なし」、`FX-ERR-05` が「予定不明」
   - ★「解消済みも表示」★で11行。★絞り込み★（施設名・種類 `secur`・期間・分類）で件数が変わり、1ページ目に戻る
   - ★状態ボタン★: 未解消 → 対応中 → 解消 → 未解消。★解消にした行が既定表示でも消えない★（取り直すまで）。★備考★を入れる → 空にする
   - ★`/alerts`★ の状態ボタンの巡回順が今までと同じ（D17）
   - ★JP/EN 切替で表の列幅・ボタンの位置が動かない★。375px 幅で表が横スクロールになり、ページ全体が横にはみ出さない（★崩れていたら直さずに報告★。AV §13-4 のサイドバーの件は既知）
6. ★ローカルDB（`dwatch_local`）で確かめる★（`USE_FIXTURES=false`。`npm run seed:local` は ★`lib/db/local-guard.ts` がローカル以外を拒否する★）:
   - `/errors` が出る。★シードの事前チェックのエラー行★が「事前チェック（N分前）」で出る
   - 1行を 未解消 → 対応中 → 解消 → 未解消、備考を入れる → 空にする。サーバーのログに `db.write` が `success: true` で出て、`route` が `/api/errors/[error_id]/status`・`columns` が実際に変えた列だけ・備考の中身が無い
   - ★AT 段階8-2 の列単位 GRANT のローカル専用ユーザーがあれば★、それで書いて ★1143（権限エラー）が出ない★ことも見る（★パスワード・ユーザー名を文書・報告に書かない★）
7. 最後の grep（★0件になること★）:
   ```
   grep -rn "\$queryRaw\|\$executeRaw" app lib components
   grep -rn "status: { in: \[" lib app --include=*.ts | grep -v __tests__
   grep -rn "robot_id: true\|robot: true\|robot: {" lib/db/robot-error-select.ts
   ```

---

## 段階8 — ステージングで確かめる（★条件付き。満たさなければ止まって報告★）

**着手の条件（すべて）**: ステージングに接続できる（★別の認証情報を探したり試したりしない★）／`/errors` に ★未解消・`responded_at` / `resolved_at` / 備考がすべて空★ の行がある。

**手順**（AT 段階9 と同じ。★ステージングへの4カラムの書き込みはユーザーが許可済み。D-152★）:
1. ★書き込むのは1行だけ★。★事前チェック以外の行（本清掃に紐づく・`robot_id` NULL の行など）★から選ぶ（★本書で新しく書けるようになった行で確かめる★）
2. ★操作前の値を記録する★: `error_id` と4列（画面・API の値）。★`mysql` で読むだけ★（★UPDATE / DELETE / INSERT を手で打たない★）
3. 未解消 → 対応中 → 解消 → 未解消、備考を入れる → 空にする
4. ★最後に4列が操作前と完全に一致すること★（`updated_at` は DB が更新するので戻らない — 値を記録して報告。AT §10 #8 で受け入れ済み）
5. ★`robot_id` が NULL の行が一覧に出て、読み取りで落ちない★ことを確かめる（D-158 時点で2行）
6. ★途中でデータが入れ替わったら止めて報告★（第10回 N-6）。★他の行に書いて代わりにしない★
7. ★`npm run seed:local` をステージングに向けない★（テーブルを消して入れ直すので実データが消える）

---

## 8. テスト一覧（本書で足す・直すもの）

| ファイル | 内容 | 段階 |
|---|---|---|
| `lib/db/__tests__/error-report-mapping.test.ts`（新規） | 表1 E1〜E9 の行の組み立て・`errorScheduleLink()`・`errorReportKeyFromRow()`・`errorReportListWhere()` | 1・3 |
| `lib/error-reports/__tests__/query.test.ts`（新規） | 表2・表1 の並び順・`parseErrorReportQuery()`・`errorReportStatusCodes()` | 1 |
| `lib/error-reports/__tests__/label.test.ts`（新規） | 表3 | 1 |
| `lib/prechecks/__tests__/status-cycle.test.ts`（新規） | 巡回順 | 1 |
| `lib/db/__tests__/robot-error-select.test.ts` | `SELECTS` に2つ足す | 1 |
| `lib/db/__tests__/fixture-error-reports.test.ts`（新規） | fixtures の11行・上書き表 | 3 |
| `app/api/errors/__tests__/route.test.ts`（新規） | GET（Prisma 経路。表1・表2・2段階の読み方） | 4 |
| `app/api/errors/[error_id]/status/__tests__/route.test.ts`（新規） | PATCH（Prisma 経路。全行に書ける・404・422・500・`db.write`） | 4 |
| `app/api/errors/__tests__/route.fixtures.test.ts`（新規） | fixtures で GET → PATCH → GET | 4 |
| ★無修正で通ること★ | `app/api/prechecks/**`・`lib/prechecks/__tests__/status-update.test.ts`・`lib/db/__tests__/robot-error-write-sql.test.ts`・`lib/db/__tests__/precheck-mapping.test.ts` | — |

---

## 9. 触ってはいけないもの

- `CLAUDE.md` / `docs/decisions.md` / `docs/code-map.md` / `docs/spec.md`（監督が直す）
- `d-watch-mock.jsx`（このページはモックに無い。モックは書き換えない）
- ★`prisma/schema.prisma` / `lib/generated/prisma/**`★（スキーマは変えない。★`RobotErrorReport.updated_at` に `@updatedAt` を付けない★。`db pull` は実行しない）
- ★`control_error_histories` への書き込み・D-Sec アラート（`/ds-alerts`・`PATCH /ds-alerts/{id}/status`・`DsAlertsScreen.tsx`・`lib/ds-alerts/**`）★（取得元の切り替えは次フェーズ。D-158 項5 / D-160）
- ★`/alerts` の挙動★: `prismaSource.prechecks()` / `precheckItemById()` / `updatePrecheckErrorColumns()` の中身、`app/api/prechecks/**`、`lib/db/fixture-precheck-overrides.ts`、`lib/fixtures/prechecks.ts`。★`AlertsScreen.tsx` は 5-4 の置き換えだけ★
- `planPrecheckStatusUpdate()` / `parsePrecheckStatusUpdateBody()` / `isEmptyPatch()` の中身（`lib/prechecks/status-update.ts`）、`lib/db/robot-error-write.ts` の中身（★書いてよい列を増やさない★）
- `detectedAtFromError()` / `precheckMinutesFromCode()` / `resolutionStatusFromRobotErrorStatus()` / `robotErrorStatusFromResolutionStatus()` の中身（`lib/db/precheck-mapping.ts`）
- `findTimetables()` / `findTimetablesByChunks()` / `findTimetableById()` / `mergeTimetableRows()` の中身（`lib/db/timetable-source.ts`）
- AV で入れたもの（`lib/dashboard/latest-run.ts`・`/api/dashboard`・`DashboardScreen.tsx`）
- `scripts/seed-local-db.ts` とガード（`lib/db/local-guard.ts`）
- ★4カラム以外への書き込み・生SQL・`eslint-disable` の追加★
- ★認証情報（パスワード・接続文字列・DB のユーザー名）を文書・コード・テスト・コミット・作業報告に書くこと★（D-154 項3）
- ★`git add -A`★

---

## 10. 範囲外（本書ではやらない。監督が扱う）

- ★エラーの区分（ロボット／制御盤／セキュリティ／清掃開始失敗）を作ること★ — P-8 の規則で作れるがユーザー判断で生値のみ（D-160 却下案）。★`error_type` にコードを付けるのは次フェーズ★
- ★D-Sec アラートの取得元を `robot_error_reports` に切り替えること★（P-10。次フェーズ）
- ★出発失敗（レポート行が無い予定）の対応状況の記録★（D-105 申し送り。「清掃開始できなかった」行 `NOT_STARTED` との関係は次フェーズで整理）
- サマリのカード（未解消◯件 等）・メニューのバッジ（モックの `/alerts` にある未解消件数のバッジ。`d-watch-mock.jsx:372`）・CSV 出力
- ★契約（`facilities.contract`）によるメニュー・データの出し分け★（D15。§11 Q2）
- ★「解消済みも表示」の性能★（保持期間ぶんのキーを読む。D8 の理由）— リリース前に本番の行数を見て見直す
- 関数名・定数名の一般化（`planPrecheckStatusUpdate` → 例 `planResolutionUpdate`、`PRECHECK_NOTE_MAX_LENGTH` → 例 `ERROR_NOTE_MAX_LENGTH`、`PrecheckStatus` → 例 `ResolutionStatus`）（§11 Q7）
- `/alerts` の備考・状態ボタンの部品を `/errors` と共通の部品にすること（本書は作法を写すだけ）

---

## 11. 監督への質問（★回答で暫定が変わったら「直す場所」を同時に直すこと★）

★**監督の決定（2026-10-06）: Q1〜Q14 はすべて下の表の「本書の暫定」どおりで確定**★。本文は暫定のとおり書いてあるので、直す場所は無い。
★Q1 の補足★: 依頼文の「SQL の take/skip でページ分け」は、`errored_at ?? created_at` の順を Prisma で書けないため取り下げる（監督の依頼文の誤り）。
★Q2 の補足★: 契約によるメニューの出し分けが未実装であることは、本書の範囲外として監督が別に扱う（`docs/decisions.md` D-160 追記）。

| # | 論点 | 本書の暫定 | 直す場所 |
|---|---|---|---|
| Q1 | ★依頼文の「ページ分けは SQL（take/skip）で」と食い違う★。検知時刻（`errored_at ?? created_at`）の順は Prisma のクエリビルダで書けない | ★キーだけを SQL で絞って全件読み、アプリで並べてページ分け → ページの行だけ読み直す★（D8）。対案: `orderBy: errored_at desc nulls last, created_at desc` で SQL ページ分け（★E7 の並びが表1 と食い違う★ので採らない） | §2 D8 / 2-4 / 3-4 / 3-5 / 4-4 |
| Q2 | ★依頼文の「メニューは /alerts と同じ規則（D-Watch の契約）」★ — ★実測では `AppShell.tsx` に契約による出し分けが無い★（全メニューを無条件に描く。`CLAUDE.md` §4.6 の「契約のないセクションはメニューごと非表示」は未実装） | ★`/alerts` と同じく常に出す★。データも契約で絞らない（`/prechecks` も絞っていない） | §2 D15 / 5-1 / §10 |
| Q3 | `status` が 0/1/2 以外（`UnsignedTinyInt` なので3以上が入りうる）の行 | ★一覧には出さない（SQL の `IN` で外れる）・例外にもしない★（表2 F12）。PATCH の1件読みで来たら500（fail-loud） | 表2 F12 / 2-4 / 2-5 |
| Q4 | 種類の絞り込み | ★部分一致・大小文字を区別しない（`includesFoldPlain()`）をアプリ側で★。依頼文の候補「存在する値から選ぶセレクト」は採らない | §2 D9 / 2-5 / 5-3 |
| Q5 | 列見出し | ★「施設名」★（依頼文は「店舗」。`CLAUDE.md` §8 の用語と、既存キー `店舗` の EN が `"Stores"` のため） | §2 D16 / 5-3 / 5-5 |
| Q6 | メニューのアイコンと EN | ★`🚨 エラー一覧` / `🚨 Errors`（暫定）★ | 5-1 / 5-5 |
| Q7 | `planPrecheckStatusUpdate` / `PRECHECK_NOTE_MAX_LENGTH` / `PrecheckStatus` / `PrecheckStatusPatch` の名前 | ★改名しない（そのまま使う）★。改名は AT のテスト・ルートを巻き込むので別作業（§10） | §2 D11 / 2-1 / §10 |
| Q8 | fixtures の上書き表を AT と共有するか | ★別のモジュール（状態を共有しない）★。fixtures の id が重ならないので `/alerts` と `/errors` の食い違いは起きない | §2 D14 / 3-3 |
| Q9 | レスポンスに `clean_timetable_id` を載せるか | ★載せる（画面には出さない）★ | §2 D18 / 2-1 / 4-1 |
| Q10 | `/alerts` の `nextResolutionStatus()` を共有の純粋関数に移すか（`AlertsScreen.tsx` に import の置き換えだけが入る） | ★移す★（2画面の巡回順が食い違わないため。挙動は変わらない） | §2 D17 / 2-2 / 5-4 / 4-16 |
| Q11 | 段階8（ステージングで1行書いて戻す）を行うか | ★条件が揃えば行う★（D-152 で許可済み。事前チェック以外の行で確かめる） | 段階8 |
| Q12 | サマリのカード（未解消◯件）・メニューのバッジ | ★作らない★（件数は「全◯件」だけ） | §10 / 5-3 |
| Q13 | 期間の日数の上限（`MAX_DATE_RANGE_DAYS`） | ★設けない★（`/prechecks` と同じ。`date_from > date_to` だけ422） | 2-5 / 4-1 |
| Q14 | 表2（F1〜F12）・表3（S1〜S6）・fixtures の11行と「既定10行・全11行」を正にしてよいか | 本書で導いた値 | §3 / 3-2 / 段階6 / 段階7 |

---

## 12. コミット

段階ごとに分ける（段階1 / 段階2 / 段階3 / 段階4 / 段階5 / 段階6）。★`git add` はファイルパスを明示する★（`git add -A` 禁止）。
メッセージには ★何を変えたか★ と ★なぜ（D-160。未対応のエラーを一覧し、対応状況と備考を更新できるようにする）★ を書く。
★段階1 のコミットは「テストだけ（この時点では落ちる）」と明記する★。
★D8（2段階で読む）を入れるコミットには、`COALESCE` の並べ替えを Prisma で書けないことと、`nulls last` を採らない理由（表1 E7）を書く★。
★PR の作成とマージは daichi がレビューしてから行う★（D-154）。

---

## 13. 実装の記録（2026-10-06）

★**実装は Cursor ではなく Claude Code**★（ユーザー指示 2026-10-06。本書の扱いを聞いた質問に「ここで実装する」と回答。段階8 も条件がそろえば行うと確認済み）。
監督がサブエージェント2本（★バックエンド: 段階1〜2 → 段階3・4・6★ / ★画面: 段階5★）に分け、ワークツリーで実装 → develop に cherry-pick。
★期待値表（§3 表1）は変えていない★。記録の正は `docs/decisions.md` D-160 の「実装の記録」と `docs/handoff.md` 追記62。

### 13-1. コミット（develop。★すべて未 push★。origin/develop は `19dbe38`）

| 段階 | コミット |
|---|---|
| 1（テストだけ。この時点では落ちる） | `fc9b3cf` |
| 2（型・純粋関数） | `499b23f` |
| 5-4（`/alerts` の巡回順を `lib/prechecks/status-cycle.ts` から import） | `5ceb30f` |
| 5（画面・メニュー・i18n） | `388172f` |
| 3（データ層。fixtures と Prisma、2段階で読む） | `e56b734` |
| 4-1（YAML） | `75a0115` |
| 4（ルート・テスト） | `9bdb8e2` |
| 6（`docs/fixtures.md` §13） | `f02eb1d` |

### 13-2. 確認（監督。`f02eb1d` 時点）

- `npx tsc --noEmit -p .` 0件 / `npm run lint` 0件 / vitest ★119ファイル・2,790件 通過★（着手前 111・2,601）/ `USE_FIXTURES=true npm run build` 成功（`/errors`・`/api/errors`・`/api/errors/[error_id]/status` が出る）
- 段階7-7 の grep: `status: { in: [` と select 定数の `robot_id` / `robot` は ★0件★。`$queryRaw|$executeRaw` は既存のコメント3か所（「使わない」と書いたもの）だけ
- ★fixtures の API（当方の fixtures 由来）★: 本番ビルドを `next start -p 3100`、2026-10-06 16:0x JST。
  既定 ★10件★・`include_resolved=true` で ★11件★／並びは検知時刻の降順（先頭は `errored_at` が NULL の `FX-ERR-07`＝`created_at` 10/6 06:40）／
  `error_type=secur` で `FX-ERR-08` の1件／`include_resolved=yes` は422／`FX-ERR-08` はシリアル「0」・種類「SECURITY_ON」／
  PATCH で 対応中 → 解消 → 未解消 の日時の入り方と消え方が §3（AT §3-4）どおり／大小文字違い・存在しない id は404／日本語の備考が UTF-8 で往復一致／`/prechecks` は200
- ★画面（当方の fixtures 由来）★: Edge ヘッドレスで撮影。メニューは「🔔 アラート」の直後に「🚨 エラー一覧」（EN "🚨 Errors"）／10行／
  予定の表示（事前チェック（10分前）・（30分前）・予定なし・予定不明・本清掃）／状態ボタンを押すと対応中になり、読み直しても保たれる／
  ★JP/EN で表の列幅は動かない★／1440px で横スクロールなし
- ★ローカルDB（`dwatch_local`。当方のシード由来）★: `next start` を環境変数でローカルに向けて起動、2026-10-06 16:10 JST。
  ★49行★（本清掃・事前チェック30/60・予定なし・予定不明がすべて出る）。`RER-1008-main`（本清掃に紐づく）で 対応中 → 解消 → 未解消・備考を入れて空に。
  `db.write` は `route` が `/api/errors/[error_id]/status`・`columns` は変えた列だけ・備考の中身なし（`comment_changed` のみ）。最後は元の値に戻した。
  ★段階7-6 の列単位 GRANT のローカル専用ユーザーでの確認は、していない★（`.env.local` に書き込み用アカウントの設定が無い）

### 13-3. ステージング（段階8。先方投入データ）

- ユーザーの dev サーバー :3000 経由、2026-10-06 16:11 JST。前後の値は `mysql` の SELECT だけで読んだ（★UPDATE を手で打っていない★）
- 一覧: ★16行すべて本清掃・未解消★。★シリアル「0」の行6行（SECURITY_ON 5・NOT_STARTED 1）も落ちずに読めた★（`robot_id` が NULL の行。§8-5）
- 書いた行は ★`1000013`（SECURITY_ON・シリアル0・本清掃）の1行だけ★: 対応中 → 解消 → 未解消・備考を入れて空に。★権限エラーなし★
- ★4列は操作前と完全に一致★（status 0・`comment` / `responded_at` / `resolved_at` は NULL）。`updated_at` は 15:09:52 → 16:11:10（DB が更新。AT §10 #8 で受け入れ済み）

### 13-4. 実装担当の判断（本書と違う・本書が決めていなかったところ）

| # | 判断 | 理由・場所 |
|---|---|---|
| 1 | ★`include_resolved` は小文字の `true` / `false` だけ★（`TRUE` も422） | 黙って倒さない（表2 F3 の延長）。`d-watch-api.yaml` にも明記 |
| 2 | GET のルートテストは `db.facilityNameIndex()`・`facilityGroupIndex()` を `vi.spyOn` で差し替えた | ★vitest でモックした `@/lib/db/prisma` を同時に動的 import すると、片方に本物が返る罠★（テスト環境だけ）。★`/clean-logs` の Prisma 経路テストで施設名と分類を同時に試すと同じ罠を踏む★ |
| 3 | ページの切り出しと行の組み立ては `lib/db/index.ts` の非公開2関数（`errorReportPageIds()` / `errorReportItemsInPageOrder()`）で fixtures と Prisma が共有 | 3-4 の「規則を2箇所に書かない」 |
| 4 | `updateErrorReportColumns()` は `updatePrecheckErrorColumns()` と同じ3行を ★共通化せず並べた★ | `updatePrecheckErrorColumns()` の挙動を変えないため（D12 と同じ考え方） |
| 5 | fixtures の `error_type` は `LowBattery` 等のダミー | 3-2 の表の例以外は実データ未受領（`docs/fixtures.md` §13） |
| 6 | 画面は `eslint-disable` を足さず、一覧の結果を `{query, items, total, error}` で持ち、読み込み中を `result.query !== queryString` で判定 | 4-19。effect の中で同期的に `setState` しない（`react-hooks/set-state-in-effect` の抑止コメントを足さずに済ませる。`ErrorsScreen.tsx:230-231`） |
| 7 | 列幅は §5-3 の目安から調整（★表の最小幅 1156px★） | `ErrorsScreen.module.css`。固定幅は守っている（4-17） |
| 8 | 期間の検証は `dateRangeError(from, to, Infinity)` | 上限を設けない（§11 Q13）。`lib/clean-logs/detail-log-date-range.ts` を流用 |
| 9 | 一覧の取得失敗の文言を `t()` に通した | 4-18 |

### 13-5. 残したもの・副作用

- ★副作用: i18n に「備考」→ "Note" を足したので、D-Secログ（`DsLogScreen.tsx` の `t("備考")`）の備考列の見出しが EN で "NOTE" になる★（これまでは EN でも「備考」のまま）。直していない（誤訳ではない）
- ★ツールバーの並びは言語で変わる★: JP では「解消済みも表示」が2行目に回り、EN では1行に収まる（★表の列幅は動かない★）。直していない
- ★375px★ はサイドバーが畳まれない既知の作り（指示書AV §13-4）で、右側が切れる。直していない（段階7-5「崩れていたら直さずに報告」）
- ★「解消済みも表示」の性能★（保持期間ぶんのキーを読む。D8 の理由）— §10 のとおりリリース前に見直す
- 新しい文言の EN（§5-5）は暫定（ユーザーが後でまとめて見直す）
- 運用上の知見: ★Bash ツールはヒアドキュメントの中でも `\\` を `\` に縮める★（スクリプトに正規表現の `\S` 等を書くときは Write ツールで書くか、バックスラッシュを使わない）
