# 指示書AX — D-Sec アラートを `robot_error_reports` から読み、編集モードで対応状況と備考を更新できるようにする（＋清掃詳細ログの 11px 横スクロールを消す）

**日付**: 2026-10-19 / **担当**: Cursor（または監督が指名する実装担当）/ **基点**: develop `64c4079`
**前提**: TeamWorks 第11回 P-8・P-10（`docs/handoff/TeamWorks回答_第11回_20261005.md:45-60`。D-158 `docs/decisions.md:12523` 以降）/
第10回 N-5（`docs/handoff/TeamWorks回答_第10回_20261002.md:100-112`。`error_type` の値）/
D-091 項3（セキュリティ無効はアラートではない。`docs/decisions.md:3510-3551`）/ D-105（`error_id` は string・`Number()` に通さない。`:5442` 以降）/
D-160（エラー一覧。`:12612`）/ D-161・D-165（対応状況はプルダウン・編集モードでまとめて保存。`:12647` / `:12706`）/ D-172（`serial_number` の拡張待ち → 当面 `db pull` しない。`:12807`）/
D-178（詳細ログの事前チェック2列。`:12901`）/ 指示書AT（4カラム UPDATE）/ 指示書AU 段階0-4（`robot_error_reports` は `select` で読む）/ 指示書AW（エラー一覧）/
`CLAUDE.md` §3・§4.7・§4.8・§6・§7・§9.3・§9.4・§9.6

★**ユーザー決定（2026-10-19。§2 の U1〜U8）と監督の設計判断（§2 の D1〜）は確定仕様。変えないこと**★（`CLAUDE.md` §0-1）。
★**期待値表（§3）の表1〜表4 は正。変えないこと**★（`CLAUDE.md` §0-2）。表1 の R1〜R7・R11 は監督確定、R8〜R10・R12・R13・表2・表3 は本書で導き ★監督が正とした（監督判断 2026-10-19。§11 Q5）★。
★**表5（削除済みの施設・清掃エリア。U8）は本書で U8 から導いたもの。監督が確認した（§11 Q9。監督判断 2026-10-19）**★。
★**§11 のユーザーへの質問（Q1〜Q4）はすべて確定し、本文に反映済み**★（Q4 は 2026-10-19 のユーザー決定 U8 で確定。暫定だった D12 は書き直した）。残るのは監督の確認 Q9（表5）だけ。

★**行番号はすべて 2026-10-19 の develop `64c4079` で確かめたもの**★。進んでいたら関数名・文で探し直すこと（特に `lib/db/index.ts`・`lib/types/index.ts`・`d-watch-api.yaml`・`scripts/seed-local-db.ts`）。

★**`docs/decisions.md` と `CLAUDE.md` は、本書の作成時点で別のセッションが未コミットの編集をしている（D-179）**★。
本書の実装で `docs/decisions.md` に足すのは ★D-180 の追記だけ★（§13 の下書き）。★D-179 の部分・`CLAUDE.md` には触らない★（`CLAUDE.md` は監督が直す）。

---

## 0. 何をするか

| # | やること | 段階 |
|---|---|---|
| 1 | ★D-Sec アラート（`GET /ds-alerts`・`/ds-alerts` 画面・ダッシュボードのセキュリティバナー）の取得元を `control_error_histories` から `robot_error_reports` に切り替える★。対象は ★`error_type = 'D-S_CONTROL_FAILURE'` の行だけ★ | 段階2〜4 |
| 2 | ★`DsAlert` の契約を直す★: `id` を string（`error_id`）に、`resolution_status` を ★エラー一覧と同じ `unresolved` / `in_progress` / `resolved`★ に、`memo` を `note` に（★YAML を先に直す★） | 段階4 |
| 3 | ★`/ds-alerts` 画面に編集モードを付ける★（エラー一覧と同じ操作。D-161 / D-165）。★書き込みはエラー一覧の `PATCH /errors/{error_id}/status` をそのまま使う★（新しい書き込みルートを作らない） | 段階5 |
| 4 | ★YAML だけにあって未実装の `PATCH /ds-alerts/{id}/status` を削除する★ | 段階4 |
| 5 | fixtures を作り直す（D-Sec アラートは `errorReportRows` の `D-S_CONTROL_FAILURE` 行から作る。セキュリティ・清掃開始失敗のシリアルを実データどおり `---` に） | 段階3 |
| 6 | ローカルDB のシードを追従させる（`control_error_histories` への投入をやめ、`robot_error_reports` に `D-S_CONTROL_FAILURE` 行を入れる） | 段階6 |
| 7 | ★（別件・独立）清掃詳細ログの表が画面幅 1440px で 11px 横スクロールするのを消す★（施設名の列 126px → 110px） | 段階7 |
| 8 | ★削除済み（論理削除。`facilities.deleted_at` / `sections.deleted_at`）の施設・清掃エリアの行を、★エラー一覧（`GET /errors`・`meta.total`）と D-Sec アラート（`GET /ds-alerts`・`summary`・ダッシュボード）の両方から外す★。★その行への `PATCH /errors/{error_id}/status` は404★（U8・D12） | 段階2〜4 |

★**変えないもの**★: D-Sec ログ（`/ds-logs`。`dsDevices()`＝`controls` ＋ `control_reports`）／エラー一覧（`/errors`）の挙動・`PATCH /errors/{error_id}/status` の挙動（★U8 の「削除済みの施設・清掃エリアの行を外す・404 にする」を除く。ルートのコードは変えない — 404 は `db.errorReportById()` が null を返すことで出る★）／施設名・分類の解決が `deleted_at` で絞らないこと（★罠7★）／
アラート（`/alerts`）・`PATCH /prechecks/{id}/status`／`lib/db/robot-error-write.ts`（書いてよい列）／`lib/prechecks/edit-drafts.ts` の中身／
ダッシュボードのセキュリティバナーのコード（`computeSecurityBanner()`。§2 D4）／`prisma/schema.prisma`（`db pull` しない。D-172）。

---

## 1. 背景

- いまの `prismaSource.dsAlerts()`（`lib/db/index.ts:1967-1996`）は ★`control_error_histories`（＋ `controls` / `facilities` / `sections`）を読む★。
  `alert_type` は固定で `"disconnected"`、`device_serial` = `controls.serial_number`、`detected_at` = `occurred_at`、
  `resolution_status` は `resolutionStatusFromControlErrorStatus()`（`lib/db/ds-status.ts:36-49`。0/1/2 → `pending` / `in_progress` / `done`、他は例外）、`memo` = `comment`。
  ★アプリで `control_error_histories` を読むのはここだけ★（書くのは `scripts/seed-local-db.ts:1160-1182` のシードだけ）。
- TeamWorks の回答:
  - ★第11回 P-10 = A★: ★`robot_error_reports` を正とする。D-Security のアラートも対応状況の更新もこちら。`control_error_histories` は読まない★（D-158 の表の P-10 行）。
  - ★第11回 P-8★: 制御盤のエラー＝`robot_id` NULL・`serial_number` が制御盤の SN／セキュリティと清掃開始失敗＝`robot_id` NULL・`serial_number` が `0`（`error_type` で見分ける）／施設は行の `facility_id`・`section_id`（d = A）。
  - ★第10回 N-5★: `error_type` は `D-S_CONTROL_FAILURE`（`error_detail` "no regular report from the D-S control."）と `SECURITY_ON`（"security is enabled."）。
- ★ステージングの実データ（2026-10-19 監督の SELECT。先方投入）★: `robot_error_reports` 16行。うち `robot_id` NULL が9行 ＝
  `D-S_CONTROL_FAILURE` 3行（シリアル `DFAS010001` / `DFAS010005` / `DFAS010002`。`status` 0 が2行・2 が1行）、`SECURITY_ON` 5行（シリアル `---`）、`NOT_STARTED` 1行（シリアル `---`）。
  - ★16行すべて `errored_at` が NULL★ → 検知時刻は ★`errored_at ?? created_at`★ でないと出せない（いまの D-Sec は `occurred_at` を読むので、切り替え後は既存の `detectedAtFromError()` を使う）。
  - ★`error_id` 1000004 は、シリアル `DFAS010001` が施設2の制御盤なのに、行の `facility_id` / `section_id` は 3★（§12 申し送り1）。
  - ★セキュリティ・清掃開始失敗のシリアルは実データでは `---`★（P-8 の回答の `0` ではない）。fixtures（`lib/fixtures/error-reports.ts:179-215`）と `docs/fixtures.md:514-534` は `'0'` のまま。
- いまの D-Sec 画面（`components/features/ds-alerts/DsAlertsScreen.tsx`）は表示だけ。`PATCH /ds-alerts/{id}/status` は ★YAML（`d-watch-api.yaml:1486-1511`）にだけあり、実装されていない★（`app/api/ds-alerts/route.ts:7-9`）。
- ★既存の不具合（本書で一緒に消える）★: D-Sec 画面の `t("未対応")` は、EN で ★"Unsupported"★ と出る（`lib/i18n.ts:182` は対応機種バッジ用の訳。キーが衝突している）。§2 U5（文言をエラー一覧に揃える。ユーザー確定）で消える。

---

## 2. 決定（実装担当は従うこと）

### 2-1. ユーザー決定（2026-10-19。確定。U5〜U8 は §11 Q1〜Q4 の回答）

| # | 決定 | 帰結 |
|---|---|---|
| U1 | ★`SECURITY_ON` は D-Sec アラートに出さない★（エラー一覧には今までどおり出る） | D-091 項3「セキュリティが無効であっても異常ではない」「D-sec アラートは…生死報告が途切れたなどのエラーのみ」と一致 |
| U2 | ★`NOT_STARTED`（清掃開始失敗）は D-Sec アラートに出さない★（ロボット側の出発失敗。エラー一覧には今までどおり出る） | — |
| U3 | ★施設は行の `facility_id`（と `section_id`）★（P-8 d = A）。シリアルが別の施設の制御盤でも、行の施設で出す | 1000004 は施設3で出る。食い違いは TeamWorks に申し送る（§12） |
| U4 | ★D-Sec アラート画面に編集モードを今回付ける★。エラー一覧と同じ操作（通常モードはバッジ表示／`編集` → `保存（N件）`・`キャンセル`／まとめて保存／下書きがあるあいだ `beforeunload` で確認／変えた行の目印／編集中は絞り込み・ページ送りを無効）。★書くのは同じ `robot_error_reports` の4カラム★。★同じ行をどちらの画面からも編集できる★ | D-161 / D-165。書き込み経路は1本（D6） |
| U5 | ★D-Sec 画面の対応状況の文言をエラー一覧に揃える★: 未対応 → ★未解消★／対応中 → 対応中／対応済み → ★解消★（数値カード・状態の絞り込みボタン・バッジ・編集のプルダウン）。★ユーザー確定 2026-10-19（§11 Q1）★ | 同じ行が2画面で同じ状態なのに文言が違うと混乱する。★EN の "Unsupported" 衝突（`lib/i18n.ts:182`）も消える★。モック（`d-watch-mock.jsx:38-42` の `DSA_INIT`）の「未対応 / 対応済み」とは食い違う（承知のうえ。モックは書き換えない） |
| U6 | ★D-Sec 画面の種別のセレクト（全種別 / セキュリティ無効 / 通信断。`DsAlertsScreen.tsx:217-228`）と副題（「セキュリティ無効・通信断のアラート管理」）は ★そのまま残す★★。★「セキュリティ無効」を選ぶと必ず0件になることは承知のうえ★。★ユーザー判断 2026-10-19（§11 Q2）★ | 本書の旧暫定（セレクトを外し副題を変える）は ★撤回★。API の `type` パラメータと enum も残る（D4） |
| U7 | ★ダッシュボードのセキュリティバナーのコードは残す★（DB のデータでは出ない）。★ユーザー確定 2026-10-19（§11 Q3）★ | D4 |
| U8 | ★削除済みの施設・清掃エリアの行は、エラー一覧からも D-Sec アラートからも出さない★。ユーザー原文（2026-10-19）:「削除済みのものは表示する必要がないので、エラー一覧からもD-sec一覧からも消してください」（§11 Q4） | D12。★本書の範囲がエラー一覧（`errorReports()` / `errorReportById()`）に広がる★。`/alerts` は範囲外（§10・§12-2） |

### 2-2. 監督の設計判断（確定。本書の判断だった D9・D10 と段階6 のシードも監督判断 2026-10-19 で確定。§11 Q6〜Q8）

| # | 決定 | 理由 |
|---|---|---|
| D1 | ★D-Sec アラート ＝ `robot_error_reports` のうち `error_type` が ★`D_SEC_ALERT_ERROR_TYPES`（`lib/config/index.ts` に新設。初期値 `["D-S_CONTROL_FAILURE"]`）★ に ★完全一致（JS の `===`）★ する行★。`alert_type` は `"disconnected"` | 監督判断。値は設定に外出しし、配列にして後から足せるようにする。★`robot_id IS NULL` やシリアルの形で見分けない★（P-8 の規則はシリアルの実値が回答と違う＝`---`。形で見分けると壊れる） |
| D2 | ★SQL の `error_type IN (...)` は「写し」、正は純粋関数の `===`★。Prisma 版も読んだ後に純粋関数で絞り直す | ★DB の照合順序は大小文字を区別せず（ci）、末尾の空白も無視する（PAD SPACE）★。SQL だけに任せると `d-s_control_failure` や `D-S_CONTROL_FAILURE␣` が当たり、fixtures と件数が割れる（D-4 の `===` と同じ考え方。表1 R8・R9） |
| D3 | ★`status` は 0/1/2 の行だけ（SQL の `status IN` と純粋関数の両方で絞る）★。3 以上は ★出さない・例外にしない★ | ★`db.dsAlerts()` はダッシュボードも呼ぶ（`app/api/dashboard/route.ts:152`）★。1行の想定外の値でダッシュボード全体が500になるのを防ぐ（エラー一覧の表2 F12 と同じ扱い）。コードは `robotErrorStatusFromResolutionStatus()` から作る（0/1/2 を直書きしない） |
| D4 | ★API の `alert_type` の enum に `security_off` は残す（互換）。ただし DB からは出ない★。★ダッシュボードのセキュリティバナー（`computeSecurityBanner()`。`lib/dashboard/aggregate.ts:130-140`）は DB のデータでは ★もう出ない★★。★コードと `aggregate.test.ts` のテスト（テスト内で作った `DsAlert` を使う）は残す★ | 監督判断。YAML の `security_banner` と `/ds-alerts` の `description` にこのことを書く。★バナーのコードを残すことはユーザー確定（2026-10-19。U7・§11 Q3）★。廃止・転用は範囲外（§10） |
| D5 | ★`DsAlert` の項目★: `id` = `error_id`（★string。`Number()` に通さない★。YAML を先に直す。前例 D-105）／`device_serial` = 行の `serial_number`（★生値。D-077★）／`facility_id`・`facility_name` = 行の `facility_id` とその施設名（U3）／`section_name` = 行の `section_id` の清掃エリア名／`detected_at` = ★`jstIsoFromDb(detectedAtFromError(row))`★（`errored_at ?? created_at`。`lib/db/precheck-mapping.ts:228`。エラー一覧と同じ）／`responded_at`・`resolved_at` = `jstIsoFromDb()`／★`note`★ = `comment` | 監督判断（`note` への改名は本書の判断。D9） |
| D6 | ★書き込みはエラー一覧の `PATCH /errors/{error_id}/status`（`app/api/errors/[error_id]/status/route.ts`）と `lib/db/robot-error-write.ts` をそのまま使う★。★新しい書き込みルート・`DataSource` の書き込みメソッドを作らない★ | 監督判断。同じ列を2つの規則で書かない |
| D7 | ★YAML の `PATCH /ds-alerts/{id}/status`（`d-watch-api.yaml:1486-1511`）は ★削除する★（`deprecated` で残さない）。`/ds-alerts` の `description` に「対応状況と備考の更新は `PATCH /errors/{error_id}/status`（同じ `robot_error_reports` の行）」と書く | 残すと「実装すべきもの」に見え、2本目の書き込み経路が生まれる。実装が一度も無いので消しても壊れる呼び出し元が無い |
| D8 | ★`resolution_status` の語彙はエラー一覧と同じ `unresolved` / `in_progress` / `resolved`（`PrecheckStatus`）に統一する★。`DsAlertStatus`（`pending` / `in_progress` / `done`）は ★削除★。変換は ★既存の `resolutionStatusFromRobotErrorStatus()`（`lib/db/precheck-mapping.ts:41`）の1本★ | 下の「D8 の理由」 |
| D9 | ★`DsAlert.memo` を `note` に改名する★（YAML を先に）。★監督判断 2026-10-19（§11 Q6）★ | ★`lib/prechecks/edit-drafts.ts:19` の `EditableRow` は `id` / `resolution_status` / `note` を要求する★。D5・D8 と合わせると `DsAlert` がそのまま `EditableRow` になり、`saveDrafts<DsAlert>()` を ★無改造で使える★。`PrecheckItem` / `ErrorReportItem` も `note`。契約は D5・D8 で既に変わるので、変えるなら同時に変える（呼び出し元は自前の画面だけ） |
| D10 | ★`GET /ds-alerts` の `status` パラメータは `open` / `all` / ★`resolved`★（旧 `done` は ★422★）★。★`summary` のキーは `unresolved` / `in_progress` / `resolved`★。★監督判断 2026-10-19（§11 Q7）★ | D8 の帰結。`done` という状態が無くなるので、パラメータとキーも揃える（別名で受けない＝黙って倒さない） |
| D11 | ★並びは `detected_at` の降順、同値は `id` の昇順（★文字列の辞書順。`localeCompare` を使わない★）★（`sortDsAlerts()` `lib/ds-alerts/query.ts:113-120` の `a.id - b.id` を直す） | id が string になるため。エラー一覧の `compareErrorReportKeys()` と同じ規則（`"E-10"` が `"E-9"` より先） |
| D12 | ★`robot_error_reports` の行のうち、★施設（`facilities.deleted_at`）か清掃エリア（`sections.deleted_at`）が論理削除されている行は「見えない行」★とし、★エラー一覧（`errorReports()`。`meta.total` も）・`errorReportById()`（＝ `PATCH /errors/{error_id}/status` の実在確認 → 404）・D-Sec アラート（`dsAlerts()`。`summary`・ダッシュボードも）のすべてで外す★。★`deleted_at` が NULL でなければ（日時の値によらず）削除済み★。★正は純粋関数 `isVisibleErrorRow()`、SQL のリレーション条件 `ERROR_ROW_VISIBLE_WHERE` は写し★（段階2-7）。★U8 で確定（ユーザー 2026-10-19）。旧暫定「絞らない」は破棄★ | U8。施設・清掃エリア・制御盤は ★論理削除なので行は残り、絞らなければエラー行も返ってくる★（§11 Q4 の事実）。★旧 D-Sec 実装の `controls.deleted_at` の絞りは復活させない★（`controls` を結合しない。行の施設・清掃エリアで決める — U3）。★施設名・分類 → 施設ID の解決は従来どおり `deleted_at` で絞らない（罠7。`lib/facilities/resolve-by-name.ts:74` / `resolve-by-master.ts:84`）★ — 解決された削除済みの施設の行は、この可視性の条件で外れる |
| D13 | ★fixtures の D-Sec アラートは `lib/fixtures/error-reports.ts` の `errorReportRows` から、Prisma 版と ★同じ純粋関数★ で作る★。★`lib/fixtures/ds-alerts.ts` は削除★ | `CLAUDE.md` §9.5（判定ロジックは切替の外側）。★fixtures でも、エラー一覧で書いた対応状況が D-Sec に出る（上書き表を共有）★ — U4「同じ行をどちらの画面からも」を fixtures でも確かめられる |
| D14 | ★`lib/db/ds-status.ts` の `resolutionStatusFromControlErrorStatus()` / `controlErrorStatusFromResolutionStatus()` は削除★（テストも）。`connectionStatusFromControlStatus()` / `controlStatusFromConnectionStatus()` は残す（`dsDevices()` とシードが使う） | 段階6 でシードが `control_error_histories` に書かなくなると、使う場所が無くなる |
| D15 | ★画面の保存後、`summary`（数値カード）は ★取り直さずに手元で付け替える★（変わった行ごとに旧状態 −1・新状態 +1）。★「対応済み」にした行は、`status=open` の表示でも次に取り直すまで消さない★ | エラー一覧（`ErrorsScreen.tsx` の `saveEdit()`）と同じ振る舞い（押し間違いをその場で戻せる）。取り直すと解消した行が消え、`summary` だけ正しくて一覧と食い違う |

### D8 の理由（語彙を揃えるか、別の変換を作るか）

| | 案A: 揃える（`unresolved` / `in_progress` / `resolved`）★採用★ | 案B: 残す（`pending` / `in_progress` / `done`）＋ 専用の変換 |
|---|---|---|
| 変換 | ★既存の `resolutionStatusFromRobotErrorStatus()` の1本★ | ★同じ列に2つ目の変換★（`robot_error_reports.status` → `DsAlertStatus`）。どちらかだけ直す事故の余地 |
| 書き込み | `PATCH /errors/...` の本文・応答の語彙と同じ。変換なし | 画面で `DsAlertStatus` ⇄ `PrecheckStatus` を往復変換（送るとき・応答を戻すとき） |
| 編集モード | `edit-drafts.ts` を無改造で使える（D9 と合わせて） | `EditableRow` が `PrecheckStatus` 固定なので、ジェネリクス化（`/alerts`・`/errors` を巻き込む）か、画面側の詰め替えが要る |
| 書き換え量 | 型・YAML・`lib/ds-alerts/query.ts`・`DsAlertsScreen.tsx`・`aggregate.ts` 1行・テスト（`query.test.ts` 約10か所・`aggregate.test.ts` 約7か所・`master-filter.test.ts` 1か所）。★`tsc` が全部指す★ | 型はそのままだが、変換・詰め替えの新しいコードとそのテストが要る |
| 同じ行の見え方 | 2画面で同じ値 | API では別の値・画面の文言も別になりやすい |

★書き換えは型エラーで漏れなく見つかる。一方、案B の「2つ目の変換」は型では見つからない食い違いの温床★なので、案A を採る。
★画面の文言（「未対応」か「未解消」か）は語彙とは別の問題★ — こちらもエラー一覧に揃えることでユーザー確定（§2 U5）。

### 2-3. 本書の暫定

★ユーザー確認待ちの暫定は無い★。旧 2-3 の U5 は確定、U6 は撤回（そのまま残す）、D12 は U8 で確定して書き直した。
★表5（U8 から導いた期待値）も監督が確認済み（§11 Q9。2026-10-19）★。

---

## 3. ★期待値表（正）★

### 表1（D-Sec アラートに出るか・出たときの値。`dsAlertsFromErrorRows()`）

★R1〜R7・R11 は §2 のユーザー決定・監督判断から直接決まる（監督確定）★。★R8〜R10・R12・R13 は本書で導き、監督が正とした（監督判断 2026-10-19。§11 Q5）★。

| # | `robot_error_reports` の行 | D-Sec に出るか | 出たときの値 | 根拠 |
|---|---|---|---|---|
| R1 | `error_type` `D-S_CONTROL_FAILURE`・`status` 0・`errored_at` あり・`robot_id` NULL・シリアル `DFAS010001` | 出る | `alert_type` `disconnected`／`device_serial` `DFAS010001`／`resolution_status` `unresolved`／`detected_at` = `errored_at`（`+09:00`）／`note` null | D1・D5 |
| R2 | 同上・`status` 1・`responded_at` あり・`comment` 「回線業者に確認中」 | 出る | `in_progress`／`responded_at` あり／`note` 「回線業者に確認中」 | D5・D8 |
| R3 | 同上・`status` 2・`responded_at`・`resolved_at` あり | 出る（`status=open` の一覧には出ない。`all`・`resolved` で出る。`summary.resolved` に数える） | `resolved` | D8・D10 |
| R4 | 同上・`status` 0・★`errored_at` NULL★ | 出る | ★`detected_at` = `created_at`★ | D5（ステージングの16行すべてがこの形） |
| R5 | `error_type` `SECURITY_ON`・`robot_id` NULL・シリアル `---` | ★出ない★ | — | U1 |
| R6 | `error_type` `NOT_STARTED`・`robot_id` NULL・シリアル `---` | ★出ない★ | — | U2 |
| R7 | ロボットのエラー（`error_type` `EmergencyStop`・`robot_id` あり） | ★出ない★ | — | D1 |
| R8 | `error_type` ★`d-s_control_failure`★（小文字） | ★出ない★（SQL の ci では当たるが、純粋関数の `===` で外れる） | — | D2 |
| R9 | `error_type` ★`D-S_CONTROL_FAILURE␣`★（末尾に空白1つ） | ★出ない★（SQL の PAD SPACE では当たるが、`===` で外れる） | — | D2 |
| R10 | `D-S_CONTROL_FAILURE`・★`status` 3★ | ★出ない・例外にならない★（ダッシュボードも500にならない） | — | D3 |
| R11 | `D-S_CONTROL_FAILURE`・シリアル `DFAS010001`（施設2の制御盤）・★行の `facility_id` / `section_id` は施設3★ | 出る | ★`facility_id` 3・施設3の名前・`section_name` は行の `section_id` の名前★／`device_serial` `DFAS010001`（そのまま） | U3（ステージング 1000004 の形） |
| R12 | `D-S_CONTROL_FAILURE`・R1 と同じ検知時刻・`error_id` ★`"E-10"`★（R1 は `"E-9"`） | 出る | 並びで ★`"E-10"` が `"E-9"` より先★（辞書順。数値順なら逆） | D11 |
| R13 | `D-S_CONTROL_FAILURE`・★`clean_timetable_id` NULL★ | 出る | 予定の有無で絞らない | D1（P-9 で制御盤の行にも予定 id が入る予定だが、入らない行も出す） |

並び順: `detected_at` の降順、同値は `id` の昇順（文字列）。

★テストの書き方の注意★:
- R8・R9 は ★Prisma 経路のルートテストで、スタブの `findMany` が「SQL のように ci・PAD SPACE で当ててしまう」★形にして返す（＝ `where` を無視して全行返してよい）。そのうえで ★応答に出ない★ことを確かめる。純粋関数の単体テストでも同じ行を入れる
- R10 は単体テストで「例外にならず外れる」、ルートテストで「`/api/ds-alerts` も `/api/dashboard` も200」を確かめる
- `robot_id` NULL の行は ★`guardNullRobotId()`（`lib/db/__tests__/robot-error-null-guard.ts`）をスタブに入れる★（`robot_id` / `robot` を読むと落ちる）
- 日時は `+09:00` 付きの ISO 文字列から `toDbJst(new Date(...))` で DB の壁時計値を作る（エラー一覧のテストと同じ）
- 設定値は ★`D_SEC_ALERT_ERROR_TYPES[0]` から読む★。R8・R9 の「わざと違う綴り」は直書きする（コメントで理由を書く）

### 表2（`GET /ds-alerts` のパラメータ・`summary`。★本書で導き監督が正とした（監督判断 2026-10-19。§11 Q5）★）

入力は表1 の R1〜R4・R11〜R13（出る7行。`status` は R1・R4・R11・R12・R13 が 0、R2 が 1、R3 が 2）と R5〜R10（出ない6行）。

| # | 入力 | 期待 | 確かめること |
|---|---|---|---|
| A1 | 既定（パラメータなし ＝ `type=all&status=open`） | 出る7行のうち R3 以外の6行・`meta.total` = 6 | D10 |
| A2 | `status=resolved` | R3 だけ | D10 |
| A3 | `status=all` | 7行 | D10 |
| A4 | ★`status=done`（旧の値）★ | ★422★。`details.status` が `open / all / resolved のいずれかを指定してください` | D10（黙って倒さない） |
| A5 | `type=security_off` | ★200・0件★（422 にしない。enum は残す） | D4。★画面の種別セレクトで「セキュリティ無効」を選んだときもこれ（ユーザー承知。U6）★ |
| A6 | `type=disconnected` | A1 と同じ | D1 |
| A7 | `summary`（どの `type` / `status` でも） | ★`{ unresolved: 5, in_progress: 1, resolved: 1 }`★（出る7行に対して数える。R5〜R10 は数えない） | D10・D-054（`summary` は `type` / `status` の前） |
| A8 | `customer_id` で絞る | `summary` も一覧も同じ施設の集合に絞られる（`app/api/__tests__/master-filter.test.ts:364-380` の形） | 分類は母集団（既存の規則） |
| A9 | `per_page=2&page=2`（既定の6行） | 並び順の3〜4行目・`meta.total` = 6 | 既存のページ分け |

### 表3（画面の保存の純粋関数。★本書で導き監督が正とした（監督判断 2026-10-19。§11 Q5）★）

`dsAlertWithResolution(row, updated)`（`lib/ds-alerts/edit.ts`。新規）と `adjustDsAlertSummary(summary, before, updated)`（同）:

| # | 入力 | 期待 |
|---|---|---|
| S1 | `row`（`unresolved`・`note` null）、`updated`（`ErrorReportItem`。同じ `id`・`resolved`・`responded_at` / `resolved_at` あり・`note` 「確認済み」） | `row` の ★4項目だけ★ 差し替え（`resolution_status` / `responded_at` / `resolved_at` / `note`）。`alert_type`・`device_serial`・`facility_name`・`section_name`・`detected_at` は `row` のまま |
| S2 | `updated.id !== row.id` | ★例外★（呼び出しの誤り） |
| S3 | `summary` `{2,1,1}`（unresolved, in_progress, resolved）、1行が `unresolved` → `resolved` | `{1,1,2}` |
| S4 | 備考だけ変えた（状態は同じ） | `summary` は変わらない |
| S5 | 2行（`unresolved` → `in_progress`、`in_progress` → `unresolved`） | `{2,1,1}` のまま |
| S6 | `updated` が空の Map | 入力と同じ値（★入力のオブジェクトを書き換えない★） |

### 表4（画面の文言。★§2 U5・U6。ユーザー確定 2026-10-19★）

| 箇所 | 今 | 本書の後 | EN |
|---|---|---|---|
| 数値カード1 | 未対応 / 即時対応 | ★未解消★ / 即時対応 | Unresolved / （既存の訳） |
| 数値カード3 | 対応済み / 完了 | ★解消★ / 完了 | Resolved |
| 状態ボタン | 未対応・対応中 / 全件 / 対応済み | ★未解消・対応中★ / 全件 / ★解消★ | 新キー「未解消・対応中」→ "Unresolved / In progress"（暫定） |
| バッジ・プルダウン | 未対応 / 対応中 / 対応済み | 未解消 / 対応中 / 解消（エラー一覧の `STATUS_OPTIONS` と同じ） | 既存（`lib/i18n.ts:161-163`） |
| 副題 | セキュリティ無効・通信断のアラート管理 | ★変えない★（U6） | — |
| 種別セレクト | 全種別 / セキュリティ無効 / 通信断 | ★変えない（残す）★。「セキュリティ無効」を選ぶと0件（ユーザー承知。U6・表2 A5） | — |

---

### 表5（削除済みの施設・清掃エリア。U8・D12。★本書で導いたもの。§11 Q9 で監督が確認済み（2026-10-19）★）

「施設」「清掃エリア」は行の `facility_id` / `section_id` の先（`facilities` / `sections`）。「削除」は `deleted_at` が NULL でないこと。

| # | 行 | エラー一覧（`GET /errors`） | D-Sec アラート（`GET /ds-alerts`） | `PATCH /errors/{error_id}/status` |
|---|---|---|---|---|
| V1 | 施設・清掃エリアとも生きている・ロボットのエラー（`EmergencyStop`）・`status` 0 | 出る（`meta.total` に数える） | 対象外（種類が違う） | 200 |
| V2 | ★施設が削除済み★（清掃エリアは生きている）・ロボットのエラー・`status` 0 | ★出ない★（既定でも「解消済みも表示」でも。`meta.total` に数えない） | 対象外 | ★404★（`updateMany` を呼ばない） |
| V3 | ★清掃エリアが削除済み★（施設は生きている）・ロボットのエラー・`status` 0 | ★出ない★ | 対象外 | ★404★ |
| V4 | 施設・清掃エリアとも生きている・`D-S_CONTROL_FAILURE`・`status` 0 | 出る | 出る（`summary.unresolved` に数える） | 200 |
| V5 | ★施設が削除済み★・`D-S_CONTROL_FAILURE`・`status` 0 | ★出ない★ | ★出ない★（`summary` にも数えない。ダッシュボードにも渡らない） | ★404★ |
| V6 | ★清掃エリアが削除済み★・`D-S_CONTROL_FAILURE`・`status` 1 | ★出ない★ | ★出ない★ | ★404★ |
| V7 | ★施設・清掃エリアとも削除済み★・`D-S_CONTROL_FAILURE`・`status` 2 | ★出ない★（「解消済みも表示」でも） | ★出ない★（`status=all` / `resolved` でも） | ★404★ |
| V8 | V2 の施設名で `facility_name` を絞る（名前の解決は `deleted_at` で絞らない＝罠7 なので施設IDは解決される） | ★0件・200★（422 / 500 にしない） | — | — |
| V9 | V1 と同じ施設で、別の清掃エリアが削除済み（V3）の場合の V1 | 出る（★清掃エリア単位で決まる。同じ施設の生きている清掃エリアの行は消えない★） | — | 200 |

★テストの書き方の注意★:
- Prisma 経路のスタブは ★`where` の `facility.deleted_at` / `section.deleted_at` の条件も評価する★形と、★`where` を無視して全行返す★形の両方で確かめる（後者で ★純粋関数側の `isVisibleErrorRow()` が外す★ことを見る。D2 と同じ考え方）
- PATCH の404 は ★`db.errorReportById()` が null を返すことで出る★。★`updateMany` が呼ばれないこと・`db.write` が出ないこと★を確かめる（存在しない id の404 と同じ扱い）
- 削除の日時は ★過去の日時★を入れる（`toDbJst(new Date("2026-10-01T10:00:00+09:00"))` 等）。★「未来の日時なら生きている」とはしない★（D12。NULL か否かだけ）

## 4. ★落とし穴（全段階で読むこと）★

| # | 罠 | どう避けるか |
|---|---|---|
| 4-1 | ★`robot_error_reports` を `select` なし・`include` で読む／`robot_id`・`robot` を選ぶ★ | ★D-Sec の行は全部 `robot_id` NULL★ — 読み取りが落ちる（指示書AU 段階0-4）。★新しい定数 `ROBOT_ERROR_DS_ALERT_SELECT` だけを使う★ |
| 4-2 | ★新しい select 定数に `clean_timetable_id` を入れ忘れる★ | `lib/db/__tests__/robot-error-select.test.ts:35-37` が「全定数が `clean_timetable_id` を含む」を見ている。★使わなくても `clean_timetable_id: true` を入れる★（`ROBOT_ERROR_LIST_KEY_SELECT` と同じ扱い） |
| 4-3 | ★SQL の `error_type IN` の結果をそのまま採る★ | D2。★読んだ後に純粋関数で `===` で絞り直す★（表1 R8・R9） |
| 4-4 | ★`resolutionStatusFromRobotErrorStatus()` に 3 以上を通す★ | 例外 → ★ダッシュボード全体が500★。★SQL の `status IN` と純粋関数の両方で 0/1/2 に絞ってから変換する★（D3・表1 R10） |
| 4-5 | ★`detected_at` を `errored_at` だけから作る／`created_at` を `new Date()` で作り直す★ | ★`detectedAtFromError()` → `jstIsoFromDb()`★。DB の値は JST の壁時計値（`CLAUDE.md` §3）。★9時間を足さない・引かない★ |
| 4-6 | ★`error_id` を `Number()` に通す／`a.id - b.id` が残る★ | ★文字列のまま★（D-105）。`sortDsAlerts()` の同値比較は ★`<` / `>` の辞書順★（D11）。React の key も文字列 |
| 4-7 | ★施設をシリアル（制御盤）から引く★ | ★行の `facility_id` / `section_id`★（U3。表1 R11）。★`controls` を結合しない★ |
| 4-8 | ★書き込みのために新しいルート・`DataSource` メソッドを足す★ | ★`PATCH /api/errors/${encodeURIComponent(id)}/status` を呼ぶだけ★（D6）。応答は `ErrorReportItem` なので `dsAlertWithResolution()` で `DsAlert` に戻す |
| 4-9 | ★保存後に一覧を取り直す★ | ★取り直さない★。成功した行だけ差し替え、`summary` は `adjustDsAlertSummary()` で付け替える（D15） |
| 4-10 | ★`summary` を一覧の件数（`meta.total`）の代わりに使う／逆★ | 既存の注記（`DsAlertsScreen.tsx` の `total` の docstring。D-054）のまま |
| 4-11 | ★fixtures の D-Sec を別の配列で持つ★ | ★`errorReportRows` から作る★（D13）。別にすると、エラー一覧で書いた状態が D-Sec に出ない（fixtures だけ U4 を満たさない） |
| 4-12 | ★fixtures の件数を変えたのに、エラー一覧の件数のテスト・資料を直し忘れる★ | `lib/db/__tests__/fixture-error-reports.test.ts:49-53` / `:96-97` / `:102-108` / `:120`、`app/api/errors/__tests__/route.fixtures.test.ts`、`docs/fixtures.md` §13 の件数と並び（段階3-4） |
| 4-13 | ★`security_off` を enum から消す★ | ★消さない★（D4）。`lib/ds-alerts/query.ts:10-14` の `TYPE_FILTERS` もそのまま |
| 4-14 | ★`computeSecurityBanner()` を消す／DB から `security_off` を作ろうとする★ | ★どちらもしない★（D4・U1）。バナーが出ないのは意図した挙動。YAML に書く |
| 4-15 | ★シードを `dwatch_local` 以外に向ける★ | ★`lib/db/local-guard.ts` を触らない★。シードはテーブルを消してから入れ直す（ステージングに向けると実データが消える。`CLAUDE.md` §3） |
| 4-16 | ★`t()` に生値（シリアル・施設名・清掃エリア名・備考）を通す★ | 通さない（辞書の対訳に誤って当たる。`ErrorsScreen.tsx` の冒頭コメント）。★「読み込み中…」は `t()` に通す★（いま `DsAlertsScreen.tsx:263` は通していない） |
| 4-17 | ★編集中に一覧が取り直されて下書きが消える★ | エラー一覧と同じ: ★編集モードに入れるのは一覧が今の絞り込みの結果と一致しているときだけ★、★編集中は絞り込みとページ送りを `<fieldset disabled>` で無効★（`ErrorsScreen.tsx` の `canStartEdit` / `filtersDisabled`） |
| 4-18 | ★`eslint-disable` を足す★ | ★足さない★。いまの `DsAlertsScreen.tsx:127` に `react-hooks/set-state-in-effect` の抑止が1つある。★書き直すなら `ErrorsScreen.tsx` の「`result.query !== queryString` で読み込み中を判定する」形に揃えて抑止を消してよい★（増やさないことが条件。消せなければ既存の1つは残してよい） |
| 4-19 | ★生SQL（`$queryRaw` / `$executeRaw`）★ | 禁止（`CLAUDE.md` §3） |
| 4-21 | ★`findUnique` の `where` に施設・清掃エリアの条件を足そうとする★ | ★`errorReportById()` は `select` で `facility.deleted_at` / `section.deleted_at` を読み、`isVisibleErrorRow()` で null を返す★（`error_id !== id` の null と同じ並び。4-8 の前例）。`findUnique` の一意条件はいじらない |
| 4-22 | ★可視性の条件を SQL だけに書く／純粋関数だけに書く★ | ★両方★: SQL（`ERROR_ROW_VISIBLE_WHERE` を `errorReportListWhere()` と `dsAlertWhere()` に混ぜる）で読む量を減らし、★正は `isVisibleErrorRow()`★（fixtures はこちらしか通らない。`USE_FIXTURES` で件数を変えない） |
| 4-23 | ★施設名・分類の解決（`resolveFacilityIdsByName()` / `resolveFacilityIdsByMasters()`）に `deleted_at` の条件を足す★ | ★足さない★（罠7。過去の実績・履歴の検索が静かに消える）。U8 の範囲は `robot_error_reports` の行の可視性だけ |
| 4-24 | ★`/alerts`（事前チェック。`prechecks()` / `precheckItemById()`）にも同じ条件を入れる★ | ★入れない★（範囲外。§10）。削除済みの施設の行が `/alerts` に出るかは監督が確かめる（§12-2） |
| 4-20 | ★`git add -A`★ | 禁止（`CLAUDE.md` §7）。★作業ツリーにある他人の変更（`CLAUDE.md`・`docs/decisions.md` の D-179・`docs/handoff/` の未追跡ファイル）をコミットに含めない★ |

---

## 段階0 — 着手前

1. ★`develop` で作業する★（`.claude/worktrees/` の中ではない。ワークツリーは main 基点で古い）。`git pull` で最新にし、`64c4079` 以降であることを確かめる。
2. `git status` を控える。★`CLAUDE.md`・`docs/decisions.md`（D-179）・`docs/handoff/TeamWorks確認事項_第13回_*`・`docs/handoff/回答待ち一覧_*`・`docs/handoff/pdf/*.py` は他のセッションの作業★。触らない・コミットに含めない。
3. `npx prisma generate` → `npx tsc --noEmit -p .` → `npm test` が通ることを確かめ、★テストの件数を記録する★。通らなければ着手前に報告。
4. ★Next.js は版が新しい★。画面・ルートを書く前に `node_modules/next/dist/docs/` の該当の項を読む（`CLAUDE.md` 末尾）。
5. ★§11 Q5〜Q8 は監督判断（2026-10-19）で確定済み★。★表5（U8）も §11 Q9 で監督が確認済み（2026-10-19）★。

---

## 段階1 — 期待値表をテストにする（実装より先）

★この時点ではテストが落ちる（関数・型が無い）のが正しい★。コミットはテストだけで1つ（「テストだけ。この時点では落ちる」と明記）。

1. ★`lib/db/__tests__/ds-alert-mapping.test.ts`（新規）★: 冒頭コメントに表1 を写し「R1〜R7・R11 は監督確定、R8〜R10・R12・R13 は指示書AX で導き監督が正としたもの（2026-10-19）」と書く。
   - `isDsAlertRow()`: R1〜R13 の出る／出ない（`it.each`）
   - `dsAlertFromErrorRow()`: R1〜R4・R11 の各値（`alert_type` / `device_serial` / `facility_id` / `facility_name` / `section_name` / `detected_at` / `resolution_status` / `responded_at` / `resolved_at` / `note`）。★R4 は `created_at` が `detected_at` に入る★
   - `dsAlertsFromErrorRows()`: 13行を入れて7行が出る・並びは呼び出し側（`sortDsAlerts()`）に任せるので ★順は問わない★（`toEqual(expect.arrayContaining(...))` と件数）
   - `dsAlertWhere()`: `{ error_type: { in: [...D_SEC_ALERT_ERROR_TYPES] }, status: { in: [0,1,2 を robotErrorStatusFromResolutionStatus から作ったもの] }, ...ERROR_ROW_VISIBLE_WHERE }` と `toEqual`（★期待値も変換関数から作る。直書きしない★。可視性は U8・段階2-7）
2. ★`lib/ds-alerts/__tests__/query.test.ts`（直す）★:
   - `row()`（`:11-25`）を新しい型に（`id: "1"`・`resolution_status: "unresolved"`・`note: null`）
   - `status` の選択肢の422の文言を `open / all / resolved` に（`:67-77`）。★`status=done` は422★（表2 A4）
   - `filterDsAlertsByStatus`（`:168-197`）: `open` = 未解消＋対応中／`resolved` = 解消のみ／`all`。「`type=disconnected & status=done` で0件」のテスト（`:187`）は `status=resolved` に読み替え、★「fixtures の再現条件」という説明は消す★（fixtures の意図的な0件は無くなる。段階3）
   - `sortDsAlerts`（`:199-217`）: 同値のときの ★文字列の辞書順★（`"10"` が `"9"` より先）を足す
   - `summarizeDsAlerts`（`:219-233`）: キーを `unresolved` / `in_progress` / `resolved` に
3. ★`lib/ds-alerts/__tests__/edit.test.ts`（新規）★: 表3 S1〜S6
4. ★`lib/dashboard/__tests__/aggregate.test.ts`（直す）★: `alert()`（`:72-91`）を新しい型に（`id: String(seq)`・`resolution_status` は `PrecheckStatus`・`resolved` のとき `resolved_at`・`note: null`）。`computeSecurityBanner` のテスト（`:300-340`）は `"pending"` → `"unresolved"`、`"done"` → `"resolved"`。`not.toHaveProperty("memo")` は ★`"note"` にも当てる★（両方書いてよい）
5. ★`lib/db/__tests__/robot-error-select.test.ts`★: `SELECTS`（`:19-27`）に `ROBOT_ERROR_DS_ALERT_SELECT` を足す
6. ★`lib/db/__tests__/ds-status.test.ts`★: `resolutionStatusFromControlErrorStatus` / `controlErrorStatusFromResolutionStatus` / 往復の describe（`:33-70`）を削る（D14）。`connectionStatus*` の2つは残す
7. ★`lib/db/__tests__/error-report-visibility.test.ts`（新規）★: `isVisibleErrorRow()` — 両方生きている → true／施設だけ削除 → false／清掃エリアだけ削除 → false／両方削除 → false（表5 V1〜V7 の可視性の列）。`ERROR_ROW_VISIBLE_WHERE` が `{ facility: { deleted_at: null }, section: { deleted_at: null } }` と `toEqual`
8. ★`lib/db/__tests__/error-report-mapping.test.ts`（直す）★: ★行の組み立てに `facility.deleted_at` / `section.deleted_at`（既定 null）を足す★（型が要求する）。`errorReportKeyFromRow()` の `visible` が表5 どおり。★`errorReportListWhere()` の期待値に `ERROR_ROW_VISIBLE_WHERE` の2キーが入る★（旧「既定 → `status` だけ」は「`status` と施設・清掃エリアの可視性」に直す。`facility_ids` / 期間のケースも同じ2キーを含む）。★AW の表1 E1〜E9 の期待値そのものは変えない★（全行が生きている）
9. ★`lib/error-reports/__tests__/query.test.ts`（直す）★: キーの組み立てに `visible: true` を足す（型）。★`visible: false` のキーは既定でも `include_resolved=true` でも外れ、`total` に数えない★（表5 V2・V3・V7）
10. `lib/db/__tests__/ds-alert-mapping.test.ts`（1 の続き）: 表5 V4〜V7 の D-Sec 列。`dsAlertWhere()` の期待値に `ERROR_ROW_VISIBLE_WHERE` の2キー
11. `lib/db/__tests__/robot-error-select.test.ts`: ★`ROBOT_ERROR_LIST_KEY_SELECT` / `ROBOT_ERROR_LIST_ROW_SELECT` / `ROBOT_ERROR_DS_ALERT_SELECT` が `facility.select.deleted_at` と `section.select.deleted_at` を含む★ことを足す（可視性の判定に要る列の取り忘れを防ぐ）

---

## 段階2 — 設定・型・純粋関数

### 2-1. `lib/config/index.ts` — 設定値を1つ足す

`PRECHECK_NOTE_MAX_LENGTH`（`:628`）の近くに:

```ts
/**
 * D-Sec アラートとして出す `robot_error_reports.error_type` の値（指示書AX D1。D-180）。
 * ★完全一致（JS の `===`）で比べる★ — SQL の `IN` は写し（DB の照合順序は大小文字・末尾の空白を区別しないため）。
 * ★`SECURITY_ON` は入れない★（ユーザー判断 2026-10-19。D-091 項3「セキュリティが無効であっても異常ではない」）。
 * ★`NOT_STARTED`（清掃開始失敗）も入れない★（ロボット側の出発失敗。同日のユーザー判断）。
 * 値の出所: TeamWorks 第10回 N-5（`D-S_CONTROL_FAILURE` = "no regular report from the D-S control."）。
 */
export const D_SEC_ALERT_ERROR_TYPES: readonly string[] = ["D-S_CONTROL_FAILURE"];
```

★配列にする★（後から値を足せる）。★`as const` にしない★（`includes(row.error_type)` で `string` を渡すため）。

### 2-2. `lib/types/index.ts`（`:1033-1058`）

- ★`DsAlertStatus` を削除★。`DsAlert.resolution_status: PrecheckStatus`（D8）
- `DsAlertStatusFilter = "open" | "all" | "resolved"`（D10）。docstring「open = unresolved + in_progress」
- `DsAlert`:
  - `id: string` — docstring「`robot_error_reports.error_id`（★string のまま。`Number()` を通さない。D-105★）」
  - `device_serial` — 「`robot_error_reports.serial_number` の生値（D-077）。制御盤の SN」
  - `section_name` — ★旧 docstring の「実DB上の取得元が不明なため fixtures では暫定のダミー値」を消し★、「行の `section_id` の清掃エリア名」に
  - `detected_at` — 「`errored_at ?? created_at`（`detectedAtFromError()`）」
  - ★`memo` → `note`★（D9）
  - `alert_type` — 「★DB からは `disconnected` しか出ない★（`security_off` は互換のため残す。D-180）」
- `DsAlertType` / `DsAlertTypeFilter` は変えない

### 2-3. `lib/db/robot-error-select.ts` — 定数を1つ足す（末尾）

```ts
/**
 * D-Sec アラート（`dsAlertFromErrorRow()`。`dsAlerts()`）に要る列（指示書AX）。
 * ★`section` は清掃エリア名だけ★（D-Sec のカードに出す）。★`robot_id` / `robot` を足さない★（D-Sec の行は全部 `robot_id` NULL）。
 * ★`clean_timetable_id` は使わないが、select の共通テスト（紐づけのキーを含む）のために読む★
 */
export const ROBOT_ERROR_DS_ALERT_SELECT = {
  error_id: true,
  clean_timetable_id: true,
  serial_number: true,
  facility_id: true,
  facility: { select: { facility_name: true, deleted_at: true } },   // deleted_at は可視性（U8・段階2-7）
  section: { select: { section_name: true, deleted_at: true } },
  error_type: true,
  errored_at: true,
  created_at: true,
  status: true,
  responded_at: true,
  resolved_at: true,
  comment: true,
} as const satisfies Prisma.RobotErrorReportSelect;
```

★`ROBOT_ERROR_LIST_ROW_SELECT` の docstring「`section` を足さない」はその定数についての話なので、そちらは変えない★。

### 2-4. `lib/db/ds-alert-mapping.ts`（新規）— ★純粋関数のみ★（`lib/db/error-report-mapping.ts` と同じ作法）

```ts
/** `ROBOT_ERROR_DS_ALERT_SELECT` の結果がそのまま入る形 */
export interface DsAlertRowSource extends PrecheckDetectedAtSource {
  error_id: string;
  serial_number: string;
  facility_id: bigint;
  facility: { facility_name: string; deleted_at: Date | null };
  section: { section_name: string; deleted_at: Date | null };
  error_type: string;
  status: number;
  responded_at: Date | null;
  resolved_at: Date | null;
  comment: string | null;
}

/** ★D-Sec アラートの対象か（正）★: `error_type` が `D_SEC_ALERT_ERROR_TYPES` に `===` で一致し、`status` が 0/1/2 */
export function isDsAlertRow(row: Pick<DsAlertRowSource, "error_type" | "status">): boolean
/** 1行 → `DsAlert`（★`isDsAlertRow()` を通った行だけを渡す★。`status` が 0/1/2 以外なら変換関数が例外） */
export function dsAlertFromErrorRow(row: DsAlertRowSource): DsAlert
/** `rows.filter((r) => isDsAlertRow(r) && isVisibleErrorRow(r)).map(dsAlertFromErrorRow)`（fixtures 版・Prisma 版が共有する1本。可視性は U8） */
export function dsAlertsFromErrorRows(rows: readonly DsAlertRowSource[]): DsAlert[]
/** SQL 側の写し: `{ error_type: { in: [...D_SEC_ALERT_ERROR_TYPES] }, status: { in: dsAlertStatusCodes() }, ...ERROR_ROW_VISIBLE_WHERE }` */
export function dsAlertWhere(): Prisma.RobotErrorReportWhereInput
```

- `isDsAlertRow()`: ★`D_SEC_ALERT_ERROR_TYPES.includes(row.error_type)`★（`Array.prototype.includes` は `===` 相当。★`toUpperCase()` / `trim()` で緩めない★）かつ ★`dsAlertStatusCodes().includes(row.status)`★
- 状態のコード: `(["unresolved", "in_progress", "resolved"] as const).map(robotErrorStatusFromResolutionStatus)`（★0/1/2 を直書きしない★。非公開の関数 `dsAlertStatusCodes()` にまとめてよい）
- `dsAlertFromErrorRow()`:
  - `id: row.error_id`（★string のまま★）／`alert_type: "disconnected"`／`device_serial: row.serial_number`（★そのまま★）
  - `facility_id: Number(row.facility_id)`／`facility_name: row.facility.facility_name`／`section_name: row.section.section_name`
  - `detected_at: jstIsoFromDb(detectedAtFromError(row))`
  - `resolution_status: resolutionStatusFromRobotErrorStatus(row.status)`
  - `responded_at` / `resolved_at`: `null` はそのまま、それ以外 `jstIsoFromDb()`／`note: row.comment`
  - ★`errorReportItemFromRow()`（`lib/db/error-report-mapping.ts:136`）と同じ書き方で揃える★（日時・BigInt の扱い）
- 冒頭の docstring に: 根拠（P-10 = A・U1〜U3・D1〜D3）、★`control_error_histories` は読まない★、★`robot_id` を読まない★、★`security_off` はここからは出ない（D4）★

### 2-5. `lib/ds-alerts/query.ts`

- `STATUS_FILTERS`（`:15`）を `["open", "all", "resolved"]` に
- `filterDsAlertsByStatus()`（`:96-111`）: `open` → `resolution_status !== "resolved"`／`resolved` → `=== "resolved"`／`all`
- `sortDsAlerts()`（`:113-120`）: 同値は ★`a.id < b.id ? -1 : a.id > b.id ? 1 : 0`★（D11）。docstring に「id は string。辞書順」
- `DsAlertSummary`（`:122-126`）: `{ unresolved; in_progress; resolved }`／`summarizeDsAlerts()`（`:136-`）も同じキーで数える
- ★`TYPE_FILTERS` と `filterDsAlertsByType()` は変えない★（D4）

### 2-6. `lib/ds-alerts/edit.ts`（新規）— ★純粋関数のみ★（画面の保存で使う。表3）

```ts
/** PATCH /errors/{error_id}/status の応答（`ErrorReportItem`）を `DsAlert` に戻す。★4項目だけ差し替え★。id が違えば例外 */
export function dsAlertWithResolution(row: DsAlert, updated: ErrorReportItem): DsAlert
/** 保存で状態が変わった行の分だけ `summary` を付け替える（旧状態 −1・新状態 +1）。★入力を書き換えない★ */
export function adjustDsAlertSummary(
  summary: DsAlertSummary,
  before: readonly DsAlert[],
  updated: ReadonlyMap<string, DsAlert>
): DsAlertSummary
```

`before` は保存前の画面の行（`id` で `updated` と突き合わせる）。`updated` に無い行・状態が同じ行は数えない（表3 S4・S6）。

段階1 のテストのうち純粋関数のものが通ること。コミット。

---

### 2-7. 削除済みの施設・清掃エリアの行を外す（U8・D12）— エラー一覧と D-Sec の共通部品

`lib/db/error-report-mapping.ts`（★純粋関数のみのまま★）に足す:

```ts
/** 可視性の判定に要る列（`facility` / `section` の `deleted_at`） */
export interface ErrorRowVisibilitySource {
  facility: { deleted_at: Date | null };
  section: { deleted_at: Date | null };
}
/**
 * ★削除済み（論理削除）の施設・清掃エリアの行は見せない★（ユーザー決定 2026-10-19「削除済みのものは表示する必要がないので、
 *   エラー一覧からもD-sec一覧からも消してください」。指示書AX U8・D12。D-180）。★`deleted_at` が NULL でなければ削除済み（日時によらない）★。
 * ★これが正★。SQL の `ERROR_ROW_VISIBLE_WHERE` は写し（fixtures はこちらしか通らない）。
 * ★施設名・分類の解決は `deleted_at` で絞らない（罠7）★ — この関数は `robot_error_reports` の行の可視性だけを決める
 */
export function isVisibleErrorRow(row: ErrorRowVisibilitySource): boolean {
  return row.facility.deleted_at === null && row.section.deleted_at === null;
}
/** SQL 側の写し（`errorReportListWhere()` と `dsAlertWhere()` に混ぜる） */
export const ERROR_ROW_VISIBLE_WHERE = {
  facility: { deleted_at: null },
  section: { deleted_at: null },
} as const satisfies Prisma.RobotErrorReportWhereInput;
```

- ★`ErrorReportKeySource`★ に `ErrorRowVisibilitySource` を足し（`extends`）、★`ErrorReportKey` に `visible: boolean`★ を足す。`errorReportKeyFromRow()` が `visible: isVisibleErrorRow(row)` を入れる
- ★`ErrorReportRowSource`★ の `facility` を `{ facility_name: string; deleted_at: Date | null }` に、★`section: { deleted_at: Date | null }` を足す★
- ★`errorReportListWhere()`★ の戻りに ★`...ERROR_ROW_VISIBLE_WHERE`★ を混ぜる（`status` の次。docstring に U8）
- ★`filterSortErrorReportKeys()`（`lib/error-reports/query.ts:151-167`）★ の `filter` の先頭に ★`if (!key.visible) return false;`★（★絞り込みの正★。`meta.total` もこれで正しくなる）
- ★`lib/db/robot-error-select.ts`★:
  - `ROBOT_ERROR_LIST_KEY_SELECT` に ★`facility: { select: { deleted_at: true } }`・`section: { select: { deleted_at: true } }`★
  - `ROBOT_ERROR_LIST_ROW_SELECT` の `facility` を ★`{ select: { facility_name: true, deleted_at: true } }`★ に、★`section: { select: { deleted_at: true } }` を足す★。docstring の「★`section` を足さない★」は ★「`section` は `deleted_at` だけ（U8）。`robot_id` / `robot` は足さない」★ に書き換える（元の理由は `robot_id` が NULL の行で落ちることで、`section` は無関係）
  - `ROBOT_ERROR_DS_ALERT_SELECT` は 2-3 のとおり（`deleted_at` 入り）
- ★`lib/db/ds-alert-mapping.ts`★: `dsAlertsFromErrorRows()` は `isDsAlertRow(r) && isVisibleErrorRow(r)`、`dsAlertWhere()` は `...ERROR_ROW_VISIBLE_WHERE` を混ぜる（2-4 のとおり）
- ★`PATCH /errors/{error_id}/status` のルート（`app/api/errors/[error_id]/status/route.ts`）は変えない★ — 実在確認（`:126` の `db.errorReportById()`）が null を返せば既存の404の分岐に入る

## 段階3 — データ層（fixtures・Prisma）

### 3-1. fixtures の行 — `lib/fixtures/error-reports.ts`

1. `FixtureErrorReportRow`（`:34-36`）に ★`section: { section_name: string; deleted_at: Date | null }`★ を足す（★全行に持たせる★。実DBでも `section_id` は NOT NULL）。`robotError()`（`:85-106`）の既定は `{ section_name: "1F売り場", deleted_at: null }`。★`facilityOf()`（`:64-68`）が返す `facility` にも `deleted_at: null` を足す★（U8）
2. ★セキュリティ・清掃開始失敗のシリアルを実データに揃える★: `FX-ERR-08`（`:179-188`）と `FX-ERR-11`（`:206-215`）の `serial_number: "0"` → ★`"---"`★。
   ★定数 `SERIAL_NOT_APPLICABLE = "---"` を置き、コメントに「ステージングの実データ（2026-10-19 監督の SELECT・先方投入）。P-8 の回答は `0` だが実値は `---`（§12 申し送り2）」★
3. ★D-Sec の行を足す★（`FX-ERR-12`〜`FX-ERR-17`。`robot_id: null`・`error_type: D_SEC_ALERT_ERROR_TYPES[0]`・`error_level: "CRITICAL"`（N-5））。★シリアルは `controlPanelSerialOf()` で引く★（直書きしない）:

   | id | 施設（顧客） | シリアル | `status` | `errored_at` / `created_at` | その他 | 表1 |
   |---|---|---|---|---|---|---|
   | FX-ERR-10（既存） | 1（1） | 施設1の制御盤（`DS-001`） | 0 | 前日 22:45 / 22:46 | `section` 1F売り場 | R1 |
   | FX-ERR-12 | 11（2） | 施設11の制御盤 | 0 | ★`errored_at` NULL★ / 当日 02:55 | ★`clean_timetable_id` NULL★・`section` 2F衣料品 | R4・R13 |
   | FX-ERR-13 | 5（1） | 施設5の制御盤 | 1 | 前日 23:10 / 23:11 | `responded_at` 当日 09:05・備考「回線業者に確認中」・`section` 1Fレジ前 | R2 |
   | FX-ERR-14 | 4（2） | 施設4の制御盤 | 2 | 前日 03:55 / 03:56 | `responded_at` 前日 08:40・`resolved_at` 前日 09:15・備考「正常復帰を確認」・`section` B1食品 | R3 |
   | FX-ERR-15 | 10（3） | 施設10の制御盤 | 2 | 2日前 02:00 / 02:01 | `responded_at` / `resolved_at` あり・備考「正常復帰を確認」 | R3 |
   | FX-ERR-16 | ★3（1）★ | ★施設2の制御盤★ | 0 | 当日 01:30 / 01:31 | ★シリアルと施設が食い違う★（ステージング 1000004 の形） | R11 |
   | FX-ERR-17 | 8（1） | 施設8の制御盤 | 1 | ★当日 01:30（FX-ERR-16 と同時刻）★ / 01:32 | `responded_at` あり | R12（`"FX-ERR-16"` が先） |

   - `clean_timetable_id` は FX-ERR-12 以外 `TT_MAIN_A` / `TT_MAIN_B` のどちらか（P-9: 制御盤の行にも予定 id が入る）
   - ★施設は顧客1と顧客1以外の両方にまたがること★（`app/api/__tests__/master-filter.test.ts:194-201` / `:364-380` が「`customer_id=1` で真に減る」を見ている）
   - ★R8〜R10（綴り違い・`status` 3）は fixtures に入れない★（単体テストとルートテストのスタブにだけ置く。fixtures はデモにも使うので）
4. ★削除済みの施設・清掃エリアの行を2つ足す★（U8。表5。どちらも `status` 0 — ★可視性が無ければ既定で出るはずの行★にして、外れることを確かめる）:

   | id | 内容 | 施設 | 清掃エリア | 表5 |
   |---|---|---|---|---|
   | FX-ERR-18 | ロボットのエラー（`EmergencyStop`・`robot_id` あり） | ★削除済みのダミー施設★（id `99`・名前「削除済みテスト施設（ダミー）」・`deleted_at` 過去の日時）。★`facilities`（`lib/fixtures/facilities.ts`）には足さない★（施設一覧・分類・他の画面に出さないため）。専用の小さな関数で作る（`facilityOf()` を通さない） | 生きている | V2 |
   | FX-ERR-19 | `D-S_CONTROL_FAILURE`（`robot_id` NULL・施設2の制御盤のシリアル） | 施設2（生きている） | ★削除済み★（`section_name` 「旧2F売り場（削除済み）」・`deleted_at` 過去の日時） | V6（`status` は 0 にする） |

   - ★施設2の他の行（FX-ERR-04・05）の清掃エリアは生きている★ので出続ける（表5 V9）
   - 実在の店名を使わない（`CLAUDE.md` §7）
5. ファイル冒頭の docstring の件数・説明を直す（★`errorReportRows` は19行。そのうち見える行は17行（既定（未解消・対応中）14行・「解消済みも表示」17行）★、D-Sec は7行（FX-ERR-19 は削除済みの清掃エリアなので出ない））

### 3-2. `lib/fixtures/ds-alerts.ts` を削除し、`lib/fixtures/index.ts:19` の `export * from "./ds-alerts";` を消す

### 3-3. `DataSource` と2つの実装 — `lib/db/index.ts`

- ★`DataSource.dsAlerts()`（`:371`）の契約は変えない★（引数なし・`Promise<DsAlert[]>`。`/dashboard` も呼ぶ）。docstring に「`robot_error_reports` の `D_SEC_ALERT_ERROR_TYPES` の行（指示書AX）。★並べ替え・絞り込み（type / status / 分類）はルート側★」
- ★fixtures 版★（`:683-685`）:
  ```ts
  async dsAlerts() {
    // ★Prisma 版と同じ純粋関数★（D13）。★エラー一覧の上書き表を反映してから作る★ —
    //   `PATCH /errors/{error_id}/status` で書いた対応状況が D-Sec にも出る（U4）
    return dsAlertsFromErrorRows(errorReportRows.map(withFixtureErrorReportOverride));
  },
  ```
- ★Prisma 版★（`:1954-1996`。docstring ごと置き換える）:
  ```ts
  async dsAlerts() {
    const { getPrisma } = await import("./prisma");
    const prisma = await getPrisma();
    // ★SQL は写し（`error_type IN`・`status IN`）。正は `dsAlertsFromErrorRows()` の `===`（D2・D3）★
    const rows = await prisma.robotErrorReport.findMany({
      where: dsAlertWhere(),
      select: ROBOT_ERROR_DS_ALERT_SELECT,
    });
    return dsAlertsFromErrorRows(rows);
  },
  ```
  ★旧 docstring の「`security_off` は返さない理由（`security_statuses` に列が無い）」は消し★、新しい理由（U1。D-091 項3）に書き換える。
- import の整理: `dsAlerts`（fixtures。`:60`）と `resolutionStatusFromControlErrorStatus`（`:168`）を消す。`ROBOT_ERROR_DS_ALERT_SELECT` / `dsAlertWhere` / `dsAlertsFromErrorRows` を足す
- ★`lib/db/index.ts:15` 付近の冒頭コメント「書き込んでよいのは robot_error_reports / control_error_histories の…」は変えない★（権限の説明として正しい）

### 3-3b. エラー一覧の可視性（U8）— `lib/db/index.ts`

- ★fixtures 版 `errorReports()`（`:647-656`）★: 変えなくてよい（`errorReportKeyFromRow()` が `visible` を入れ、`filterSortErrorReportKeys()` が外す）。★確かめるだけ★
- ★fixtures 版 `errorReportById()`（`:658-667`）★: 行が見つかっても ★`!isVisibleErrorRow(row)` なら null★（＝ PATCH は404）
- ★Prisma 版 `errorReports()`（`:1743-1783`）★: 1段目の `where` は `errorReportListWhere()`（2-7 で可視性入り）・`select` は `ROBOT_ERROR_LIST_KEY_SELECT`（`deleted_at` 入り）のまま。コードの変更は無いはず
- ★Prisma 版 `errorReportById()`（`:1785-1806`）★: `report.error_id !== id` の null の直後に ★`if (!isVisibleErrorRow(report)) return null;`★（4-21）。コメントに U8
- ★`errorReportItemsInPageOrder()`（`:540-556`）★: ★見えない行も「消えた行」と同じく飛ばす★（1段目と2段目の間に施設が削除された場合。`meta.total` は1段目のまま — 4-15 と同じ扱い）
- ★`updateErrorReportColumns()` は変えない★（存在・可視性はルートが書く前に `errorReportById()` で確かめる）

### 3-4. 件数が変わるテスト・資料を直す（★エラー一覧の fixtures が 11 → 19 行。見える行は17★）

- `lib/db/__tests__/fixture-error-reports.test.ts`: `:49-53`（17行・`FX-ERR-01〜17`）／`:88`・`:92`（シリアル `---`）／`:96-97`（`robot_id` NULL は `08`・`10`〜`17`）／`:102-108`（既定14・全17）／`:120` 付近（ページの `total`）／並びの期待（降順の列）を ★テストを走らせて得た順ではなく、表の時刻から手で導いて★ 書く
- `lib/db/__tests__/fixture-error-reports.test.ts` の続き: ★`errorReportRows` は19行（`FX-ERR-01〜19`）★・`robot_id` NULL は `08`・`10`〜`17`・`19`・★既定14・全17（FX-ERR-18・19 は出ない）★・★`db.errorReportById("FX-ERR-18")` / `("FX-ERR-19")` は null★
- `app/api/errors/__tests__/route.fixtures.test.ts`: 件数（既定14・全17）。★`PATCH /api/errors/FX-ERR-18/status`・`FX-ERR-19` は404★（表5 V2・V6）
- ★新規 `app/api/ds-alerts/__tests__/route.fixtures.test.ts`★: fixtures で `GET /api/ds-alerts`（既定5件・`status=all` で7件・`summary` `{unresolved:3, in_progress:2, resolved:2}`・`FX-ERR-08` / `FX-ERR-11` / ★`FX-ERR-19`（削除済みの清掃エリア）★ が出ない）→ ★`PATCH /api/errors/FX-ERR-10/status`（`resolved`）→ `GET /api/ds-alerts` に反映（`status=open` から消え、`summary.resolved` が 3）★。`beforeEach` で `resetFixtureErrorReportOverrides()`

段階1 の残りのテストが通ること。コミット。

---

## 段階4 — API（YAML → ルート）

### 4-1. `d-watch-api.yaml`（★先に直す。`CLAUDE.md` §6-1★）

1. `/ds-alerts`（`:1452-1484`）の `description` に:
   - ★取得元は `robot_error_reports` のうち `error_type` が `D-S_CONTROL_FAILURE` の行（完全一致。D-180）★。`SECURITY_ON`・`NOT_STARTED` は含まない（エラー一覧 `/errors` には出る）
   - ★`alert_type` は DB からは `disconnected` だけ★。`security_off` は互換のため enum に残すが出ない
   - 施設は行の `facility_id`（制御盤のシリアルの所属ではない）／`detected_at` は `errored_at`（無ければ `created_at`）
   - 並び順は `detected_at` の降順、同値は ★`id`（`error_id`）の昇順（文字列の辞書順）★
   - ★対応状況・備考の更新は `PATCH /errors/{error_id}/status`（同じ行）★
2. `status` パラメータ（`:1469-1475`）: `enum: [open, all, resolved]`／`description: open=未解消・対応中 / resolved=解消`
3. ★`/ds-alerts/{id}/status`（`:1486-1511`）を削除★（D7）
4. `DsAlert`（`:2647-2678`）: `id: { type: string, maxLength: 50, example: "1000001", description: robot_error_reports.error_id }`／`alert_type` の `description` に D4／`device_serial` の example を `"DFAS010001"`・description「生値（D-077）」／
   `section_name` の description を「行の section_id の清掃エリア名」（★「取得元が未確定で fixtures では暫定のダミー値」を消す★）／`detected_at` に description／
   `resolution_status: enum [unresolved, in_progress, resolved]`・description「未解消 / 対応中 / 解消」／★`memo` → `note`★
5. `DsAlertListResponse.summary`（`:2683-2688`）: `unresolved` / `in_progress` / `resolved`
6. `security_banner`（`:308-316`）の `description` に「★`/ds-alerts` が DB から `security_off` を返さないため、DB 接続時は常に空配列（D-180）★」を足す

### 4-2. `app/api/ds-alerts/route.ts`

- 冒頭コメント（`:1-10`）の「★対応状況の更新（PATCH /ds-alerts/{id}/status）は今回作らない…★」を ★「対応状況・備考の更新は `PATCH /errors/{error_id}/status`（`robot_error_reports` の同じ行。指示書AX D6）。このルートは読むだけ」★ に書き換える
- ★処理の流れは変えない★（`parseDsAlertQuery()` → 分類の解決と `db.dsAlerts()` を同じ try → `filterRowsByFacilityIds()` → `summary` → type / status → 並べ替え → ページ）。型の変更に追従するだけ

### 4-3. `app/api/dashboard/route.ts`

★コードは変えない★（`db.dsAlerts()` → `computeSecurityBanner()` のまま。D4）。`:189-211` 付近に「★DB のデータでは `security_off` が出ないのでバナーは常に空（指示書AX D4）★」のコメントを1つ足してよい。

### 4-4. テスト

1. ★`app/api/ds-alerts/__tests__/route.test.ts`（新規。Prisma 経路）★ — `app/api/errors/__tests__/route.test.ts` の作法（`vi.mock("@/lib/db/use-fixtures", () => ({ USE_FIXTURES: false }))`・`@/lib/db/prisma` のスタブ）:
   - スタブの `robotErrorReport.findMany` は ★`guardNullRobotId()` を通し★、★`where` を無視して全行（表1 の13行）を返す★（＝ SQL が ci・PAD SPACE で余分に当てた状態を再現。D2）
   - ★`findMany` の引数の `where` が `dsAlertWhere()` と `toEqual`、`select` が `ROBOT_ERROR_DS_ALERT_SELECT`★（ルートが別の条件を組み立てていない）
   - 表2 A1〜A7・A9。★R5〜R10 が出ない★・★R11 が施設3で出る★・★R12 の並び★
   - ★`controlErrorHistory` のスタブを置かない（呼ばれたら落ちる形にする）★ — もう読まないことの証拠
   - 分類（A8）は既存の `master-filter.test.ts` に任せてよい（fixtures 経路）。★Prisma 経路で施設名・分類を同時に試すと、モックした `@/lib/db/prisma` の動的 import の罠を踏む★（指示書AW §13-4 #2）。要るなら `db.facilityGroupIndex()` 等を `vi.spyOn` で差し替える
1b. ★`app/api/errors/__tests__/route.test.ts`（直す。Prisma 経路）★: スタブの行に `facility.deleted_at` / `section.deleted_at` を持たせ（既存の行は null）、★スタブの `where` の評価に `facility.deleted_at` / `section.deleted_at` の条件を足す★。表5 V1〜V3・V9 の行を足し、★V2・V3 が既定でも `include_resolved=true` でも出ず `meta.total` に数えない★、★V8（V2 の施設名で絞る）が0件・200★。★`where` を無視して全行返すスタブでも V2・V3 が出ない★（純粋関数側で外れる）ことを1件確かめる。★1段目の `where` が `errorReportListWhere()` と `toEqual`★ は既存のまま（可視性が入った戻りと一致）
1c. ★`app/api/errors/[error_id]/status/__tests__/route.test.ts`（足す）★: `findUnique` のスタブが ★施設が削除済みの行（V2）・清掃エリアが削除済みの行（V3）・D-Sec の V5★ を返す → ★404（`error.code` は既存の404と同じ）・`updateMany` を呼ばない・`db.write` を出さない★。生きている行（V1・V4）は200（既存）
1d. `app/api/ds-alerts/__tests__/route.test.ts`（1 の続き）: 表5 V4〜V7。★V5〜V7 は一覧にも `summary` にも出ない★（`where` を無視して全行返すスタブで）
2. ★`app/api/dashboard/__tests__/route.test.ts`★: スタブの `controlErrorHistory`（`:174`）を消す（読まないので）。★`robotErrorReport.findMany` のスタブ（`:172`）は `[]` のまま★でよい。★表1 R10（`status` 3）の行を返しても200★のテストを1つ足す（`dsAlerts()` がダッシュボードを落とさない証拠。D3）
3. `app/api/__tests__/master-filter.test.ts`: `:364-380` の `summary` の型とキーを `unresolved` / `in_progress` / `resolved` に。`:194-201` は `status=all` のまま（`all` は残る）
4. ★既存のテストが無修正で通ること★（★U8 で `app/api/errors/**` と `lib/db/__tests__/error-report-mapping.test.ts`・`lib/error-reports/__tests__/query.test.ts` は行の型の追従と表5 の追加が要るので、このリストから外れる★）: `app/api/prechecks/**`・`lib/prechecks/__tests__/*`・`lib/db/__tests__/robot-error-write-sql.test.ts`・`lib/db/__tests__/error-report-mapping.test.ts`（★AW の表1 E8 のシリアル `'0'` はテスト内の行なので変えない★）

YAML → ルート → テストの順でコミット。

---

## 段階5 — 画面（`/ds-alerts` に編集モード）

★見た目と操作はエラー一覧（`components/features/errors/ErrorsScreen.tsx`・`.module.css`）から借りる。新しい共通部品は作らない★（指示書AW と同じ考え方）。
★D-Sec 画面はカードの一覧（表ではない）★なので、カードの中で同じ部品を使う。

### 5-1. `components/features/ds-alerts/DsAlertsScreen.tsx`

1. **型の追従**: `DsAlertStatusFilter` は `"open" | "all" | "resolved"`、`EMPTY_SUMMARY` は `{ unresolved: 0, in_progress: 0, resolved: 0 }`、バッジの分岐（`:294-308`）は `resolved` / `in_progress` / `unresolved`
2. **文言（表4。§2 U5）**: 数値カード（`:141-143`）・状態ボタン（`:229-261`）・バッジを表4 のとおり。★副題（`:136`）と種別のセレクト（`:217-228`）・`typeFilter` の state は ★変えない★（U6。ユーザー判断で残す。「セキュリティ無効」を選ぶと0件になるのは承知のうえ）★。★編集中は種別のセレクトも他の絞り込みと同じく無効にする★（`<fieldset disabled>` の中に入れる）。カードの「通信断」Pill（`:281`）は残す
3. **編集モード**（エラー一覧の `ErrorsScreen.tsx` の `editing` / `drafts` / `rowErrors` / `saving` / `beforeunload` / `canStartEdit` / `filtersDisabled` / `startEdit` / `cancelEdit` / `saveEdit` をそのまま写す）:
   - 見出しの右に ★`編集` ／ 編集中は `保存（N件）`（N=0 なら `保存`、保存中は `保存中…`）・`キャンセル`★（ボタンは固定幅。`ErrorsScreen.module.css:53-` の `.editButton` 等を写す）
   - 下書きは ★`lib/prechecks/edit-drafts.ts` の `draftStatus` / `draftNote` / `setDraftStatus` / `setDraftNote` / `saveDrafts`★（`DsAlert` がそのまま `EditableRow` になる。D9）
   - 送る関数:
     ```ts
     /** ★書き込みはエラー一覧と同じルート（指示書AX D6）★。応答は ErrorReportItem なので DsAlert に戻す */
     async function sendDsAlertUpdate(row: DsAlert, request: PrecheckStatusUpdateRequest): Promise<DsAlert> {
       const updated = await apiFetch<ErrorReportItem>(`/api/errors/${encodeURIComponent(row.id)}/status`, {
         method: "PATCH",
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify(request),
       });
       return dsAlertWithResolution(row, updated);
     }
     ```
   - 保存後: ★成功した行だけ差し替え★、★`summary` は `adjustDsAlertSummary(summary, 保存前の items, updated)`★（D15）。★1件でも失敗したら編集モードのまま★、失敗したカードの直下に文言（`t(rowError)`。サーバーの文言は辞書に無ければ原文）。全部成功なら通常モードへ
   - **カードの中身**: 通常モードは今のバッジ（★押しても何も起きない★）。編集モードは ★対応状況のプルダウン★（エラー一覧の `STATUS_OPTIONS` と同じ並び・文言。`.statusSelect` を写す）と ★備考の textarea★（2行・`resize: none`・`maxLength={PRECHECK_NOTE_MAX_LENGTH}`・placeholder `t("備考を入力")`）。
     ★通常モードの備考は今の `memoLine` の位置に `item.note` を出す★（空なら出さない＝今と同じ）
   - ★解消のときはバッジの下に解消日時★（`item.resolution_status === "resolved"` かつ ★保存済みの値で判定★。エラー一覧と同じ）
   - ★変えたカードの目印★（`ErrorsScreen.module.css` の `.rowChanged` と同じ色をカードの背景に）
   - ★編集中は絞り込み（分類のセレクト・状態ボタン）とページ送りを `<fieldset disabled>` で無効★、案内「編集中は絞り込みとページ送りを使えません」と失敗件数（`{n}件を保存できませんでした`）を出す
   - ★キャンセルは下書きがあるときだけ `window.confirm(t("変更を破棄しますか？"))`★
4. **読み込み**: `t("読み込み中…")` に直す（4-16）。effect の書き方は 4-18
5. ★key は `item.id`（文字列）のまま★

### 5-2. `components/features/ds-alerts/DsAlertsScreen.module.css`

編集ボタン・プルダウン・textarea・目印・案内・失敗の文言のクラスを `ErrorsScreen.module.css` から写す（★JP/EN で位置が動かないよう固定幅★）。

### 5-3. `lib/i18n.ts`

- 足すキー（★EN は暫定★）: `"未解消・対応中": "Unresolved / In progress"`（★副題は変えないので副題のキーは足さない★。U6）
- ★既にあるキーは足さない★: `未解消`・`対応中`・`解消`・`編集`・`保存`・`保存（{n}件）`・`キャンセル`・`保存中…`・`変更を破棄しますか？`・`編集中は絞り込みとページ送りを使えません`・`{n}件を保存できませんでした`・`備考を入力`・`読み込み中…`
- ★`未対応: "Unsupported"`（`:182`）は変えない★（対応機種バッジの訳。U5 で D-Sec 画面がこのキーを使わなくなるだけ）
- 使わなくなるキー（「対応済み」「未対応・対応中」等）は ★他の画面で使っていないか grep してから★ 消す。★「全種別」「セキュリティ無効」「通信断」「セキュリティ無効・通信断のアラート管理」は使い続ける（U6）ので消さない★

### 5-4. 確認

画面のテストは vitest で走らない（`vitest.config.ts`）ので段階10 で見る。保存の規則は純粋関数（表3）と `edit-drafts.test.ts` で固定してある。

コミット。

---

## 段階6 — ローカルDB のシード（`scripts/seed-local-db.ts`）

★ローカル（`dwatch_local`）の `/ds-alerts` を空にしないため★。★`lib/db/local-guard.ts` は触らない★。

1. ★`control_error_histories` への投入をやめる★: import（`:65` の `dsAlerts`、`:44` の `controlErrorStatusFromResolutionStatus`）と、`:812-830`（`security_off` を弾く・件数7の検査）・`:1160-1182`（`controlErrorHistory.create`）を消す。
   ★`:900` の `controlErrorHistory.deleteMany({})` は残す★（前回のシードの行を消すため）。`:2238` の件数のログから `control_error_histories` を外す
2. ★`robot_error_reports` に D-Sec の行を入れる★: `errorReportRows`（`lib/fixtures/error-reports.ts`）のうち ★`isDsAlertRow()` と `isVisibleErrorRow()` の両方を通る行★（★FX-ERR-19 は入れない★ — ローカルでは施設の生きている清掃エリアに付け替わって出てしまうため）を、次の値で `robotErrorReport.create`:
   - `id` = `error_id` = fixtures の `error_id`／`facility_id` = fixtures の値／`section_id` = ★`facilityIdToSectionId.get(facility_id)`★（無ければ例外。`:1094` と同じ）
   - `robot_id` = ★`section_id` と同じ値★（★ローカルのスキーマは `robot_id` が NOT NULL★。シードの既存の採番規則「sections.id とロボットの id は同値」に合わせたダミー。コメントで理由を書く）
   - `clean_timetable_id` = ★null★（fixtures の予定 id 9001 等はローカルに無い。予定不明として出さないため）
   - `serial_number` / `error_type` / `error_level` / `status` / `comment` / `errored_at` / `created_at` / `responded_at` / `resolved_at` は fixtures の値そのまま（日時は ★既に `toDbJst()` 済み★。もう一度通さない）
   - `error_detail` = 既存の `ERROR_DETAIL_PLACEHOLDER`
3. 件数の検査: ★D-Sec の行が7件★でなければ例外で止める（既存の「想定と異なれば中断」の作法）
4. ★清掃エリア名は fixtures の `section.section_name` ではなく、ローカルの `sections` の名前で出る★（`USE_FIXTURES` の true/false で清掃エリア名だけ違う。承知のうえ。コメントで書く）

コミット（シードだけで1つ）。

---

## 段階7 — （別件・独立）清掃詳細ログの 11px 横スクロールを消す

★段階1〜6 と独立★。先にやってもよい。★コミットは別にする★。

### 7-1. 原因（監督の調査）

画面幅 1440px のとき、表の入れ物の幅 ＝ 1440 − サイドバー 220 − `.main` の左右 padding 64 − カードの枠 2 ＝ ★1154px★。
行が多くて `.main` に縦スクロールバー（15px）が出ると ★1139px★。表の最小幅は ★126 + 1024 ＝ 1150px★ なので、★11px 横スクロールする★。

### 7-2. 直し方 — `components/features/detail-log/CleanLogTable.module.css`

- `:38` の ★`--col-facility: 126px;` → `110px;`★ → 最小幅 ★110 + 1024 ＝ 1134px★。
  縦スクロールバーが 17px でも入れ物は 1137px なので収まる。★画面幅 1435px 以上で横スクロールが出ない★（スクロールバー 15px のとき）
- ★他の列は変えない★（`.compact` の 240px も変えない）
- 見出し「施設名」/ EN「FACILITY」（11px・太字・大文字・字間 0.6px）≒ 65px ＋ padding 24px ＝ 約 89px なので 110px に収まる。中身は `overflow-wrap: anywhere` で折り返す（★長い施設名は1行増えることがある。承知のうえ★）

### 7-3. 古いコメントを直す

| 場所 | 今 | 直す |
|---|---|---|
| `CleanLogTable.module.css:10` | 施設名 126 は「画面幅 1440px で表 1151px」に収まる値（126 + 1024 = 1150。…） | ★施設名 110 は、画面幅 1440px で縦スクロールバー（15〜17px）が出ても収まる値（110 + 1024 = 1134。入れ物は 1154px、スクロールバーが出ると 1137〜1139px）★。2026-10-19 に 126 → 110（11px の横スクロールを消すため。指示書AX 段階7） |
| `CleanLogTable.module.css:26` | 表の最小幅 1150px を変えないため | ★表の最小幅 1134px を増やさないため★ |
| `CleanLogTable.module.css:33-34` | 通常表示 = 126 + 1024 = 1150px（画面幅 1440px のとき表は 1151px なので横スクロールは出ない…） | ★通常表示 = 110 + 1024 = 1134px（画面幅 1440px のとき入れ物は 1154px、縦スクロールバーが出ると 1137〜1139px なので横スクロールは出ない…）★ |
| `FacilitiesScreen.module.css:43` | 親の幅 1151px を既に超えて… | ★親の幅 1154px（縦スクロールバーが出ると約 1139px）を既に超えて…★（★値の `min-width: 1300px` は変えない★） |
| `CleanAreaScreen.module.css:209` | 切替前の実測（画面幅1440px・表の幅1151px） | ★過去の実測なので数値は変えない★。直後に「（縦スクロールバーが出ると約 1139px）」を足すだけ |
| `DsLogScreen.module.css:83` | 画面幅1440px 時の親の幅は約1150px なので | 「約1150px（縦スクロールバーが出ると約1139px）」に（最小幅 800px なので結論は同じ） |

★D-178 の決定文（`docs/decisions.md:12901` 以降）にある「最小幅 1150 を変えないため」は本書では直さない★（`docs/decisions.md` は D-180 の追記だけ。§13 の下書きに「D-178 の 1150 は 1134 に読み替える」と書いてある）。

### 7-4. 範囲外（触らない・報告だけ）

施設ロボットマスタ（`FacilitiesScreen`）の表は `min-width: 1300px` で、画面幅 1440px では約 161px はみ出す（既知。コメント `:43` のとおり）。★本書では直さない★。

---

## 段階8 — 資料（本書で直すのは次だけ）

1. `docs/fixtures.md`:
   - §1（`:21`）の `ds-alerts.ts` を消す
   - §6（`:293-305`）を書き換える: ★D-Sec アラートは `errorReportRows` の `D-S_CONTROL_FAILURE` 行から作る（7行。§13 の FX-ERR-10・12〜17）★／状態3つ・備考あり／なし・`errored_at` NULL・シリアルと施設の食い違い・同時刻／★`security_off` は fixtures にも無い（D-180）★／★「disconnected × done で0件」の意図的な0件は無くなった★（0件を見たいときは分類で絞る）
   - §12（`:444-451`）: `security_banner` の「複数件」の行に「★fixtures・DB とも `security_off` が無いので出ない（D-180）。描画は `aggregate.test.ts` で検証★」、`:451` の `section_name` の注記を消す
   - §13（`:503-`）: ★19行（見える行17）★・既定14・全17、FX-ERR-12〜19 の行（★FX-ERR-18・19 は削除済みの施設・清掃エリアなので、エラー一覧にも D-Sec にも出ない。U8★）、FX-ERR-08・11 のシリアル `---`、既定の並び（★表の時刻から手で導く★）
2. `d-watch-api.yaml` / `lib/types/index.ts` / ルートの冒頭コメント（段階2・4 で書いたもの）
3. ★`docs/decisions.md` に D-180 を追記★（§13 の下書き。★追記直前に `grep -n "^### D-1[78][0-9]" docs/decisions.md` で最終番号を確かめる★。D-179 の本文は触らない。★他のセッションの未コミットの差分を自分のコミットに含めない — `git add -p` で D-180 の塊だけを選ぶか、D-179 がコミットされるのを待つ★）

★`CLAUDE.md` / `docs/code-map.md` / `docs/spec.md` / `docs/handoff.md` は監督が直す★（§12 の監督向けの申し送り）。

---

## 段階9 — 全体の確認

1. `npx tsc --noEmit -p .` — 0件
2. `npm run lint` — 0件（★`eslint-disable` を足していない★・i18n の重複キー・`$queryRaw`・`any`・未使用の import が無い）
3. `npm test` — ★全件通過★（段階0 の件数から増えている）
4. ★`USE_FIXTURES=true` で `npm run build`★ — 成功（★ワークツリーではなく本体で★。Turbopack が node_modules のジャンクションを拒否する）
5. 最後の grep（★0件になること★。ただし `$queryRaw` は既存の「使わない」と書いたコメントだけ）:
   ```
   grep -rn "controlErrorHistory" app lib components --include=*.ts --include=*.tsx | grep -v generated | grep -v __tests__
   grep -rn "resolutionStatusFromControlErrorStatus\|controlErrorStatusFromResolutionStatus\|DsAlertStatus\b" app lib components scripts
   grep -rn "\"pending\"\|\"done\"" lib/ds-alerts components/features/ds-alerts lib/dashboard
   grep -rn "memo" lib/types/index.ts lib/ds-alerts components/features/ds-alerts lib/db/ds-alert-mapping.ts
   grep -rn "robot_id: true\|robot: true\|robot: {" lib/db/robot-error-select.ts
   grep -rn "ds-alerts/{id}/status" d-watch-api.yaml
   grep -rn "\$queryRaw\|\$executeRaw" app lib components
   ```

---

## 段階10 — 画面での確認

★**アプリ内ブラウザ（`mcp__Claude_Browser__*`）を使う。Edge（ヘッドレス・CDP を含む）は使わない**★（ユーザー指示 2026-10-16）。
★**ユーザーの `:3000`（`next dev`。ステージング向け）は止めない**★。

### 10-1. fixtures（`:3100`）

1. 本体で `USE_FIXTURES=true npm run build`（PowerShell: `$env:USE_FIXTURES='true'; npm run build`）
2. ★テスト用の `AUTH_TOKEN` をその場で生成★（例: Node の `crypto.randomBytes(24).toString("hex")`）し、★環境変数で渡して★ `USE_FIXTURES=true npx next start -p 3100` で起動（`process.env` は `.env.local` より優先）。★トークンを文書・コミット・報告に書かない★
3. `preview_start` の `url` で `http://localhost:3100` を開き、ログイン画面にそのトークンを入れる
4. 確かめること（`read_page` / `get_page_text` / `javascript_tool` / スクリーンショット）:
   - ★D-Sec アラート★: 既定で ★5件★、数値カード ★未解消3・対応中2・解消2★（表4 の文言）。`FX-ERR-16` が施設3の名前で出る。`FX-ERR-12` の検知時刻が `created_at`（当日 02:55）。★「セキュリティ無効」のカードが無い★。★種別のセレクトは残っていて、「セキュリティ無効」を選ぶと0件・「通信断」で5件★（U6・表2 A5）
   - 状態ボタン「全件」で7件、「解消」で2件
   - ★削除済み（U8）★: `/errors`（「解消済みも表示」オンでも）と `/ds-alerts`（「全件」でも）に ★FX-ERR-18・19 が出ない★。`javascript_tool` で `fetch("/api/errors/FX-ERR-18/status", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: "in_progress" }) })` → ★404★（fixtures の上書き表だけなので実害なし）
   - ★編集モード★: `編集` → `FX-ERR-10` を「解消」・`FX-ERR-12` に備考 → `保存（2件）` → 通常モードに戻り、★`FX-ERR-10` は「未解消・対応中」の表示のまま残る★（取り直すまで）・数値カードが ★未解消2・対応中2・解消3★ に変わる（保存前は 3・2・2。`FX-ERR-12` は備考だけなので数は変わらない）
   - ★同じ行がエラー一覧にも反映★: `/errors` で `FX-ERR-10` が「解消」・`FX-ERR-12` の備考が出る。★逆に `/errors` で `FX-ERR-13` を「未解消」にすると `/ds-alerts` に反映★
   - 編集中: 絞り込み・ページ送りが無効、下書きがある状態でリロードすると確認が出る（`beforeunload`）、キャンセルで確認 → 下書きが消える
   - ★JP/EN 切替★で、ボタン・プルダウンの位置が動かない。EN で「未対応」が "Unsupported" と出ない
   - ★清掃詳細ログ（段階7）★: `resize_window` で 1440×900 にし、★行が多くて縦スクロールが出ている状態★（期間を広げる・`per_page` を最大に）で、JP と EN の両方で:
     ```js
     const w = document.querySelector('[class*="tableWrap"]');
     const main = document.querySelector('main') ?? document.querySelector('[class*="main"]');
     ({ scroll: w.scrollWidth, client: w.clientWidth, scrollbar: main.offsetWidth - main.clientWidth })
     ```
     ★`scroll === client`★ であること。★`scrollbar` が 0 なら縦スクロールバーが出ていない（オーバーレイ型）ので、そのことを報告する★（それでも `scroll === client` を確かめる）。終わったら `resize_window` を `desktop` に戻す
5. ★終わったら :3100 の listen プロセスを止める★（自分で起動したものだけ）

### 10-2. ローカルDB（`dwatch_local`。条件付き）

`npm run seed:local`（★ローカル以外を拒否する★）→ `USE_FIXTURES=false` で ★ホストが localhost・DB 名が `dwatch_local` であることを検査してから★ `next start -p 3100`:
- `/ds-alerts` に7件（既定5件）。1行を「対応中 → 解消 → 未解消」・備考を入れて空に → サーバーのログに `db.write` が `success: true`・`route` が `/api/errors/[error_id]/status`・備考の中身なし。最後は元の値に戻す

---

## 段階11 — ステージングで確かめる（★条件付き。満たさなければ止まって報告★）

**着手の条件（すべて）**: ステージングに接続できる（★別の認証情報を探したり試したりしない★）／`D-S_CONTROL_FAILURE` の行が3行ある（2026-10-19 時点。★件数が違えば、書かずに件数だけ報告★）。

**手順**（★ステージングへの4カラムの書き込みはユーザーが許可済み。D-152 / `CLAUDE.md` §3★）:
0. ★削除済みの施設・清掃エリアに紐づく行を数える（読むだけ）★: `robot_error_reports` を `facilities` / `sections` と結合し、どちらかの `deleted_at` が NULL でない行の件数と `error_type` を記録する。★0件でなければ、下の「3行」等の期待値はその分減る★（減った数と一致すれば合格。食い違えば書かずに報告）
1. ユーザーの `:3000` 経由（止めない）。★`/ds-alerts` が3行（既定で未解消の2行、「全件」で3行・解消1行）★、★1000004 が施設3で出る★、検知時刻が `created_at`（16行すべて `errored_at` NULL）、★`SECURITY_ON`・`NOT_STARTED` が出ない★
2. ★書き込むのは1行だけ★。未解消・`responded_at` / `resolved_at` / `comment` が空の D-Sec の行を選ぶ
3. ★操作前の値を記録する★: `error_id` と4列。★`mysql` で SELECT するだけ★（UPDATE / DELETE / INSERT を手で打たない。パスワードは表示しない）
4. ★D-Sec 画面の編集モードで★: 対応中 → 保存 → 解消 → 保存 → 未解消 → 保存、備考を入れて保存 → 空にして保存。★途中でエラー一覧（`/errors`）にも同じ状態が出ること★を見る
5. ★最後に4列が操作前と完全に一致すること★（`updated_at` は DB が更新するので戻らない — 値を記録して報告。AT §10 #8 で受け入れ済み）
6. ★途中でデータが入れ替わったら止めて報告★（第10回 N-6）。★他の行に書いて代わりにしない★
7. ★`npm run seed:local` をステージングに向けない★

---

## 8. テスト一覧（本書で足す・直すもの）

| ファイル | 内容 | 段階 |
|---|---|---|
| `lib/db/__tests__/ds-alert-mapping.test.ts`（新規） | 表1・`dsAlertWhere()` | 1 |
| `lib/ds-alerts/__tests__/query.test.ts` | 語彙・`status=resolved`・`done` は422・文字列の並び・`summary` のキー | 1 |
| `lib/ds-alerts/__tests__/edit.test.ts`（新規） | 表3 | 1 |
| `lib/dashboard/__tests__/aggregate.test.ts` | 型の追従（バナーのテストは残す） | 1 |
| `lib/db/__tests__/robot-error-select.test.ts` | `ROBOT_ERROR_DS_ALERT_SELECT` を足す | 1 |
| `lib/db/__tests__/ds-status.test.ts` | 制御盤エラーの変換のテストを削る | 1 |
| `lib/db/__tests__/fixture-error-reports.test.ts` | 17行・既定14・シリアル `---`・`robot_id` NULL の行 | 3 |
| `app/api/errors/__tests__/route.fixtures.test.ts` | 件数 | 3 |
| `app/api/ds-alerts/__tests__/route.fixtures.test.ts`（新規） | fixtures で GET → `PATCH /errors` → GET に反映 | 3 |
| `app/api/ds-alerts/__tests__/route.test.ts`（新規） | Prisma 経路（表1・表2・`where` / `select`・`controlErrorHistory` を呼ばない） | 4 |
| `app/api/dashboard/__tests__/route.test.ts` | `controlErrorHistory` のスタブを消す・`status` 3 でも200 | 4 |
| `app/api/__tests__/master-filter.test.ts` | `summary` のキー | 4 |
| `lib/db/__tests__/error-report-visibility.test.ts`（新規） | `isVisibleErrorRow()`・`ERROR_ROW_VISIBLE_WHERE`（表5） | 1 |
| `lib/db/__tests__/error-report-mapping.test.ts` | 行の型の追従・`visible`・`errorReportListWhere()` に可視性の2キー | 1 |
| `lib/error-reports/__tests__/query.test.ts` | `visible: false` のキーが外れ `total` に数えない | 1 |
| `app/api/errors/__tests__/route.test.ts` | Prisma 経路で表5 V1〜V3・V8・V9 | 4 |
| `app/api/errors/[error_id]/status/__tests__/route.test.ts` | 削除済みの施設・清掃エリアの行は404・`updateMany` を呼ばない | 4 |
| ★無修正で通ること★ | `app/api/prechecks/**`・`lib/prechecks/__tests__/*`・`lib/db/__tests__/robot-error-write-sql.test.ts`・`lib/db/__tests__/precheck-mapping.test.ts` | — |

---

## 9. 触ってよいもの・触ってはいけないもの

### 触ってよい

`lib/config/index.ts`（定数1つ）・`lib/types/index.ts`（`DsAlert` まわり）・`lib/db/robot-error-select.ts`（定数1つと、既存2定数への `deleted_at` の追加）・`lib/db/ds-alert-mapping.ts`（新規）・`lib/db/index.ts`（`dsAlerts()` 2本と import、★U8 の `errorReportById()` 2本・`errorReportItemsInPageOrder()`★）・
★`lib/db/error-report-mapping.ts`（2-7 の可視性の型・`isVisibleErrorRow()`・`ERROR_ROW_VISIBLE_WHERE`・`errorReportListWhere()` だけ）★・★`lib/error-reports/query.ts`（`filterSortErrorReportKeys()` の `visible` の1行だけ）★・
`lib/db/ds-status.ts`（2関数の削除）・`lib/ds-alerts/query.ts`・`lib/ds-alerts/edit.ts`（新規）・`lib/dashboard/aggregate.ts`（★`!== "done"` → `!== "resolved"` の1か所とコメントだけ★）・
`lib/fixtures/error-reports.ts`・`lib/fixtures/ds-alerts.ts`（削除）・`lib/fixtures/index.ts`（1行）・`app/api/ds-alerts/route.ts`・`app/api/dashboard/route.ts`（コメントだけ）・
`components/features/ds-alerts/DsAlertsScreen.tsx`・`.module.css`・`lib/i18n.ts`・`scripts/seed-local-db.ts`（段階6 の範囲だけ）・
`components/features/detail-log/CleanLogTable.module.css`（段階7）・`FacilitiesScreen.module.css` / `CleanAreaScreen.module.css` / `DsLogScreen.module.css`（★コメントだけ★）・
`d-watch-api.yaml`・`docs/fixtures.md`・`docs/decisions.md`（★D-180 の追記だけ★）・§8 のテスト

### 触ってはいけない

- ★`CLAUDE.md`★（監督が直す。いま別のセッションが編集中）・★`docs/decisions.md` の D-179 とそれ以前の本文★・`docs/code-map.md`・`docs/spec.md`・`docs/handoff.md`
- ★`docs/handoff/TeamWorks確認事項_第13回_*`・`docs/handoff/回答待ち一覧_*`・`docs/handoff/pdf/*`★（他のセッションの作業）
- `d-watch-mock.jsx`（モックは書き換えない。U5 の文言の食い違いは承知のうえ。ユーザー確定）
- ★`prisma/schema.prisma` / `lib/generated/prisma/**`★（`db pull` しない。D-172。`RobotErrorReport.updated_at` に `@updatedAt` を付けない）
- ★`lib/db/robot-error-write.ts`・`app/api/errors/**` のルート・`lib/prechecks/status-update.ts`・`lib/prechecks/edit-drafts.ts` の中身★（書き込みの規則は1本。D6）
- `lib/db/precheck-mapping.ts` の中身（`resolutionStatusFromRobotErrorStatus()` 等は使うだけ）・`lib/db/error-report-mapping.ts` の ★2-7 以外★（`errorScheduleLink()` / `errorReportItemFromRow()` の規則）
- ★施設名・分類の解決（`lib/facilities/resolve-by-name.ts` / `resolve-by-master.ts` / `lib/db/facility-name-source.ts` / `facility-group-source.ts`）★（`deleted_at` で絞らない＝罠7 を変えない）
- ★`/alerts` の読み取り（`prechecks()` / `precheckItemById()`）★（U8 は範囲外。§12-2）
- `components/features/errors/**`・`components/features/alerts/**`（写すだけ。変えない）
- `dsDevices()` / `/ds-logs` / `lib/fixtures/ds-devices.ts`（D-Sec ログは影響なし）
- `computeSecurityBanner()` の中身（`"done"` → `"resolved"` の1か所以外）・`components/features/dashboard/**`
- `lib/db/local-guard.ts`
- ★4カラム以外への書き込み・生SQL・`eslint-disable` の追加★
- ★認証情報（パスワード・接続文字列・DB のユーザー名・生成したトークン）を文書・コード・テスト・コミット・作業報告に書くこと★
- ★`git add -A`★

---

## 10. 範囲外（本書ではやらない）

- ★ダッシュボードのセキュリティバナーを廃止する／別の意味（例: 未解消の D-S_CONTROL_FAILURE）に転用する★（コードを残すことはユーザー確定。U7・§11 Q3）
- ★`facilities.contract` による D-Sec メニュー・データの出し分け★（`CLAUDE.md` §4.6。P2）
- エラーの区分（ロボット／制御盤／セキュリティ／清掃開始失敗）を画面に作ること（D-160 のまま生値）
- `prisma db pull`（`robot_id` NULL 可・`serial_number` の拡張の取り込み。D-172）
- 関数名・型名の一般化（`PrecheckStatus` → `ResolutionStatus` 等。指示書AW §10 と同じ）
- 施設ロボットマスタの表の横スクロール（段階7-4）
- D-Sec 画面のサーバー側ページ分け・SQL への絞り込みの降ろし込み（行数が少ないので今のまま）
- ★`/alerts`（事前チェック＋出発失敗）で削除済みの施設・清掃エリアの行を外すこと★（U8 はエラー一覧と D-Sec だけ。§12-2 で監督が確かめる）
- 施設名・分類の解決、清掃詳細ログ・レポート・清掃面積など、他の画面の `deleted_at` の規則（罠7 を含む）

---

## 11. 質問と回答（2026-10-19）

★**Q1〜Q3・Q5〜Q8 は確定し、本文に反映済み。未確定は Q4 だけ**★。

### 11-1. ユーザーへの質問（監督経由）

| # | 論点 | 具体例 | 回答 | 反映した場所 |
|---|---|---|---|---|
| Q1 | D-Sec 画面の対応状況の文言をエラー一覧に揃えるか、モックの文言（未対応 / 対応中 / 対応済み）を残すか | ステージングの同じ行（例: 1000004）が、エラー一覧では「未解消」、D-Sec 画面では「未対応」と出る。★今の D-Sec 画面は EN で「未対応」が "Unsupported" と出ている★ | ★確定: 揃える（未解消 / 対応中 / 解消）★（ユーザー 2026-10-19） | §2 U5 / 表4 / 段階5-1-2 / 5-3 / 段階10 |
| Q2 | D-Sec 画面の種別セレクトと副題 | DB からは「通信断」しか出ないので、「セキュリティ無効」を選ぶと必ず0件 | ★確定: そのまま残す（0件になるのは承知のうえ）★（ユーザー 2026-10-19）。★本書の旧暫定（外す・副題を変える）は撤回★ | §2 U6 / 表2 A5 / 表4 / 段階5-1-2 / 5-3 / 段階10 / §13 |
| Q3 | ダッシュボードのセキュリティバナー（赤い警告帯）と「D-Sec N件」の Pill | U1 により DB のデータでは ★二度と出ない★。`docs/spec.md:34` は F-01 の機能として挙げている | ★確定: コードは残す★（ユーザー 2026-10-19）。廃止・転用は範囲外 | §2 D4・U7 / 段階4-1-6 / §10 |
| Q4 | 削除済み（`deleted_at`）の施設・清掃エリアのアラートを出すか | 旧 D-Sec 実装は `controls.deleted_at` で絞っていた。★`facilities` / `sections` / `controls` は `deleted_at` の論理削除なので行は残り、絞らなければそこに紐づくエラー行も返ってくる★（ユーザーの「削除済みなら DB にいないので返ってこないのでは」への答え） | ★確定（ユーザー 2026-10-19）★:「削除済みのものは表示する必要がないので、エラー一覧からもD-sec一覧からも消してください」→ ★エラー一覧と D-Sec の両方で外す。PATCH は404★（U8）。旧暫定「絞らない」は破棄 | §0 #8 / §2 U8・D12 / 表5 / 4-21〜4-24 / 段階1-7〜11 / 2-7 / 3-1-4 / 3-3b / 3-4 / 4-4 / 段階6 / 段階8 / 段階10・11 / §8 / §9 / §10 / §13 / §15 |

### 11-2. 監督への質問 → ★すべて本書のとおりで確定（監督判断 2026-10-19）★

| # | 論点 | 決定 | 反映した場所 |
|---|---|---|---|
| Q5 | 表1 の R8〜R10・R12・R13、表2（A1〜A9）、表3（S1〜S6）、fixtures の件数（エラー一覧 17行・既定14／D-Sec 7行・既定5・`summary` 3/2/2） | ★正とする★ | §3 / 3-1 / 3-4 / 段階8 / 段階10 |
| Q6 | `DsAlert.memo` → `note` の改名（D9） | ★改名する★ | §2 D5・D9 / 2-2 / 4-1-4 / 段階5 |
| Q7 | `status` パラメータの `done` → `resolved`・`summary` のキーの改名（D10） | ★改名する。旧 `done` は422★ | §2 D10 / 2-5 / 4-1-2・5 / 表2 A4 |
| Q8 | シード（段階6）を本書に含めるか | ★含める★（`robot_error_reports` に D-Sec 7行を入れる） | 段階6 / 段階10-2 |

### 11-3. 監督への質問（★U8 で増えたもの。段階1 の前に要る★）

| # | 論点 | 本書の暫定 | 直す場所 |
|---|---|---|---|
| Q9 | ★表5（V1〜V9）と fixtures の FX-ERR-18・19（`errorReportRows` 19行・見える行17・D-Sec 7行は変わらず）を正にしてよいか★。特に ★V9（清掃エリア単位で決まる）★・★`deleted_at` は NULL か否かだけで決める（未来の日時でも削除済み）★・★FX-ERR-18 の施設は `facilities` の fixture に足さないダミー（id 99）★ | 本書で U8 から導いた値。★本書のとおりで確定（監督判断 2026-10-19）★ | 表5 / 3-1-4・5 / 3-4 / 段階8 / 段階10 |

---

## 12. 申し送り

### 12-1. TeamWorks へ（★監督が確認事項にまとめる。本書の実装では送らない・`docs/handoff/TeamWorks確認事項_第13回_*` は触らない★）

1. ★`error_id` 1000004（`D-S_CONTROL_FAILURE`）★: シリアル `DFAS010001` は施設2の制御盤だが、行の `facility_id` / `section_id` は 3（2026-10-19 監督の SELECT・先方投入）。
   当方は P-8 d = A のとおり ★行の施設（3）で出す★。★どちらが正しいか（行の施設の誤りか、制御盤の付け替えか）★を確かめたい。
2. ★セキュリティ（`SECURITY_ON`）・清掃開始失敗（`NOT_STARTED`）の `serial_number`★: 第11回 P-8 の回答は `0` だが、ステージングの実値は ★`---`★（6行すべて）。当方は値で見分けないので影響は無いが、★今後どちらに揃うのか★を確かめたい（第10回 N-5 では「`serial_number` が null」という案もあった）。
3. ★`errored_at` がすべて NULL★（16行。2026-10-19）。当方は `created_at` で代用している。★`errored_at` は今後入るのか★（入らないなら「エラーが起きた時刻」ではなく「記録した時刻」が画面に出ている）。

### 12-2. 監督へ（`CLAUDE.md` 等、本書の実装では直さないもの）

1. `CLAUDE.md` §4.7 の取得元の表の「エラー」の行 — 「★3区分の見分けと D-Sec アラートの取得元の切り替えは次フェーズ★」を、本書の実装後に「D-Sec アラートは `robot_error_reports` の `D-S_CONTROL_FAILURE`（D-180）」に。§3 の接続先の注記の P-8 のシリアル `'0'` も実値 `---` の注記を
2. `docs/decisions.md` の D-091 項3 の「確認が必要なこと（バナーをどうするか）」は、★コードを残す★ことでユーザー確定（§11 Q3）。廃止・転用するかは未決のまま
3. `docs/code-map.md` / `docs/spec.md`（`/ds-alerts` の取得元・編集モード）
4. 指示書AW の表1 E8（シリアル `'0'`）は単体テストの行なので変えていない。fixtures と資料だけ `---` にした
5. ★`/alerts`（事前チェック＋出発失敗。`prechecks()` / `precheckItemById()` / `PATCH /prechecks/{id}/status`）は、削除済みの施設・清掃エリアの行を出しているか確かめてほしい★（本書の範囲外。U8 の「エラー一覧からもD-sec一覧からも」に `/alerts` は入っていない）。出しているなら、同じ規則（`isVisibleErrorRow()`）に揃えるかをユーザーに確かめる
6. `CLAUDE.md` に U8（削除済みの施設・清掃エリアの行はエラー一覧・D-Sec に出さない。罠7 の解決は別）を足すかどうか

---

## 13. 決定の記録（★D-180 の下書き★。実装時に `docs/decisions.md` の末尾に追記する）

```markdown
### D-180 `[決定]` D-Sec アラートを `robot_error_reports`（`D-S_CONTROL_FAILURE`）から読み、D-Sec 画面に編集モードを付ける

★根拠★: TeamWorks 第11回 P-10 = A（`robot_error_reports` を正とする。D-Sec のアラートも対応状況の更新もこちら。`control_error_histories` は読まない）／
P-8（制御盤のエラーは `robot_id` NULL・シリアルは制御盤の SN。施設は行の `facility_id`・`section_id`）／第10回 N-5（`error_type` の値）／
ユーザー判断 2026-10-19（下の1〜4・7・7b・11）。指示書AX。

1. ★`SECURITY_ON` は D-Sec アラートに出さない★（エラー一覧には出る）。D-091 項3「セキュリティが無効であっても異常ではない」と一致
2. ★`NOT_STARTED`（清掃開始失敗）も D-Sec アラートに出さない★（ロボット側の出発失敗）
3. ★施設は行の `facility_id`★（P-8 d = A）。ステージングの 1000004 はシリアルが施設2の制御盤なのに行は施設3 → ★施設3で出す★（TeamWorks に申し送り）
4. ★D-Sec アラート画面に編集モードを付ける★（エラー一覧と同じ操作。D-161 / D-165）。書くのは同じ行の4カラム。★書き込みは `PATCH /errors/{error_id}/status` の1本★（新しいルートを作らない。YAML の未実装の `PATCH /ds-alerts/{id}/status` は削除）
5. ★対象は `error_type` が `D_SEC_ALERT_ERROR_TYPES`（`lib/config/index.ts`。初期値 `["D-S_CONTROL_FAILURE"]`）に完全一致する行★。★SQL の `IN` は写し、正は JS の `===`★（照合順序が大小文字・末尾の空白を区別しないため）。★`robot_id IS NULL` やシリアルの形では見分けない★（実データのシリアルは回答の `0` ではなく `---`）。`status` は 0/1/2 の行だけ（3以上は出さず例外にもしない — ダッシュボードを落とさない）
6. ★`DsAlert` の契約★: `id` は string（`error_id`）／`resolution_status` はエラー一覧と同じ `unresolved` / `in_progress` / `resolved`（`DsAlertStatus` は削除。変換は `resolutionStatusFromRobotErrorStatus()` の1本）／`memo` → `note`／`status` パラメータは `open` / `all` / `resolved`（旧 `done` は422）／`summary` のキーも同じ／`detected_at` は `errored_at ?? created_at`／`section_name` は行の `section_id` の清掃エリア名（暫定のダミーではなくなった）
7. ★`alert_type` の `security_off` は互換のため enum に残すが、DB からは出ない★。★ダッシュボードのセキュリティバナーは DB のデータでは常に空★（★コードとテストは残す — ユーザー確定 2026-10-19★。廃止・転用は未決 — D-091 項3 の「確認が必要なこと」）
7b. ★D-Sec 画面の対応状況の文言はエラー一覧に揃える（未解消 / 対応中 / 解消）★（ユーザー確定 2026-10-19。モックの「未対応 / 対応済み」とは食い違う）。★種別のセレクト（全種別 / セキュリティ無効 / 通信断）と副題はそのまま残す★（ユーザー判断 2026-10-19。「セキュリティ無効」を選ぶと0件になることは承知のうえ）
8. `control_error_histories` はアプリから読まない（シードも書かない。`deleteMany` だけ残す）。`lib/db/ds-status.ts` の制御盤エラーの変換2関数は削除
9. fixtures: D-Sec アラートは `errorReportRows` の `D-S_CONTROL_FAILURE` 行（7行）から同じ純粋関数で作る（`lib/fixtures/ds-alerts.ts` は削除）。セキュリティ・清掃開始失敗のシリアルを実データどおり `---` に
10. （別件）清掃詳細ログの施設名の列を 126px → 110px（表の最小幅 1150 → 1134px）。画面幅 1440px で縦スクロールバーが出ると入れ物が 1139px になり、11px 横スクロールしていた。★D-178 の「最小幅 1150 を変えないため」は 1134 に読み替える★
11. ★削除済み（論理削除）の施設・清掃エリアの行は、エラー一覧（`GET /errors`・`meta.total`）からも D-Sec アラート（`GET /ds-alerts`・`summary`・ダッシュボード）からも外す。その行への `PATCH /errors/{error_id}/status` は404★（ユーザー決定 2026-10-19「削除済みのものは表示する必要がないので、エラー一覧からもD-sec一覧からも消してください」）。★`deleted_at` が NULL でなければ削除済み★。正は `isVisibleErrorRow()`、SQL の `ERROR_ROW_VISIBLE_WHERE` は写し。★施設名・分類の解決は従来どおり `deleted_at` で絞らない（罠7）★。★`/alerts` は範囲外（出しているかは要確認）★

★`memo` → `note`・`done` → `resolved`（旧 `done` は422）・期待値表（指示書AX §3）・シードの手当ては監督判断 2026-10-19★。
★ユーザー回答待ちの暫定は無い★（指示書AX §11 Q1〜Q4 はすべて確定）。
```

（★実装の記録（コミット・確認の結果）は、実装後に D-180 の末尾に「実装の記録」として足す★）

---

## 14. コミット

段階ごとに分ける（段階1 / 段階2 / 段階3 / 段階4 / 段階5 / 段階6 / ★段階7（別件）★ / 段階8）。★`git add` はファイルパスを明示する★（`git add -A` 禁止）。
メッセージには ★何を変えたか★ と ★なぜ（P-10 = A。D-Sec アラートを `robot_error_reports` から読む。D-180）★ を書く。★段階1 は「テストだけ（この時点では落ちる）」と明記★。
★`docs/decisions.md` は D-180 の塊だけをコミットする（D-179 の未コミットの差分を巻き込まない）★。
★PR の作成とマージは daichi がレビューしてから★（D-154）。

---

## 15. 完了条件

- [ ] §11 の確定（Q1〜Q8）どおりに実装した。★Q9（表5）の監督の確認を取ってから段階1 に入った★
- [ ] ★削除済みの施設・清掃エリアの行（表5 V2・V3・V5〜V7）が、エラー一覧（`meta.total` を含む）にも D-Sec アラート（`summary` を含む）にも出ない。PATCH は404で `updateMany` を呼ばない★（fixtures の FX-ERR-18・19 で画面と API の両方を確かめた）
- [ ] 施設名・分類の解決（罠7）と `/alerts` の読み取りは変えていない
- [ ] D-Sec 画面の種別セレクトと副題が今のまま残っている（U6）
- [ ] `tsc` 0件・`lint` 0件・`npm test` 全件通過（件数が増えている）・`USE_FIXTURES=true npm run build` 成功
- [ ] 段階9 の grep がすべて0件（`$queryRaw` は既存のコメントだけ）
- [ ] アプリが `control_error_histories` を読まない。`robot_error_reports` の読み取りは定数の `select` だけ（`robot_id` / `robot` を含まない）
- [ ] 書き込み経路は `PATCH /errors/{error_id}/status` の1本（新しいルート・`DataSource` の書き込みメソッドが無い）。YAML に `/ds-alerts/{id}/status` が無い
- [ ] fixtures（:3100）で段階10-1 の項目をすべて確かめた（D-Sec 5件・編集モードの保存・エラー一覧との相互反映・JP/EN）
- [ ] 清掃詳細ログ: 1440px・縦スクロールあり・JP/EN で `tableWrap.scrollWidth === clientWidth`
- [ ] （条件が揃えば）ステージングで3行が出て、1行の往復の後に4列が操作前と完全に一致した
- [ ] `docs/fixtures.md`・YAML・型のコメントを直し、`docs/decisions.md` に D-180 を追記した（D-179・`CLAUDE.md` には触っていない）
- [ ] 作業報告に: コミット一覧／テスト件数（前後）／画面確認の結果（出所: 当方の fixtures 由来・当方のシード由来・先方投入 を書き分ける。`CLAUDE.md` §6-6）／本書と違う判断をしたところ
